Related
In other words, I want to make a heatmap (or surface plot) where the color varies as a function of 2 variables. (Specifically, luminance = magnitude and hue = phase.) Is there any native way to do this?
Some examples of similar plots:
Several good examples of exactly(?) what I want to do.
More examples from astronomy, but with non-perceptual hue
Edit: This is what I did with it: https://github.com/endolith/complex_colormap
imshow can take an array of [r, g, b] entries. So you can convert the absolute values to intensities and phases - to hues.
I will use as an example complex numbers, because for it it makes the most sense. If needed, you can always add numpy arrays Z = X + 1j * Y.
So for your data Z you can use e.g.
imshow(complex_array_to_rgb(Z))
where (EDIT: made it quicker and nicer thanks to this suggestion)
def complex_array_to_rgb(X, theme='dark', rmax=None):
'''Takes an array of complex number and converts it to an array of [r, g, b],
where phase gives hue and saturaton/value are given by the absolute value.
Especially for use with imshow for complex plots.'''
absmax = rmax or np.abs(X).max()
Y = np.zeros(X.shape + (3,), dtype='float')
Y[..., 0] = np.angle(X) / (2 * pi) % 1
if theme == 'light':
Y[..., 1] = np.clip(np.abs(X) / absmax, 0, 1)
Y[..., 2] = 1
elif theme == 'dark':
Y[..., 1] = 1
Y[..., 2] = np.clip(np.abs(X) / absmax, 0, 1)
Y = matplotlib.colors.hsv_to_rgb(Y)
return Y
So, for example:
Z = np.array([[3*(x + 1j*y)**3 + 1/(x + 1j*y)**2
for x in arange(-1,1,0.05)] for y in arange(-1,1,0.05)])
imshow(complex_array_to_rgb(Z, rmax=5), extent=(-1,1,-1,1))
imshow(complex_array_to_rgb(Z, rmax=5, theme='light'), extent=(-1,1,-1,1))
imshow will take an NxMx3 (rbg) or NxMx4 (grba) array so you can do your color mapping 'by hand'.
You might be able to get a bit of traction by sub-classing Normalize to map your vector to a scaler and laying out a custom color map very cleverly (but I think this will end up having to bin one of your dimensions).
I have done something like this (pdf link, see figure on page 24), but the code is in MATLAB (and buried someplace in my archives).
I agree a bi-variate color map would be useful (primarily for representing very dense vector fields where your kinda up the creek no matter what you do).
I think the obvious extension is to let color maps take complex arguments. It would require specialized sub-classes of Normalize and Colormap and I am going back and forth on if I think it would be a lot of work to implement. I suspect if you get it working by hand it will just be a matter of api wrangling.
I created an easy to use 2D colormap class, that takes 2 NumPy arrays and maps them to an RGB image, based on a reference image.
I used #GjjvdBurg's answer as a starting point. With a bit of work, this could still be improved, and possibly turned into a proper Python module - if you want, feel free to do so, I grant you all credits.
TL;DR:
# read reference image
cmap_2d = ColorMap2D('const_chroma.jpeg', reverse_x=True) # , xclip=(0,0.9))
# map the data x and y to the RGB space, defined by the image
rgb = cmap_2d(data_x, data_y)
# generate a colorbar image
cbar_rgb = cmap_2d.generate_cbar()
The ColorMap2D class:
class ColorMap2D:
def __init__(self, filename: str, transpose=False, reverse_x=False, reverse_y=False, xclip=None, yclip=None):
"""
Maps two 2D array to an RGB color space based on a given reference image.
Args:
filename (str): reference image to read the x-y colors from
rotate (bool): if True, transpose the reference image (swap x and y axes)
reverse_x (bool): if True, reverse the x scale on the reference
reverse_y (bool): if True, reverse the y scale on the reference
xclip (tuple): clip the image to this portion on the x scale; (0,1) is the whole image
yclip (tuple): clip the image to this portion on the y scale; (0,1) is the whole image
"""
self._colormap_file = filename or COLORMAP_FILE
self._img = plt.imread(self._colormap_file)
if transpose:
self._img = self._img.transpose()
if reverse_x:
self._img = self._img[::-1,:,:]
if reverse_y:
self._img = self._img[:,::-1,:]
if xclip is not None:
imin, imax = map(lambda x: int(self._img.shape[0] * x), xclip)
self._img = self._img[imin:imax,:,:]
if yclip is not None:
imin, imax = map(lambda x: int(self._img.shape[1] * x), yclip)
self._img = self._img[:,imin:imax,:]
if issubclass(self._img.dtype.type, np.integer):
self._img = self._img / 255.0
self._width = len(self._img)
self._height = len(self._img[0])
self._range_x = (0, 1)
self._range_y = (0, 1)
#staticmethod
def _scale_to_range(u: np.ndarray, u_min: float, u_max: float) -> np.ndarray:
return (u - u_min) / (u_max - u_min)
def _map_to_x(self, val: np.ndarray) -> np.ndarray:
xmin, xmax = self._range_x
val = self._scale_to_range(val, xmin, xmax)
rescaled = (val * (self._width - 1))
return rescaled.astype(int)
def _map_to_y(self, val: np.ndarray) -> np.ndarray:
ymin, ymax = self._range_y
val = self._scale_to_range(val, ymin, ymax)
rescaled = (val * (self._height - 1))
return rescaled.astype(int)
def __call__(self, val_x, val_y):
"""
Take val_x and val_y, and associate the RGB values
from the reference picture to each item. val_x and val_y
must have the same shape.
"""
if val_x.shape != val_y.shape:
raise ValueError(f'x and y array must have the same shape, but have {val_x.shape} and {val_y.shape}.')
self._range_x = (np.amin(val_x), np.amax(val_x))
self._range_y = (np.amin(val_y), np.amax(val_y))
x_indices = self._map_to_x(val_x)
y_indices = self._map_to_y(val_y)
i_xy = np.stack((x_indices, y_indices), axis=-1)
rgb = np.zeros((*val_x.shape, 3))
for indices in np.ndindex(val_x.shape):
img_indices = tuple(i_xy[indices])
rgb[indices] = self._img[img_indices]
return rgb
def generate_cbar(self, nx=100, ny=100):
"generate an image that can be used as a 2D colorbar"
x = np.linspace(0, 1, nx)
y = np.linspace(0, 1, ny)
return self.__call__(*np.meshgrid(x, y))
Usage:
Full example, using the constant chroma reference taken from here as a screenshot:
# generate data
x = y = np.linspace(-2, 2, 300)
xx, yy = np.meshgrid(x, y)
ampl = np.exp(-(xx ** 2 + yy ** 2))
phase = (xx ** 2 - yy ** 2) * 6 * np.pi
data = ampl * np.exp(1j * phase)
data_x, data_y = np.abs(data), np.angle(data)
# Here is the 2D colormap part
cmap_2d = ColorMap2D('const_chroma.jpeg', reverse_x=True) # , xclip=(0,0.9))
rgb = cmap_2d(data_x, data_y)
cbar_rgb = cmap_2d.generate_cbar()
# plot the data
fig, plot_ax = plt.subplots(figsize=(8, 6))
plot_extent = (x.min(), x.max(), y.min(), y.max())
plot_ax.imshow(rgb, aspect='auto', extent=plot_extent, origin='lower')
plot_ax.set_xlabel('x')
plot_ax.set_ylabel('y')
plot_ax.set_title('data')
# create a 2D colorbar and make it fancy
plt.subplots_adjust(left=0.1, right=0.65)
bar_ax = fig.add_axes([0.68, 0.15, 0.15, 0.3])
cmap_extent = (data_x.min(), data_x.max(), data_y.min(), data_y.max())
bar_ax.imshow(cbar_rgb, extent=cmap_extent, aspect='auto', origin='lower',)
bar_ax.set_xlabel('amplitude')
bar_ax.set_ylabel('phase')
bar_ax.yaxis.tick_right()
bar_ax.yaxis.set_label_position('right')
for item in ([bar_ax.title, bar_ax.xaxis.label, bar_ax.yaxis.label] +
bar_ax.get_xticklabels() + bar_ax.get_yticklabels()):
item.set_fontsize(7)
plt.show()
I know this is an old post, but want to help out others that may arrive late. Below is a python function to implement complex_to_rgb from sage. Note: This implementation isn't optimal, but it is readable. See links: (examples)(source code)
Code:
import numpy as np
def complex_to_rgb(z_values):
width = z_values.shape[0]
height = z_values.shape[1]
rgb = np.zeros(shape=(width, height, 3))
for i in range(width):
row = z_values[i]
for j in range(height):
# define value, real(value), imag(value)
zz = row[j]
x = np.real(zz)
y = np.imag(zz)
# define magnitued and argument
magnitude = np.hypot(x, y)
arg = np.arctan2(y, x)
# define lighness
lightness = np.arctan(np.log(np.sqrt(magnitude) + 1)) * (4 / np.pi) - 1
if lightness < 0:
bot = 0
top = 1 + lightness
else:
bot = lightness
top = 1
# define hue
hue = 3 * arg / np.pi
if hue < 0:
hue += 6
# set ihue and use it to define rgb values based on cases
ihue = int(hue)
# case 1
if ihue == 0:
r = top
g = bot + hue * (top - bot)
b = bot
# case 2
elif ihue == 1:
r = bot + (2 - hue) * (top - bot)
g = top
b = bot
# case 3
elif ihue == 2:
r = bot
g = top
b = bot + (hue - 2) * (top - bot)
# case 4
elif ihue == 3:
r = bot
g = bot + (4 - hue) * (top - bot)
b = top
# case 5
elif ihue == 4:
r = bot + (hue - 4) * (top - bot)
g = bot
b = top
# case 6
else:
r = top
g = bot
b = bot + (6 - hue) * (top - bot)
# set rgb array values
rgb[i, j, 0] = r
rgb[i, j, 1] = g
rgb[i, j, 2] = b
return rgb
I am trying to create a heightmap by interpolating between a bunch of heights at certain points in an area. To process the whole image, I have the following code snippet:
map_ = np.zeros((img_width, img_height))
for x in range(img_width):
for y in range(img_height):
map_[x, y] = calculate_height(set(points.items()), x, y)
This is calculate_height:
def distance(x1, y1, x2, y2) -> float:
return np.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
def calculate_height(points: set, x, y) -> float:
total = 0
dists = {}
for pos, h in points:
d = distance(pos[0], pos[1], x, y)
if x == pos[0] and y == pos[1]:
return h
d = 1 / (d ** 2)
dists[pos] = d
total += d
r = 0
for pos, h in points:
ratio = dists[pos] / total
r += ratio * h
r = r
return r
This snippet works perfectly, but if the image is too big, it takes a long time to process, because this is O(n^2). The problem with this, is that a "too big" image is 800 600, and it takes almost a minute to process, which to me seems a bit excessive.
My goal is not to reduce the time complexity from O(n^2), but to reduce the time it takes to process images, so that I can get decently sized images in a reasonable amount of time.
I found
this post, but I couldn't really try it out because it's all for a 1D array, and I have a 2D array, and I also need to pass the coordinates of each point and the set of existing points to the calculate_height function. What can I try to optimize this code snippet?
Edit: Moving set(points.items) out of the loop as #thethiny suggested was a HUGE improvement. I had no idea it was such a heavy thing to do. This makes it fast enough for me, but feel free to add more suggestions for the next people to come by!
Edit 2: I have further optimized this processing by including the following changes:
# The first for loop inside the calculate_distance function
for pos, h in points:
d2 = distance2(pos[0], pos[1], x, y)
if x == pos[0] and y == pos[1]:
return h
d2 = d2 ** -1 # 1 / (d ** 2) == d ** -2 == d2 ** -1
dists[pos] = d2 # Having the square of d on these two lines makes no difference
total += d2
This reduced execution time for a 200x200 image from 1.57 seconds to 0.76 seconds. The 800x600 image mentioned earlier now takes 6.13 seconds to process :D
This is what points looks like (as requested by #norok12):
# Hints added for easier understanding, I know this doesn't run
points: dict[tuple[int, int], float] = {
(x: int, y: int): height: float,
(x: int, y: int): height: float,
(x: int, y: int): height: float
}
# The amount of points varies between datasets, so I can't provide a useful range other than [3, inf)
There's a few problems with your implementation.
Essentially what you're implementing is approximation using radial basis functions.
The usual algorithm for that looks like:
sum_w = 0
sum_wv = 0
for p,v in points.items():
d = distance(p,x)
w = 1.0 / (d*d)
sum_w += w
sum_wv += w*v
return sum_wv / sum_w
Your code has some extra logic for bailing out if p==x - which is good.
But it also allocates an array of distances - which this single loop form does not need.
This brings execution of an example in a workbook from 13s to 12s.
The next thing to note is that collapsing the points dict into an numpy array gives us the chance to use numpy functions.
points_array = np.array([(p[0][0],p[0][1],p[1]) for p in points.items()]).astype(np.float32)
Then we can write the function as
def calculate_height_fast(points, x, y) -> float:
dx = points[:,0] - x
dy = points[:,1] - y
r = np.hypot(dx,dy)
w = 1.0 / (r*r)
sum_w = np.sum(w)
return np.sum(points[:,2] * w) / np.sum(w)
This brings our time down to 658ms. But we can do better yet...
Since we're now using numpy functions we can apply numba.njit to JIT compile our function.
#numba.njit
def calculate_height_fast(points, x, y) -> float:
dx = points[:,0] - x
dy = points[:,1] - y
r = np.hypot(dx,dy)
w = 1.0 / (r*r)
sum_w = np.sum(w)
return np.sum(points[:,2] * w) / np.sum(w)
This was giving me 105ms (after the function had been run once to ensure it got compiled).
This is a speed up of 130x over the original implementation (for my data)
You can see the full implementations here
This really is a small addition to #MichaelAnderson's detailed answer.
Probably calculate_height_fast() can get faster by optimizing a bit more with explicit looping:
#numba.njit
def calculate_height_faster(points, x, y) -> float:
dx = points[:, 0] - x
dy = points[:, 1] - y
r = np.hypot(dx, dy)
# compute weighted average
n = r.size
sum_w = sum_wp = 0
for i in range(n):
w = 1.0 / (r[i] * r[i])
sum_w += w
sum_wp += points[i, 2] * w
return sum_wp / sum_w
The problem - given a list of planar points [p_1, ..., p_n] and the dimensions of some rectangle w, h, find the minimal set of rectangles w, h that cover all points (edit - the rectangles are not rotated).
My inital solution was:
find the bounding-box of all points
divide the width and height of the bounding-box by the w, h of the given rectangle and round the number up to get the number of instances of the rectangle in x and y
to further optimize, go through all rectangles and delete the ones that have zero points inside them.
An example in Python:
def tile_rect(points, rect):
w, h = rect
xs = [p.x for p in points]
ys = [p.y for p in points]
bbox_w = abs(max(xs) - min(xs))
bbox_h = abs(max(ys) - min(ys))
n_x, n_y = ceil(bbox_w / w), ceil(bbox_h / h)
rect_xs = [(min(xs) + n * w for n in range(n_x)]
rect_ys = [(min(ys) + n * h for n in range(n_y)]
rects = remove_empty(rect_xs, rect_ys)
return rects
How can I do better? What algorithm can I use to decrease the number of rectangles?
To discretize the problem for integer programming, observe that given a rectangle we can slide it in the +x and +y directions without decreasing the coverage until the min x and the min y lines both have a point on them. Thus the integer program is just the standard min cover:
minimize sum_R x_R
subject to
for every point p, sum_{R contains p} x_R >= 1
x_R in {0, 1}
where R ranges over all rectangles whose min x is the x of some point and whose min y is the y of some point (not necessarily the same point).
Demo Python:
import random
from ortools.linear_solver import pywraplp
w = 0.1
h = 0.1
points = [(random.random(), random.random()) for _ in range(100)]
rectangles = [(x, y) for (x, _) in points for (_, y) in points]
solver = pywraplp.Solver.CreateSolver("min cover", "SCIP")
objective = solver.Objective()
constraints = [solver.RowConstraint(1, pywraplp.inf, str(p)) for p in points]
variables = [solver.BoolVar(str(r)) for r in rectangles]
for (x, y), var in zip(rectangles, variables):
objective.SetCoefficient(var, 1)
for (px, py), con in zip(points, constraints):
if x <= px <= x + w and y <= py <= y + h:
con.SetCoefficient(var, 1)
solver.Objective().SetMinimization()
solver.Solve()
scale = 6 * 72
margin = 72
print(
'<svg width="{}" height="{}">'.format(
margin + scale + margin, margin + scale + margin
)
)
print(
'<text x="{}" y="{}">{} rectangles</text>'.format(
margin // 2, margin // 2, round(objective.Value())
)
)
for x, y in points:
print(
'<circle cx="{}" cy="{}" r="3" fill="none" stroke="black"/>'.format(
margin + x * scale, margin + y * scale
)
)
for (x, y), var in zip(rectangles, variables):
if var.solution_value():
print(
'<rect x="{}" y="{}" width="{}" height="{}" fill="none" stroke="rgb({},{},{})"/>'.format(
margin + x * scale,
margin + y * scale,
w * scale,
h * scale,
random.randrange(192),
random.randrange(192),
random.randrange(192),
)
)
print("</svg>")
Example output:
Assuming an approximate, rather than optimal solution is acceptable, how about a routine generally like:
Until no points are left:
(1) Find the convex hull of the remaining points.
(2) Cover each point/s on the hull so the
covering rectangles extend "inward."
(Perhaps examine neighbouring hull points
to see if a rectangle can cover more than one.)
(3) Remove the newly covered points.
Clearly, the orientation of the covering rectangles has an effect on the procedure and result. I think there is a way to combine (1) and (3), or possibly rely on a nested convex hull, but I don't have too much experience with those.
This is can be transformed into a mostly standard set cover problem. The general steps are as follows, given n points in the plane.
First, generate all possible maximally inclusive rectangles, of which there are at most n^2, named R. The key insight is that given a point p1 with coordinates (x1, y1), use x1 as the leftmost bound for a set of rectangles. For all other points p2 with (x2,y2) where x1 <= x2 <= x1+w and where y1-h <= y2 <= y1+h, generate a rectangle ((x1, y2), (x1+w, y2+h)).
For each rectangle r generated, count the points included in that rectangle cover(r).
Choose a subset of the rectangles R, s, such that all points are in Union(r in s) cover(r)
Now, the tricky part is that last step. Fortunately, it is a standard problem and there are many algorithms suggested in the literature. For example, combinatorial optimization solvers (such as SAT solvers, MIP solvers, and Constraint programming solvers) can be used.
Note that the above re-formulation only works if it is ok for rectangles to cover each other. It might be the case that the generated set of rectangles is not enough to find the least set of rectangles that do not overlap.
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.
I'm writing a Python program to generate the Luna Free State flag from the famous Heinlein novel The Moon is a Harsh Mistress, as a personal project. I've been cribbing heraldry rules and matching mathematical formulas off the web, but something is clearly wrong in my bendsinister routine, since the assertion fails when uncommented. The area of the bend sinister should be 1/3 the total area of the flag, and it isn't. The only really dodgy thing I've done is to guess at the formula for the height of the trapezoid, but I guess the errors could be anywhere. I've trimmed out most of the code, leaving only what's necessary to show the problem. Hopefully someone less mathematically-challenged can spot the error!
#!/usr/bin/python
'generate bend sinister according to rules of heraldry'
import sys, os, random, math, Image, ImageDraw
FLAG = Image.new('RGB', (900, 600), 'black')
CANVAS = ImageDraw.Draw(FLAG)
DEBUGGING = True
def bendsinister(image = FLAG, draw = CANVAS):
'''a bend sinister covers 1/3 of the field, sinister chief to dexter base
(some sources on the web say 1/5 of the field, but we'll use 1/3)
the "field" in this case being the area of the flag, so we need to
find a trapezoid which is 1/6 the total area (width * height).
we need to return only the width of the diagonal, which is double
the height of the calculated trapezoid
'''
x, y = image.size
b = math.sqrt((x ** 2) + (y ** 2))
A = float(x * y)
debug('%d * %d = %d' % (x, y, A))
H = triangle_height(A / 2, b) # height of triangular half of flag
width = trapezoid_height(b, H, A / 6) * 2
if command == 'bendsinister':
show_bendsinister(x, y, width, image, draw)
return width
def show_bendsinister(x, y, width, image = FLAG, draw = CANVAS):
'for debugging formula'
dexter_base, sinister_chief = (0, y), (x, 0)
draw.line((dexter_base, sinister_chief), 'blue', int(width))
image.show()
debug(image.getcolors(2)) # should be twice as many black pixels as blue
def triangle_height(a, b):
'a=bh/2'
h = float(a) / (float(b) / 2)
debug('triangle height: %.2f' % h)
return h
def trapezoid_height(b, H, a):
'''calculate trapezoid height (h) given the area (a) of the trapezoid and
base b, the longer base, when it is known that the trapezoid is a section
of a triangle of height H, such that the top, t, equals b when h=0 and
t=0 when h=H. h is therefore inversely proportional to t with the formula
t=(1-(h/H))*b, found simply by looking for what fit the two extremes.
the area of a trapezoid is simply the height times the average length of
the two bases, b and t, i.e.: a=h*((b+t)/2). the formula reduces
then to (2*a)/b=(2*h)+(h**2)/H, which is the quadratic equation
(1/H)*(h**2)+(2*h)-((2*a)/b)=0; solve for h using the quadratic formula
'''
try:
h = (-2 + math.sqrt(4 - 4 * (1.0 / H) * -((2 * a) / b))) / (2 * (1.0 / H))
debug('trapezoid height with plus: %.2f' % h)
except: # must be imaginary, so try minus instead
h = (-2 - math.sqrt(4 - 4 * (1.0 / H) * -((2 * a) / b))) / (2 * (1.0 / H))
debug('trapezoid height with minus: %.2f' % h)
t = (1 - (float(h) / H)) * b
debug('t=%d, a=%d, check=%d' % (t, round(a), round(h * ((b + t) / 2))))
#assert round(a) == round(h * ((b + t) / 2))
return h
def debug(message):
if DEBUGGING:
print >>sys.stderr, message
if __name__ == '__main__':
command = os.path.splitext(os.path.basename(sys.argv[0]))[0]
print eval(command)(*sys.argv[1:]) or ''
Here is the debugging output, showing I'm far off from the 1/3 area:
jcomeau#intrepid:~/rentacoder/jcomeau/tanstaafl$ ./bendsinister.py
900 * 600 = 540000
triangle height: 499.23
trapezoid height with plus: 77.23
t=914, a=90000, check=77077
[(154427, (0, 0, 255)), (385573, (0, 0, 0))]
154.462354191
Here is an image of the output, with some added lines:
The red line divides the two triangles, either can be used for the calculation of the trapezoid. I'm using the one starting at the top left. The green line is the height of that triangle, the variable H in the program.
For the finished script and flag (using the correction supplied by Michael Anderson), see http://unternet.net/tanstaafl/. Thanks all for the help!
Break the rectangle into two triangles. They will be identical.
The Black triangle + Blue Trapezoid is Triangle A.
The Black Triangle on its own is Triangle B
Triangle A and Triangle B are similar triangles so their area is related by the square of the scale factor relating them.
We want the Blue Trapezoid to be one third of the area of Triangle A. (This way the bend will take one third of the overall rectangle). This means that Triangle B must be 2/3 area of Triangle A. Thus the scalefactor must be sqrt(2/3).
You should then be able to convert this to give you the coordinates of the bend geometry pretty easily.
I executed the following code in an IDLE session
from PIL import Image, ImageDraw
from math import sqrt
'generate bend sinister according to rules of heraldry'
import sys, os, random, math
FLAG = Image.new('RGB', (900, 600), 'black')
CANVAS = ImageDraw.Draw(FLAG)
DEBUGGING = True
def debug(message):
if DEBUGGING:
print >>sys.stderr, message
def show_bendsinister(x, y, width, image = FLAG, draw = CANVAS):
'for debugging formula'
dexter_base, sinister_chief = (0, y), (x, 0)
print 'dexter_base==',dexter_base,'sinister_chief==',sinister_chief
draw.line((dexter_base, sinister_chief), 'blue', int(width))
image.show()
debug(image.getcolors(2)) # should be twice as many black pixels as blue
def trapezoid_height(x, y, P):
'''Given a rectangle whose width and length are (x) and (y)
The half of this rectangle is a large triangle A
whose base (b) is the diagonal of the rectangle
and its height (H) goes from its base (b) to
the right angle of the large triangle.
(x) and (y) are the side-lengths of the triangle.
The area of this large triangle is (x*y)/2 = (H*b)/2
Given a trapezoid whose base is the diagonal (b) of the rectangle
and base (b) of the large triangle, its height is (h)
and its top is (t).
Given (S) as the area of the trapezoid.
In general, the trapezoid is disymtric because the triangle have x != y.
So the area is S = h*(b + t)/2
This function trapezoid_height() calculates the height (h) of the trapezoid
in order that the trapezoid have an area (S) which must be
the percentage (P) of the area of the large triangle A. So:
h*(b + t)/2 = S = P*[H*b /2] ==> h*(b + t) = P*H*b
==> h*t = P*H*b - h*b ==> h*t*(H-h) = [P*H - h]*b*(H-h)
The large triangle is the sum of the trapezoid and of a little triangle B
having an height equal to (H-h) and a base which is the top (t)
of the trapezoid.
The area of this little triangle B is t*(H-h)/2 and must be equal to (1-P)*[H*b / 2]
==> t*(H-h) = (1-P)*H*b ==> h*t*(H-h) = h*(1-P)*H*b
From h*t*(H-h) = [P*H - h]*b*(H-h) and h*t*(H-h) = h*(1-P)*H*b
we obtain [P*H - h]*b*(H-h) = h*(1-P)*H*b
==> b*h**2 - (b*H + xy)*h + P*x*y*H = 0
==> h**2 - 2*H*h + P*(H**2) = 0
That leads to the solution H*(1 - sqrt(1-P)), the other H*(1 + sqrt(1-P))
being bigger than H
'''
H = math.sqrt( (x*x*y*y) / (x*x + y*y) )
return H*(1 - sqrt(1-P))
def bendsinister(image = FLAG, draw = CANVAS):
'''a bend sinister covers 1/3 of the field, sinister chief to dexter base
(some sources on the web say 1/5 of the field, but we'll use 1/3)
the "field" in this case being the area of the flag, so we need to
find a trapezoid which is 1/6 the total area (width * height).
we need to return only the width of the diagonal, which is double
the height of the calculated trapezoid
'''
x, y = image.size
print 'x ==',x,'y ==',y
percentage = float(1)/3
width = 2 * trapezoid_height(x, y , percentage)
print 'height ==',width/2
print 'width==',width
if command == 'bendsinister':
show_bendsinister(x, y, width, image, draw)
return width
command = 'bendsinister'
print bendsinister()
result
x == 900 y == 600
height == 91.6103029364
width== 183.220605873
dexter_base== (0, 600) sinister_chief== (900, 0)
[(180340, (0, 0, 255)), (359660, (0, 0, 0))]
183.220605873
The blue stripe displayed doesn't give the impression to be 1/3 of the field's area, but the numbers speak:
359660 / 180340 = 1.994344