Drawing diagonal lines on an image - python

Hi im trying to draw diagonal lines across an image top right to bottom left here is my code so far.
width = getWidth(picture)
height = getHeight(picture)
for x in range(0, width):
for y in range(0, height):
pixel = getPixel(picture, x, y)
setColor(pixel, black)
Thanks

Most graphic libraries have some way to draw a line directly.
In JES there is the addLine function, so you could do
addLine(picture, 0, 0, width, height)
If you're stuck with setting single pixels, you should have a look at Bresenham Line Algorithm, which is one of the most efficient algorithms to draw lines.
A note to your code: What you're doing with two nested loops is the following
for each column in the picture
for each row in the current column
set the pixel in the current column and current row to black
so basically youre filling the entire image with black pixels.
EDIT
To draw multiple diagonal lines across the whole image (leaving a space between them), you could use the following loop
width = getWidth(picture)
height = getHeight(picture)
space = 10
for x in range(0, 2*width, space):
addLine(picture, x, 0, x-width, height)
This gives you an image like (the example is hand-drawn ...)
This makes use of the clipping functionality, most graphics libraries provide, i.e. parts of the line that are not within the image are simply ignored. Note that without 2*width (i.e. if x goes only up to with), only the upper left half of the lines would be drawn...

I would like to add some math considerations to the discussion...
(Just because it is sad that JES's addLine function draws black lines only and is quite limited...)
Note : The following code uses the Bresenham's Line Algorithm pointed out by MartinStettner (so thanks to him).
The Bresenham's line algorithm is an algorithm which determines which order to form a close approximation to a straight line between two given points. Since a pixel is an atomic entity, a line can only be drawn on a computer screen by using some kind of approximation.
Note : To understand the following code, you will need to remember a little bit of your basic school math courses (line equation & trigonometry).
Code :
# The following is fast implementation and contains side effects...
import random
# Draw point, with check if the point is in the image area
def drawPoint(pic, col, x, y):
if (x >= 0) and (x < getWidth(pic)) and (y >= 0) and (y < getHeight(pic)):
px = getPixel(pic, x, y)
setColor(px, col)
# Draw line segment, given two points
# From Bresenham's line algorithm
# http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
def drawLine(pic, col, x0, y0, x1, y1):
dx = abs(x1-x0)
dy = abs(y1-y0)
sx = sy = 0
#sx = 1 if x0 < x1 else -1
#sy = 1 if y0 < y1 else -1
if (x0 < x1):
sx = 1
else:
sx = -1
if (y0 < y1):
sy = 1
else:
sy = -1
err = dx - dy
while (True):
drawPoint(pic, col, x0, y0)
if (x0 == x1) and (y0 == y1):
break
e2 = 2 * err
if (e2 > -dy):
err = err - dy
x0 = x0 + sx
if (x0 == x1) and (y0 == y1):
drawPoint(pic, col, x0, y0)
break
if (e2 < dx):
err = err + dx
y0 = y0 + sy
# Draw infinite line from segment
def drawInfiniteLine(pic, col, x0, y0, x1, y1):
# y = m * x + b
m = (y0-y1) / (x0-x1)
# y0 = m * x0 + b => b = y0 - m * x0
b = y0 - m * x0
x0 = 0
y0 = int(m*x0 + b)
# get a 2nd point far away from the 1st one
x1 = getWidth(pic)
y1 = int(m*x1 + b)
drawLine(pic, col, x0, y0, x1, y1)
# Draw infinite line from origin point and angle
# Angle 'theta' expressed in degres
def drawInfiniteLineA(pic, col, x, y, theta):
# y = m * x + b
dx = y * tan(theta * pi / 180.0) # (need radians)
dy = y
if (dx == 0):
dx += 0.000000001 # Avoid to divide by zero
m = dy / dx
# y = m * x + b => b = y - m * x
b = y - m * x
# get a 2nd point far away from the 1st one
x1 = 2 * getWidth(pic)
y1 = m*x1 + b
drawInfiniteLine(pic, col, x, y, x1, y1)
# Draw multiple parallele lines, given offset and angle
def multiLines(pic, col, offset, theta, randOffset = 0):
# Range is [-2*width, 2*width] to cover the whole surface
for i in xrange(-2*getWidth(pic), 2*getWidth(pic), offset):
drawInfiniteLineA(pic, col, i + random.randint(0, randOffset), 1, theta)
# Draw multiple lines, given offset, angle and angle offset
def multiLinesA(pic, col, offsetX, offsetY, theta, offsetA):
j = 0
# Range is [-2*width, 2*width] to cover the whole surface
for i in xrange(-2*getWidth(pic), 2*getWidth(pic), offsetX):
drawInfiniteLineA(pic, col, i, j, theta)
j += offsetY
theta += offsetA
file = pickAFile()
picture = makePicture(file)
color = makeColor(0, 65, 65) #pickAColor()
#drawline(picture, color, 10, 10, 100, 100)
#drawInfiniteLine(picture, color, 10, 10, 100, 100)
#drawInfiniteLineA(picture, color, 50, 50, 135.0)
#multiLines(picture, color, 20, 56.0)
#multiLines(picture, color, 10, 56.0, 15)
multiLinesA(picture, color, 10, 2, 1.0, 1.7)
show(picture)
Output (Painting by Pierre Soulages) :
Hope this gave some fun and ideas to JES students... And to others as well...

Where does your picture object comes from? What is it? What is not working so far? And what library for image access are you trying to use? (I mean, where do you get, or intend to get "getWidth, getHeight, getPixel, setColor) from?
I think no library that gives you a "pixel" as a whole object which can be used in a setColor call exists, and if it does, it would be the slowest thing in the World - maybe in the galaxy.
On the other hand, if these methods did exist and your Picture, the code above would cover all the image in black - you are getting all possible "y" values (from 0 to height) inside all possible x values (from 0 to width) of the image, and coloring each Black.
Drawing a line would require you to change x, and y at the same time, more like:
(using another "imaginary library", but one more plausible:
for x, y in zip(range(0, width), range(0, height)):
picture.setPixel((x,y), Black) )
This would sort of work, but the line would not be perfect unless the image was perfectly square - else it would skip pixels in the widest direction of the image. To solve that a more refined algorithm is needed - but that is second to you have a real way to access pixels on an image - like using Python's Imaging Library (PIL or Pillow), or pygame, or some other library.

Related

Epipolar geometry opencv python

I have a set of two images of the sae scene, taken from different views, and I'm trying to use the epipolar geometry to compute the distance of the object to the camera.
Considering a point on the left image, I'm trying to find the matching point on the right image. To do this, I compute the fundamental matrix of the scene, then I compute the epipolar line of the point on the right image. After that, I think I would be able to find the matching point on the line by testing point of the line and use the condition x.T # F # x' = 0.
The problem is that the point I find is wrong. When I draw the epiline on the right image, it semms perfect as the matching point belongs to it. But when I make sure my fundamental matrix is right, the condition mentionned before is never right, the result is far from 0.
Here is my code:
discret = 500
pointg = np.array([mx, my, 1])
pts1 = np.array([[mx, my]])
lines2 = cv2.computeCorrespondEpilines(pts1, 1, F)
lines2 = lines2.reshape(-1, 3)
r, c, rien = frame1.shape
x0, y0 = map(int, [0, -lines2[0, 2] / lines2[0, 1]])
x1, y1 = map(int, [c, -(lines2[0, 2] + lines2[0, 0] * c) / lines2[0, 1]])
a = (y1 - y0) / (x1 - x0)
x, y = x0, y0
k = (x1 - x0) / discret
matd = np.array([[x], [y], [1]])
produit = (pointg # F) # matd
mini = produit
x_selec, y_selec = x, y
for i in range(discret):
x += k
y = a * x + y0
matd = np.array([[x], [y], [1]])
produit = (pointg # F) # matd
if abs(produit) < mini:
mini = produit
x_selec, y_selec = int(x), int(y)
pointd = np.array([x_selec, y_selec, 1])
(frame1 is an image read with opencv)
Since the result will surely not be 0, I'm trying to find the minimum of the values, but the programm returns x_selec, y_selec = x0, y0
And when I compute by myself the product x.T # F # x', the result is never right, despite my epiline being right.
Does anyone know how to fix this ?

Calculating area under the curves

I am attempting to calculate the area of the blue region and the area of yellow region:
In this graph: y=blue, peak_line=green, thresh=orange.
I am using this code:
idx = np.argwhere(np.diff(np.sign(y - peak_line))).flatten()
bounds = [1077.912, 1078.26, 1078.336, 1078.468, 1078.612, 1078.78, 1078.828, 1078.88, 1079.856, 1079.86]
plt.plot(x, y, x, thresh, x, peak_line)
plt.fill_between(x, y, thresh, where=(y>=peak_line),interpolate=True, color='#fff8ba')
plt.fill_between(x, thresh, peak_line, where=(y<=peak_line),interpolate=True, color='#fff8ba')
plt.fill_between(x, y, peak_line, where=(y>=peak_line) & (x>=x[idx][0]) & (x<=bounds[-1]), interpolate=True, color='#CDEAFF')
plt.plot(x[idx], y[idx], 'ro')
plt.show()
estimated_y = interp1d(x, y, kind='cubic')
estimated_peak_line = interp1d(x, peak_line, kind='cubic')
estimated_thresh = interp1d(x, thresh, kind='cubic')
yellow_areas = []
blue_areas = []
for i in range(len(bounds) - 1):
midpoint = (bounds[i] + bounds[i+1]) / 2
if estimated_y(midpoint) < estimated_peak_line(midpoint):
above_peak_line = abs(integrate.quad(estimated_peak_line, bounds[i], bounds[i+1])[0])
above_thresh_line = abs(integrate.quad(estimated_thresh, bounds[i], bounds[i+1])[0])
yellow_areas.append(above_peak_line - above_thresh_line)
else:
above_peak_line = abs(integrate.quad(estimated_peak_line, bounds[i], bounds[i+1])[0])
above_y = abs(integrate.quad(estimated_y, bounds[i], bounds[i+1])[0])
blue_areas.append(above_peak_line - above_y)
print(sum(yellow_areas))
print(sum(blue_areas))
4.900000000000318
2.999654602006661
I thought I calculated the area of the blue region and the area of yellow region correct, until I calculated the area of the polygon:
bunch_of_xs = np.linspace(min(x), max(x), num=10000, endpoint=True)
final_curve = estimated_y(bunch_of_xs)
final_thresh = estimated_thresh(bunch_of_xs)
final_peak_line = estimated_peak_line(bunch_of_xs)
def PolygonArea(corners):
n = len(corners) # of corners
area = 0.0
for i in range(n):
j = (i + 1) % n
area += corners[i][0] * corners[j][1]
area -= corners[j][0] * corners[i][1]
area = abs(area) / 2.0
return area
vertex1 = (bunch_of_xs[0], final_thresh[0])
vertex2 = (bunch_of_xs[-1], final_thresh[-1])
vertex3 = (x[idx][-1], y[idx][-1])
vertex4 = (x[idx][0], y[idx][0])
coords = (vertex1,vertex2,vertex3,vertex4)
plt.plot(x, y, 'o', bunch_of_xs, final_curve, '--', bunch_of_xs, final_thresh, bunch_of_xs, final_peak_line)
x_val = [x[0] for x in coords]
y_val = [x[1] for x in coords]
plt.plot(x_val,y_val,'or')
print("Coordinates of total polygon:", coords)
print("Total polygon area:", PolygonArea(coords))
Coordinates of total polygon: ((1077.728, -41.30177170550451), (1079.96, -42.254314285935834), (1079.86, -49.207348695828706), (1077.912, -48.271572477115136))
Total polygon area: 14.509708069890621
The sum of the area of the blue region and the area of yellow region should equal the total polygon area.
4.900000000000318 + 2.999654602006661 ≠ 14.509708069890621
What am I doing wrong?
Edit: This code will be used for many different graphs. Not all graphs look the same. For example, this graph has 3 blue regions and so I have to calculate the area of all 3 blue regions and add them together to get the total blue area. Every graph has a different amount of blue regions (some only have 1 region). So, I have to make the code flexible to account for the possibility of a graph having multiple blue regions to add together to get the total blue region area.
Since I don't have all of your data I will give something between pseudo-code and implementation.
Say we have arrays x (x-axis), y1 (data), y2 (some line which bounds the parts over which we want to integrate).
First step: Iterate over your bounds array and see which parts we want to integrate over. I assume that you have the bounds array already, as your question suggests.
def get_pairs_of_idxs(x, y1, y2, bounds):
lst_pairs = []
for i in range(len(bounds)-1):
x0, x1 = bounds[i], bounds[i+1]
xc = 0.5 * (x0 + x1) # we want to see if the straight line y2 is above or below, so we take one x value and test it
indx_xc = np.searchsorted(x, xc) # this returns us the index at which xc is located
y1c, y2c = y1[indx_xc], y2[indx_xc]
if y2c < y1c: # then the line is below the curve, so we want to integrate
lst_pairs.append((x0, x1))
Now we have a list of pairs of indices, between which we want to integrate.
def solution(x, y1, y2, bounds):
tot_area = 0
lst_pairs = get_pairs_of_idxs(x, y1, y2, bounds)
for x0, x1 in lst_pairs:
mask = np.logical_and(x >= x0, x <= x1) # relevant places in x and y data
xs = x[mask] # the x values along which we integrate
ys = (y2 - y1)[mask] # we want to integrate the difference of the curves
tot_area += np.trapz(ys, xs)
return tot_area
That's what I was thinking about.
In general, the area between two curves f(x) and g(x) is integral(g(x) - f(x)).
So say we have two curves:
xvals = np.linspace(0, 1, 100)
yvals_1 = np.sin(xvals * 10)
yvals_2 = 0.5 - 0.5 * xvals
plt.plot(xvals, yvals_1, '-b')
plt.plot(xvals, yvals_2, '-g')
The "transformed" curve becomes:
yvals_3 = yvals_1 - yvals_2
plt.plot(xvals, yvals_3, '--r')
plt.plot(xvals, np.zeros(xvals.shape), '--k')
And since we want to ignore everything under the green line,
yvals_3[yvals_3 < 0] = 0
plt.plot(xvals, yvals_3, '-r')
Since you want to impose additional constraints, such as "only the area between the first and last intersections", do that now.
# Cheating a little bit -- but you already know how to get the intersections.
first_intersection_x = xvals[4]
last_intersection_x = xvals[94]
cfilter = np.logical_and(xvals >= first_intersection_x, xvals <= last_intersection_x)
xvals_calc = xvals[cfilter]
yvals_calc = yvals_3[cfilter]
The area under this curve is easily calculated using np.trapz
area_under_curve = np.trapz(yvals_calc, xvals_calc)
Of course, this answer assumes that yvals_1 and yvals_2 are available at the same xvals. If not, interpolation is easy.

How to get extream x,y coordinates for hough line peaks with skimage

h, theta, d = transform.hough_line(outlines)
for acum, angle, dist in zip(*transform.hough_line_peaks(h, theta, d)):
y0 = (dist - 0 * np.cos(angle)) / np.sin(angle)
y1 = (dist - outlines.shape[1] * np.cos(angle)) / np.sin(angle)
x0 = ...
x1 = ...
rr,cc,_ = draw.line_aa(x0,y0,x1,y1)
What I want is the x0 and x1 values between the range of my outline shape, that is 640,640 (2D). And I want to scale the y0 and y1 to the size of my outline.shape.
The y0, y1 coordinate that you calculate with that formula correspond to where the line intersects the edges of your image. That is why it includes 0 and outlines.shape[1].
y0 corresponds to the row where the line intersects column 0, hence 0 * cos(angle).
y1 corresponds to the row where the line intersects the last column of your image, i.e. its width (which is outlines.shape[1])
So you can draw a line from (0, y0) to (width, y1) to emphasize the detected lines. Use, for example outlines[line(0, y0, width-1, y1] = 1. Note I put width - 1 because indexing starts from 0 and width is out of bounds. This is not the case in your formula because it is subtracted from dist
This tutorial illustrates well how it works and how to add the discovered lines to your image (in the first part). Replace the uninspired X-shaped image with an image of your choice and you will see the lines. Your image should ideally be binarised and have not too many points, so try passing it through and edge detector (Canny) or a skeletonization procedure.

extract (ordered) pixel values on circle given midpoint and radius using python

I have an image and want to pull out intensities of pixels that belong to a circle of radius r around a midpoint (Mx,My).
The critical part is building the list of coordinates (x,y) which define the perimeter of the circle for lookup. I adapted a version of the midpoint circle algorithm from rosetta but the points on the circle aren't ordered. Is there a smart way to obtain a list of coordinates that I can use for lookup of the intensity values in an image? Ideally avoiding numpy as I have to implement it without it (but I can use openCV). Any hints that would speed up the value lookup from the image once I have the coordinates would also be appreciated.
This is my code for the unordered list:
def circle(x0, y0, radius):
f = 1 - radius
ddf_x = 1
ddf_y = -2 * radius
x = 0
y = radius
clist=[]
while x < y:
if f >= 0:
y -= 1
ddf_y += 2
f += ddf_y
x += 1
ddf_x += 2
f += ddf_x
clist.append([x0 + x, y0 + y])
clist.append([x0 - x, y0 + y])
clist.append([x0 + x, y0 - y])
clist.append([x0 - x, y0 - y])
clist.append([x0 + y, y0 + x])
clist.append([x0 - y, y0 + x])
clist.append([x0 + y, y0 - x])
clist.append([x0 - y, y0 - x])
return clist
c=circle(10,10,5)
for i in range(len(c)):
plt.plot(c[i][0],c[i][1],'o',color=[i/50.0,1-i/50.0,1])
I am sure there are built-in functions for that in opencv as mentioned in the comments.
To get a simple ordered list of coordinates on a circle just use trigonometry.
x_perimeter = x_center + sin(angle) * radius
y_perimeter = y_center + cos(angle) * radius
Do that for reasonable angle increments and you get the desired number of points in whatever order you like.
It's for sure not the most efficient way but its simple and intuitive. All you need is a function that gives you subpixel intensities which most image processing libraries provide. Otherwise write one your own or round the coordinate values.

Generate random number outside of range in python

I'm currently working on a pygame game and I need to place objects randomly on the screen, except they cannot be within a designated rectangle. Is there an easy way to do this rather than continuously generating a random pair of coordinates until it's outside of the rectangle?
Here's a rough example of what the screen and the rectangle look like.
______________
| __ |
| |__| |
| |
| |
|______________|
Where the screen size is 1000x800 and the rectangle is [x: 500, y: 250, width: 100, height: 75]
A more code oriented way of looking at it would be
x = random_int
0 <= x <= 1000
and
500 > x or 600 < x
y = random_int
0 <= y <= 800
and
250 > y or 325 < y
Partition the box into a set of sub-boxes.
Among the valid sub-boxes, choose which one to place your point in with probability proportional to their areas
Pick a random point uniformly at random from within the chosen sub-box.
This will generate samples from the uniform probability distribution on the valid region, based on the chain rule of conditional probability.
This offers an O(1) approach in terms of both time and memory.
Rationale
The accepted answer along with some other answers seem to hinge on the necessity to generate lists of all possible coordinates, or recalculate until there is an acceptable solution. Both approaches take more time and memory than necessary.
Note that depending on the requirements for uniformity of coordinate generation, there are different solutions as is shown below.
First attempt
My approach is to randomly choose only valid coordinates around the designated box (think left/right, top/bottom), then select at random which side to choose:
import random
# set bounding boxes
maxx=1000
maxy=800
blocked_box = [(500, 250), (100, 75)]
# generate left/right, top/bottom and choose as you like
def gen_rand_limit(p1, dim):
x1, y1 = p1
w, h = dim
x2, y2 = x1 + w, y1 + h
left = random.randrange(0, x1)
right = random.randrange(x2+1, maxx-1)
top = random.randrange(0, y1)
bottom = random.randrange(y2, maxy-1)
return random.choice([left, right]), random.choice([top, bottom])
# check boundary conditions are met
def check(x, y, p1, dim):
x1, y1 = p1
w, h = dim
x2, y2 = x1 + w, y1 + h
assert 0 <= x <= maxx, "0 <= x(%s) <= maxx(%s)" % (x, maxx)
assert x1 > x or x2 < x, "x1(%s) > x(%s) or x2(%s) < x(%s)" % (x1, x, x2, x)
assert 0 <= y <= maxy, "0 <= y(%s) <= maxy(%s)" %(y, maxy)
assert y1 > y or y2 < y, "y1(%s) > y(%s) or y2(%s) < y(%s)" % (y1, y, y2, y)
# sample
points = []
for i in xrange(1000):
x,y = gen_rand_limit(*blocked_box)
check(x, y, *blocked_box)
points.append((x,y))
Results
Given the constraints as outlined in the OP, this actually produces random coordinates (blue) around the designated rectangle (red) as desired, however leaves out any of the valid points that are outside the rectangle but fall within the respective x or y dimensions of the rectangle:
# visual proof via matplotlib
import matplotlib
from matplotlib import pyplot as plt
from matplotlib.patches import Rectangle
X,Y = zip(*points)
fig = plt.figure()
ax = plt.scatter(X, Y)
p1 = blocked_box[0]
w,h = blocked_box[1]
rectangle = Rectangle(p1, w, h, fc='red', zorder=2)
ax = plt.gca()
plt.axis((0, maxx, 0, maxy))
ax.add_patch(rectangle)
Improved
This is easily fixed by limiting only either x or y coordinates (note that check is no longer valid, comment to run this part):
def gen_rand_limit(p1, dim):
x1, y1 = p1
w, h = dim
x2, y2 = x1 + w, y1 + h
# should we limit x or y?
limitx = random.choice([0,1])
limity = not limitx
# generate x, y O(1)
if limitx:
left = random.randrange(0, x1)
right = random.randrange(x2+1, maxx-1)
x = random.choice([left, right])
y = random.randrange(0, maxy)
else:
x = random.randrange(0, maxx)
top = random.randrange(0, y1)
bottom = random.randrange(y2, maxy-1)
y = random.choice([top, bottom])
return x, y
Adjusting the random bias
As pointed out in the comments this solution suffers from a bias given to points outside the rows/columns of the rectangle. The following fixes that in principle by giving each coordinate the same probability:
def gen_rand_limit(p1, dim):
x1, y1 = p1Final solution -
w, h = dim
x2, y2 = x1 + w, y1 + h
# generate x, y O(1)
# --x
left = random.randrange(0, x1)
right = random.randrange(x2+1, maxx)
withinx = random.randrange(x1, x2+1)
# adjust probability of a point outside the box columns
# a point outside has probability (1/(maxx-w)) v.s. a point inside has 1/w
# the same is true for rows. adjupx/y adjust for this probability
adjpx = ((maxx - w)/w/2)
x = random.choice([left, right] * adjpx + [withinx])
# --y
top = random.randrange(0, y1)
bottom = random.randrange(y2+1, maxy)
withiny = random.randrange(y1, y2+1)
if x == left or x == right:
adjpy = ((maxy- h)/h/2)
y = random.choice([top, bottom] * adjpy + [withiny])
else:
y = random.choice([top, bottom])
return x, y
The following plot has 10'000 points to illustrate the uniform placement of points (the points overlaying the box' border are due to point size).
Disclaimer: Note that this plot places the red box in the very middle such thattop/bottom, left/right have the same probability among each other. The adjustment thus is relative to the blocking box, but not for all areas of the graph. A final solution requires to adjust the probabilities for each of these separately.
Simpler solution, yet slightly modified problem
It turns out that adjusting the probabilities for different areas of the coordinate system is quite tricky. After some thinking I came up with a slightly modified approach:
Realizing that on any 2D coordinate system blocking out a rectangle divides the area into N sub-areas (N=8 in the case of the question) where a valid coordinate can be chosen. Looking at it this way, we can define the valid sub-areas as boxes of coordinates. Then we can choose a box at random and a coordinate at random from within that box:
def gen_rand_limit(p1, dim):
x1, y1 = p1
w, h = dim
x2, y2 = x1 + w, y1 + h
# generate x, y O(1)
boxes = (
((0,0),(x1,y1)), ((x1,0),(x2,y1)), ((x2,0),(maxx,y1)),
((0,y1),(x1,y2)), ((x2,y1),(maxx,y2)),
((0,y2),(x1,maxy)), ((x1,y2),(x2,maxy)), ((x2,y2),(maxx,maxy)),
)
box = boxes[random.randrange(len(boxes))]
x = random.randrange(box[0][0], box[1][0])
y = random.randrange(box[0][1], box[1][1])
return x, y
Note this is not generalized as the blocked box may not be in the middle hence boxes would look different. As this results in each box chosen with the same probability, we get the same number of points in each box. Obviously the densitiy is higher in smaller boxes:
If the requirement is to generate a uniform distribution among all possible coordinates, the solution is to calculate boxes such that each box is about the same size as the blocking box. YMMV
I've already posted a different answer that I still like, as it is simple and
clear, and not necessarily slow... at any rate it's not exactly what the OP asked for.
I thought about it and I devised an algorithm for solving the OP's problem within their constraints:
partition the screen in 9 rectangles around and comprising the "hole".
consider the 8 rectangles ("tiles") around the central hole"
for each tile, compute the origin (x, y), the height and the area in pixels
compute the cumulative sum of the areas of the tiles, as well as the total area of the tiles
for each extraction, choose a random number between 0 and the total area of the tiles (inclusive and exclusive)
using the cumulative sums determine in which tile the random pixel lies
using divmod determine the column and the row (dx, dy) in the tile
using the origins of the tile in the screen coordinates, compute the random pixel in screen coordinates.
To implement the ideas above, in which there is an initialization phase in which we compute static data and a phase in which we repeatedly use those data, the natural data structure is a class, and here it is my implementation
from random import randrange
class make_a_hole_in_the_screen():
def __init__(self, screen, hole_orig, hole_sizes):
xs, ys = screen
x, y = hole_orig
wx, wy = hole_sizes
tiles = [(_y,_x*_y) for _x in [x,wx,xs-x-wx] for _y in [y,wy,ys-y-wy]]
self.tiles = tiles[:4] + tiles[5:]
self.pixels = [tile[1] for tile in self.tiles]
self.total = sum(self.pixels)
self.boundaries = [sum(self.pixels[:i+1]) for i in range(8)]
self.x = [0, 0, 0,
x, x,
x+wx, x+wx, x+wx]
self.y = [0, y, y+wy,
0, y+wy,
0, y, y+wy]
def choose(self):
n = randrange(self.total)
for i, tile in enumerate(self.tiles):
if n < self.boundaries[i]: break
n1 = n - ([0]+self.boundaries)[i]
dx, dy = divmod(n1,self.tiles[i][0])
return self.x[i]+dx, self.y[i]+dy
To test the correctness of the implementation, here it is a rough check that I
run on python 2.7,
drilled_screen = make_a_hole_in_the_screen((200,100),(30,50),(20,30))
for i in range(1000000):
x, y = drilled_screen.choose()
if 30<=x<50 and 50<=y<80: print "***", x, y
if x<0 or x>=200 or y<0 or y>=100: print "+++", x, y
A possible optimization consists in using a bisection algorithm to find the relevant tile in place of the simpler linear search that I've implemented.
It requires a bit of thought to generate a uniformly random point with these constraints. The simplest brute force way I can think of is to generate a list of all valid points and use random.choice() to select from this list. This uses a few MB of memory for the list, but generating a point is very fast:
import random
screen_width = 1000
screen_height = 800
rect_x = 500
rect_y = 250
rect_width = 100
rect_height = 75
valid_points = []
for x in range(screen_width):
if rect_x <= x < (rect_x + rect_width):
for y in range(rect_y):
valid_points.append( (x, y) )
for y in range(rect_y + rect_height, screen_height):
valid_points.append( (x, y) )
else:
for y in range(screen_height):
valid_points.append( (x, y) )
for i in range(10):
rand_point = random.choice(valid_points)
print(rand_point)
It is possible to generate a random number and map it to a valid point on the screen, which uses less memory, but it is a bit messy and takes more time to generate the point. There might be a cleaner way to do this, but one approach using the same screen size variables as above is here:
rand_max = (screen_width * screen_height) - (rect_width * rect_height)
def rand_point():
rand_raw = random.randint(0, rand_max-1)
x = rand_raw % screen_width
y = rand_raw // screen_width
if rect_y <= y < rect_y+rect_height and rect_x <= x < rect_x+rect_width:
rand_raw = rand_max + (y-rect_y) * rect_width + (x-rect_x)
x = rand_raw % screen_width
y = rand_raw // screen_width
return (x, y)
The logic here is similar to the inverse of the way that screen addresses are calculated from x and y coordinates on old 8 and 16 bit microprocessors. The variable rand_max is equal to the number of valid screen coordinates. The x and y co-ordinates of the pixel are calculated, and if it is within the rectangle the pixel is pushed above rand_max, into the region that couldn't be generated with the first call.
If you don't care too much about the point being uniformly random, this solution is easy to implement and very quick. The x values are random, but the Y value is constrained if the chosen X is in the column with the rectangle, so the pixels above and below the rectangle will have a higher probability of being chosen than pizels to the left and right of the rectangle:
def pseudo_rand_point():
x = random.randint(0, screen_width-1)
if rect_x <= x < rect_x + rect_width:
y = random.randint(0, screen_height-rect_height-1)
if y >= rect_y:
y += rect_height
else:
y = random.randint(0, screen_height-1)
return (x, y)
Another answer was calculating the probability that the pixel is in certain regions of the screen, but their answer isn't quite correct yet. Here's a version using a similar idea, calculate the probability that the pixel is in a given region and then calculate where it is within that region:
valid_screen_pixels = screen_width*screen_height - rect_width * rect_height
prob_left = float(rect_x * screen_height) / valid_screen_pixels
prob_right = float((screen_width - rect_x - rect_width) * screen_height) / valid_screen_pixels
prob_above_rect = float(rect_y) / (screen_height-rect_height)
def generate_rand():
ymin, ymax = 0, screen_height-1
xrand = random.random()
if xrand < prob_left:
xmin, xmax = 0, rect_x-1
elif xrand > (1-prob_right):
xmin, xmax = rect_x+rect_width, screen_width-1
else:
xmin, xmax = rect_x, rect_x+rect_width-1
yrand = random.random()
if yrand < prob_above_rect:
ymax = rect_y-1
else:
ymin=rect_y+rect_height
x = random.randrange(xmin, xmax)
y = random.randrange(ymin, ymax)
return (x, y)
If it's the generation of random you want to avoid, rather than the loop, you can do the following:
Generate a pair of random floating point coordinates in [0,1]
Scale the coordinates to give a point in the outer rectangle.
If your point is outside the inner rectangle, return it
Rescale to map the inner rectangle to the outer rectangle
Goto step 3
This will work best if the inner rectangle is small as compared to the outer rectangle. And it should probably be limited to only going through the loop some maximum number of times before generating new random and trying again.

Categories

Resources