Algorithm to subdivide a polygon in smaller polygons - algorithm

I have a polygon made of successive edges on a plane, and would like to subdivide it in sub-polygons being triangles or rectangles.
Where can I find an algorithm to do this ?
Thanks !

In computational geometry, the problem you want to solve is called triangulation.
There are algorithms to solve this problem, giving triangulations with different properties. You will need to decide which one is the best fit.

I was looking for an answer for this myself but couldn't find one. Tried to stitch together several pieces and here's the result.
This is not necessarily the most optimal routine but it did the job for me. If you want to increase performance, try experimenting with the code.
A brief description of the algorithm:
Using the boundaries of the original geometry itself, and the boundaries of its convex hull, and its minimum rotated rectangle, derive all possible rectangles.
Divide all rectangles into smaller squares of specified side length.
Drop duplicates using a rounded off centroid. (r: round off param)
Retain either those squares 'within' the geometry, or those that 'intersect' the geometry, depending on whichever is closer to the total number of required squares.
EDITED
New Solution
#### Python script for dividing any shapely polygon into smaller equal sized polygons
import numpy as np
from shapely.ops import split
import geopandas
from shapely.geometry import MultiPolygon, Polygon
def rhombus(square):
"""
Naively transform the square into a Rhombus at a 45 degree angle
"""
coords = square.boundary.coords.xy
xx = list(coords[0])
yy = list(coords[1])
radians = 1
points = list(zip(xx, yy))
Rhombus = Polygon(
[
points[0],
points[1],
points[3],
((2 * points[3][0]) - points[2][0], (2 * points[3][1]) - points[2][1]),
points[4],
]
)
return Rhombus
def get_squares_from_rect(RectangularPolygon, side_length=0.0025):
"""
Divide a Rectangle (Shapely Polygon) into squares of equal area.
`side_length` : required side of square
"""
rect_coords = np.array(RectangularPolygon.boundary.coords.xy)
y_list = rect_coords[1]
x_list = rect_coords[0]
y1 = min(y_list)
y2 = max(y_list)
x1 = min(x_list)
x2 = max(x_list)
width = x2 - x1
height = y2 - y1
xcells = int(np.round(width / side_length))
ycells = int(np.round(height / side_length))
yindices = np.linspace(y1, y2, ycells + 1)
xindices = np.linspace(x1, x2, xcells + 1)
horizontal_splitters = [
LineString([(x, yindices[0]), (x, yindices[-1])]) for x in xindices
]
vertical_splitters = [
LineString([(xindices[0], y), (xindices[-1], y)]) for y in yindices
]
result = RectangularPolygon
for splitter in vertical_splitters:
result = MultiPolygon(split(result, splitter))
for splitter in horizontal_splitters:
result = MultiPolygon(split(result, splitter))
square_polygons = list(result)
return square_polygons
def split_polygon(G, side_length=0.025, shape="square", thresh=0.9):
"""
Using a rectangular envelope around `G`, creates a mesh of squares of required length.
Removes non-intersecting polygons.
Args:
- `thresh` : Range - [0,1]
This controls - the number of smaller polygons at the boundaries.
A thresh == 1 will only create (or retain) smaller polygons that are
completely enclosed (area of intersection=area of smaller polygon)
by the original Geometry - `G`.
A thresh == 0 will create (or retain) smaller polygons that
have a non-zero intersection (area of intersection>0) with the
original geometry - `G`
- `side_length` : Range - (0,infinity)
side_length must be such that the resultant geometries are smaller
than the original geometry - `G`, for a useful result.
side_length should be >0 (non-zero positive)
- `shape` : {square/rhombus}
Desired shape of subset geometries.
"""
assert side_length>0, "side_length must be a float>0"
Rectangle = G.envelope
squares = get_squares_from_rect(Rectangle, side_length=side_length)
SquareGeoDF = geopandas.GeoDataFrame(squares).rename(columns={0: "geometry"})
Geoms = SquareGeoDF[SquareGeoDF.intersects(G)].geometry.values
if shape == "rhombus":
Geoms = [rhombus(g) for g in Geoms]
geoms = [g for g in Geoms if ((g.intersection(G)).area / g.area) >= thresh]
elif shape == "square":
geoms = [g for g in Geoms if ((g.intersection(G)).area / g.area) >= thresh]
return geoms
# Reading geometric data
geo_filepath = "/data/geojson/pc_14.geojson"
GeoDF = geopandas.read_file(geo_filepath)
# Selecting random shapely-geometry
G = np.random.choice(GeoDF.geometry.values)
squares = split_polygon(G,shape='square',thresh=0.5,side_length=0.025)
rhombuses = split_polygon(G,shape='rhombus',thresh=0.5,side_length=0.025)
Previous Solution:
import numpy as np
import geopandas
from shapely.ops import split
from shapely.geometry import MultiPolygon, Polygon, Point, MultiPoint
def get_rect_from_geom(G, r=2):
"""
Get rectangles from a geometry.
r = rounding factor.
small r ==> more rounding off ==> more rectangles
"""
coordinate_arrays = G.exterior.coords.xy
coordinates = list(
zip(
[np.round(c, r) for c in coordinate_arrays[0]],
[np.round(c, r) for c in coordinate_arrays[1]],
)
)
Rectangles = []
for c1 in coordinates:
Coords1 = [a for a in coordinates if a != c1]
for c2 in Coords1:
Coords2 = [b for b in Coords1 if b != c2]
x1, y1 = c1[0], c1[1]
x2, y2 = c2[0], c2[1]
K1 = [k for k in Coords2 if k == (x1, y2)]
K2 = [k for k in Coords2 if k == (x2, y1)]
if (len(K1) > 0) & (len(K2) > 0):
rect = [list(c1), list(K1[0]), list(c2), list(K2[0])]
Rectangles.append(rect)
return Rectangles
def get_squares_from_rect(rect, side_length=0.0025):
"""
Divide a rectangle into equal area squares
side_length = required side of square
"""
y_list = [r[1] for r in rect]
x_list = [r[0] for r in rect]
y1 = min(y_list)
y2 = max(y_list)
x1 = min(x_list)
x2 = max(x_list)
width = x2 - x1
height = y2 - y1
xcells, ycells = int(np.round(width / side_length)), int(
np.round(height / side_length)
)
yindices = np.linspace(y1, y2, ycells + 1)
xindices = np.linspace(x1, x2, xcells + 1)
horizontal_splitters = [
LineString([(x, yindices[0]), (x, yindices[-1])]) for x in xindices
]
vertical_splitters = [
LineString([(xindices[0], y), (xindices[-1], y)]) for y in yindices
]
result = Polygon(rect)
for splitter in vertical_splitters:
result = MultiPolygon(split(result, splitter))
for splitter in horizontal_splitters:
result = MultiPolygon(split(result, splitter))
square_polygons = list(result)
return [np.stack(SQPOLY.exterior.coords.xy, axis=1) for SQPOLY in square_polygons]
def round_centroid(g, r=10):
"""
Get Centroids.
Round off centroid coordinates to `r` decimal points.
"""
C = g.centroid.coords.xy
return (np.round(C[0][0], r), np.round(C[1][0], r))
def subdivide_polygon(g, side_length=0.0025, r=10):
"""
1. Create all possible rectangles coordinates from the geometry, its minimum rotated rectangle, and its convex hull.
2. Divide all rectangles into smaller squares.
small r ==> more rounding off ==> fewer overlapping squares. (these are dropped as duplicates)
large r ==> may lead to a few overlapping squares.
"""
# Number of squares required.
num_squares_reqd = g.area // (side_length ** 2)
# Some of these combinations can be dropped to improve performance.
Rectangles = []
Rectangles.extend(get_rect_from_geom(g))
Rectangles.extend(get_rect_from_geom(g.minimum_rotated_rectangle))
Rectangles.extend(get_rect_from_geom(g.convex_hull))
Squares = []
for r in range(len(Rectangles)):
rect = Rectangles[r]
Squares.extend(get_squares_from_rect(rect, side_length=side_length))
SquarePolygons = [Polygon(square) for square in Squares]
GDF = geopandas.GeoDataFrame(SquarePolygons).rename(columns={0: "geometry"})
GDF.loc[:, "centroid"] = GDF.geometry.apply(round_centroid, r=r)
GDF = GDF.drop_duplicates(subset=["centroid"])
wgeoms = GDF[GDF.within(g)].geometry.values
igeoms = GDF[GDF.intersects(g)].geometry.values
w = abs(num_squares_reqd - len(wgeoms))
i = abs(num_squares_reqd - len(igeoms))
print(w, i)
if w <= i:
return wgeoms
else:
return igeoms
geoms = subdivide(g)

Stumbled across this after many searches.
Thanks #Aditya Chhabra for your submission, it works great but get_squares_from_rect is very slow for small side lengths due to iterative clips.
We can do this instantaneously if we combine all LineStrings into a single collection, then clip and polygonize in one step, which I found in in this question.
Previously side lengths of 0.0001 (EPSG:4326) took > 1 minute, now it takes no time.
from shapely.ops import unary_union, polygonize, linemerge
from shapely.geometry import LineString
import numpy as np
def get_squares_from_rect_faster(RectangularPolygon, side_length=0.0025):
rect_coords = np.array(RectangularPolygon.boundary.coords.xy)
y_list = rect_coords[1]
x_list = rect_coords[0]
y1 = min(y_list)
y2 = max(y_list)
x1 = min(x_list)
x2 = max(x_list)
width = x2 - x1
height = y2 - y1
xcells = int(np.round(width / side_length))
ycells = int(np.round(height / side_length))
yindices = np.linspace(y1, y2, ycells + 1)
xindices = np.linspace(x1, x2, xcells + 1)
horizontal_splitters = [
LineString([(x, yindices[0]), (x, yindices[-1])]) for x in xindices
]
vertical_splitters = [
LineString([(xindices[0], y), (xindices[-1], y)]) for y in yindices
]
lines = horizontal_splitters + vertical_splitters
lines.append(RectangularPolygon.boundary)
lines = unary_union(lines)
lines = linemerge(lines)
return list(polygonize(lines))

Related

Shortest distance between a specific object to other objects

Following these two posts that deals with finding the distance between objects in binary image, how can I only output/calculate only the shortest distance between a specific object to the rest (for examples, {1->3}, {2->5}, {3->1}, {4->7)?
https://www.mathworks.com/matlabcentral/answers/164955-distance-between-several-objects-in-binary-image
Pairwise distance between all centroid coordinate combinations - Matlab
Script:
clc;
clear all;
I = rgb2gray(imread('E:/NCircles.png'));
imshow(I);
BW = imbinarize(I,'adaptive');
BW = imfill(BW, 'holes');
BW = bwlabel(BW);
s = regionprops(BW,'Area', 'BoundingBox', 'Eccentricity', 'MajorAxisLength', 'MinorAxisLength', 'Orientation', 'Perimeter','Centroid');
imshow(BW)
hold on
for k = 1:numel(s)
c = s(k).Centroid;
text(c(1), c(2), sprintf('%d', k), 'HorizontalAlignment', 'center', 'VerticalAlignment', 'middle');
end
boundaries = bwboundaries(BW);
numberOfBoundaries = size(boundaries, 1);
for k = 1 : numberOfBoundaries
thisBoundary = boundaries{k};
plot(thisBoundary(:,2), thisBoundary(:,1), 'r', 'LineWidth', 3);
end
hold off;
% Define object boundaries
numberOfBoundaries = size(boundaries, 1)
for b1 = 1 : numberOfBoundaries
for b2 = 1 : numberOfBoundaries
if b1 == b2
% Can't find distance between the region and itself
continue;
end
boundary1 = boundaries{b1};
boundary2 = boundaries{b2};
boundary1x = boundary1(:, 2);
boundary1y = boundary1(:, 1);
x1=1;
y1=1;
x2=1;
y2=1;
overallMinDistance = inf; % Initialize.
% For every point in boundary 2, find the distance to every point in boundary 1.
for k = 1 : size(boundary2, 1)
% Pick the next point on boundary 2.
boundary2x = boundary2(k, 2);
boundary2y = boundary2(k, 1);
% For this point, compute distances from it to all points in boundary 1.
allDistances = sqrt((boundary1x - boundary2x).^2 + (boundary1y - boundary2y).^2);
% Find closest point, min distance.
[minDistance(k), indexOfMin] = min(allDistances);
if minDistance(k) < overallMinDistance
x1 = boundary1x(indexOfMin);
y1 = boundary1y(indexOfMin);
x2 = boundary2x;
y2 = boundary2y;
overallMinDistance = minDistance(k);
end
end
% Find the overall min distance
minDistance = min(minDistance);
% Report to command window.
fprintf('The minimum distance from region %d to region %d is %.3f pixels\n', b1, b2, minDistance);
% Draw a line between point 1 and 2
line([x1, x2], [y1, y2], 'Color', 'y', 'LineWidth', 3);
end
end
Given BW and boundaries as defined above, and a source object from which to calculate distances to all other objects:
source_object = 1; % label of source object in BW
Construct a distance image such that the value of each pixel is its distance from the source object using bwdist:
% anonymous function to convert cell array of subsripts
% into cell array of indices
indsfun = #(a) sub2ind(size(BW), a(:,1), a(:,2));
% use function on all of the cell's boundary objects
object_inds = cellfun(indsfun, boundaries, 'UniformOutput', false);
source_image = zeros(size(BW)); % create image containing only source object
source_image(object_inds{source_object}) = 1;
% compute distance from source to all other pixels in image
dist_image = bwdist(source_image, 'euclidean'); % replace with desired metric
imagesc(dist_image); % not necessary, but gives a cool image
Now, for each object in the original image, find the minimum distance of its boundary to the source object boundary:
min_dist = zeros(1,numel(boundaries)); % hold minimum distances
for target_object = 1:numel(boundaries)
% get the distance values at the indices of the target object
% and store the minimum.
min_dist(target_object) = min(dist_image(object_inds{target_object}));
end
In the end, min_dist will contain the minimum (boundary) distance from the source object to all other objects. A sample run on your image gives the following Euclidean distances:
min_dist =
Columns 1 through 7:
0.00000 67.54258 60.00000 207.23416 154.48625 168.79869 319.01410
Columns 8 through 13:
236.05296 324.71063 344.05814 367.00000 469.07996 509.00000

Fast Radial Symmetry Transform (FRST) implementation (python) results in unusual cross-hair looking artifacts

I am trying to implement FRST on python to detect centroids of elliptical objects (e.g. cells in microscopy images), but my implementation does not find seed points (more or less center points) of elliptical objects. This effort comes from duplicating FRST from Segmentation of Overlapping Elliptical Objects in Silhouette Images (https://ieeexplore.ieee.org/document/7300433). I don't know why I have these artifacts. An interesting thing is that I see these patterns (crosses) all in the same direction per object. Any point in the right direction to generate the same result as in the paper (just to find the seed points) will be most welcome.
Original Paper: A Fast Radial Symmetry Transform for Detecting Points of Interest by Loy and Zelinsky (ECCV 2002)
I have also tried the pre-existing python package for FRST: https://pypi.org/project/frst/. This somehow results in the same artifacts. Weird.
First image: Original Image
Second image: Sobel-operated Image
Third image: Magnitude Projection Image
Fourth image: Magnitude Projection Image with positively affected pixels only
Fifth image: FRST'd image: end-product with original image overlaid (shadowed)
Sixth image: FRST'd image by the pre-existing python package with original image overlaid (shadowed).
from scipy.ndimage import gaussian_filter
import numpy as np
from scipy.signal import convolve
# Get orientation projection image
def get_proj_img(image, radius):
workingDims = tuple((e + 2*radius) for e in image.shape)
h,w = image.shape
ori_img = np.zeros(workingDims) # Orientation Projection Image
mag_img = np.zeros(workingDims) # Magnitutde Projection Image
# Kenels for the sobel operator
a1 = np.matrix([1, 2, 1])
a2 = np.matrix([-1, 0, 1])
Kx = a1.T * a2
Ky = a2.T * a1
# Apply the Sobel operator
sobel_x = convolve(image, Kx)
sobel_y = convolve(image, Ky)
sobel_norms = np.hypot(sobel_x, sobel_y)
# Distances to afpx, afpy (affected pixels)
dist_afpx = np.multiply(np.divide(sobel_x, sobel_norms, out = np.zeros(sobel_x.shape), where = sobel_norms!=0), radius)
dist_afpx = np.round(dist_afpx).astype(int)
dist_afpy = np.multiply(np.divide(sobel_y, sobel_norms, out = np.zeros(sobel_y.shape), where = sobel_norms!=0), radius)
dist_afpy = np.round(dist_afpy).astype(int)
for cords, sobel_norm in np.ndenumerate(sobel_norms):
i, j = cords
pos_aff_pix = (i+dist_afpx[i,j], j+dist_afpy[i,j])
neg_aff_pix = (i-dist_afpx[i,j], j-dist_afpy[i,j])
ori_img[pos_aff_pix] += 1
ori_img[neg_aff_pix] -= 1
mag_img[pos_aff_pix] += sobel_norm
mag_img[neg_aff_pix] -= sobel_norm
ori_img = ori_img[:h, :w]
mag_img = mag_img[:h, :w]
print ("Did it go back to the original image size? ")
print (ori_img.shape == image.shape)
# try normalizing ori and mag img
return ori_img, mag_img
def get_sn(ori_img, mag_img, radius, kn, alpha):
ori_img_limited = np.minimum(ori_img, kn)
fn = np.multiply(np.divide(mag_img,kn), np.power((np.absolute(ori_img_limited)/kn), alpha))
# convolute fn with gaussian filter.
sn = gaussian_filter(fn, 0.25*radius)
return sn
def do_frst(image, radius, kn, alpha, ksize = 3):
ori_img, mag_img = get_proj_img(image, radius)
sn = get_sn(ori_img, mag_img, radius, kn, alpha)
return sn
Parameters:
radius = 50
kn = 10
alpha = 2
beta = 0
stdfactor = 0.25

How to find the centroid of pixels with same color in a 360° video?

Let I be a w x h frame from a 360° video stream.
Let R be a red rectangle on that frame. R is smaller than the width of the image.
To compute the centroid of this rectangle we need to distinguish two cases:
case 1 where R is on the edges
case 2 where R is fully inside the frame
As you can see there will be a problem to compute the centroid with classical methods in case 1. Please note that I only care about horizontal overlapping.
For the moment I am doing like this. First we detect the first point we find and use it as a reference, then we normalize dx which is the difference between a point and the reference and then we accumulate:
width = frame.width
rectangle_pixel = (255,0,0)
first_found_coord = (-1,-1)
centroid = (0,0)
centroid_count = 0
for pixel, coordinates in image:
if(pixel != rectangle_pixel):
continue
if(first_found_coord == (-1,-1)):
first_found_coord = coordinates
centroid = coordinates
continue
dx = coordinates.x - first_found_coord.x
if(dx > width/2):
dx -= width
else if(dx < - width/2):
dx -= width
centroid += (dx, coordinates.y)
centroid_count++
final_centroid = centroid / centroid_count
But it doesn't work as expected. Where is the problem, is there a faster solution ?
Here is a solution based on transition points, i.e when you move from red to non red, or in the other way. To capture the horizontal center, I needed the following information :
gridSize.x : width of the space where rectangles can live.
w : width of your rectangle.
PseudoCode:
redPixel = (255,0,0);
transitionPoints = [];
betweenTransitionsColor = -1;
// take i and i+1 pixel+position, increment i by one at each step.
for (pixel1, P1), (pixel1, P2) in gridX : // horizontal points for a fixed `y`
if pixel1 != pixel2: // one is red, the other white
nonRedPosition = (pixel1 != redPixel ? P1 : P2)
transitionPoints.append(nonRedPosition)
continue
if(transitionPoints.length == 1 && betweenTransitionsColor == -1):
betweenTransitionsColor = pixel2
if transitionPoints.length == 2:
break
//Case where your rectangle is on the edge (left or right)
if(transitionPoints.length == 1):
if(abs(transitionPoints[0].x - w) < 2):
xCenter = w/2
else:
xCenter = gridSize.x - w/2
else:
[tP1, tP2] = transitionPoints
// case 1 : The rectangle is splitted
if betweenTransitionsColor != redPixel:
xCenter = (tP2.x - gridSize.x + tP1.x)/2
else:
xCenter = (tP1.x + tP1.x)/2
Note :
you must start at a y position where you could get red pixels. This shouldn't be very hard to achieve. If your rectangle's height is bigger than gridSize.y/2, you can begin at gridSize.y/2. Otherwise, you can search for a first red pixel, and set y to the corresponding position.
Since I'm computing the bounding boxes in the same scope, I do it in two steps.
I first accumulate the coordinates of the pixels of interest. Then when I'm checking for overlapping bounding boxes, I subtract the with for each overlapping colors on the right half of the image. So I end up with a completed but slided rectangle.
At the end I divide by the number of point found per color. If the result is negative I shift it by the size of width of the image.
Alternatively:
def get_centroid(image, interest_color):
acc_x = 0
acc_y = 0
count = 0
first_pixel = (0,0)
for (x,y, color) in image:
if(color not in interest_color):
continue
if(count == 0):
first_pixel = (x,y)
dx = x - first_pixel.x
if(dx > L/2)
dx -= L
else if (dx < -L/2)
dx += L
acc_x += x
acc_y += y
count++
non_scaled_result = acc_x / count, acc_y / count
result = non_scaled_result + first_pixel
return result

Color gradient algorithm

Given two rgb colors and a rectangle, I'm able to create a basic linear gradient. This blog post gives very good explanation on how to create it. But I want to add one more variable to this algorithm, angle. I want to create linear gradient where I can specified the angle of the color.
For example, I have a rectangle (400x100). From color is red (255, 0, 0) and to color is green (0, 255, 0) and angle is 0°, so I will have the following color gradient.
Given I have the same rectangle, from color and to color. But this time I change angle to 45°. So I should have the following color gradient.
Your question actually consists of two parts:
How to generate a smooth color gradient between two colors.
How to render a gradient on an angle.
The intensity of the gradient must be constant in a perceptual color space or it will look unnaturally dark or light at points in the gradient. You can see this easily in a gradient based on simple interpolation of the sRGB values, particularly the red-green gradient is too dark in the middle. Using interpolation on linear values rather than gamma-corrected values makes the red-green gradient better, but at the expense of the back-white gradient. By separating the light intensities from the color you can get the best of both worlds.
Often when a perceptual color space is required, the Lab color space will be proposed. I think sometimes it goes too far, because it tries to accommodate the perception that blue is darker than an equivalent intensity of other colors such as yellow. This is true, but we are used to seeing this effect in our natural environment and in a gradient you end up with an overcompensation.
A power-law function of 0.43 was experimentally determined by researchers to be the best fit for relating gray light intensity to perceived brightness.
I have taken here the wonderful samples prepared by Ian Boyd and added my own proposed method at the end. I hope you'll agree that this new method is superior in all cases.
Algorithm MarkMix
Input:
color1: Color, (rgb) The first color to mix
color2: Color, (rgb) The second color to mix
mix: Number, (0..1) The mix ratio. 0 ==> pure Color1, 1 ==> pure Color2
Output:
color: Color, (rgb) The mixed color
//Convert each color component from 0..255 to 0..1
r1, g1, b1 ← Normalize(color1)
r2, g2, b2 ← Normalize(color1)
//Apply inverse sRGB companding to convert each channel into linear light
r1, g1, b1 ← sRGBInverseCompanding(r1, g1, b1)
r2, g2, b2 ← sRGBInverseCompanding(r2, g2, b2)
//Linearly interpolate r, g, b values using mix (0..1)
r ← LinearInterpolation(r1, r2, mix)
g ← LinearInterpolation(g1, g2, mix)
b ← LinearInterpolation(b1, b2, mix)
//Compute a measure of brightness of the two colors using empirically determined gamma
gamma ← 0.43
brightness1 ← Pow(r1+g1+b1, gamma)
brightness2 ← Pow(r2+g2+b2, gamma)
//Interpolate a new brightness value, and convert back to linear light
brightness ← LinearInterpolation(brightness1, brightness2, mix)
intensity ← Pow(brightness, 1/gamma)
//Apply adjustment factor to each rgb value based
if ((r+g+b) != 0) then
factor ← (intensity / (r+g+b))
r ← r * factor
g ← g * factor
b ← b * factor
end if
//Apply sRGB companding to convert from linear to perceptual light
r, g, b ← sRGBCompanding(r, g, b)
//Convert color components from 0..1 to 0..255
Result ← MakeColor(r, g, b)
End Algorithm MarkMix
Here's the code in Python:
def all_channels(func):
def wrapper(channel, *args, **kwargs):
try:
return func(channel, *args, **kwargs)
except TypeError:
return tuple(func(c, *args, **kwargs) for c in channel)
return wrapper
#all_channels
def to_sRGB_f(x):
''' Returns a sRGB value in the range [0,1]
for linear input in [0,1].
'''
return 12.92*x if x <= 0.0031308 else (1.055 * (x ** (1/2.4))) - 0.055
#all_channels
def to_sRGB(x):
''' Returns a sRGB value in the range [0,255]
for linear input in [0,1]
'''
return int(255.9999 * to_sRGB_f(x))
#all_channels
def from_sRGB(x):
''' Returns a linear value in the range [0,1]
for sRGB input in [0,255].
'''
x /= 255.0
if x <= 0.04045:
y = x / 12.92
else:
y = ((x + 0.055) / 1.055) ** 2.4
return y
def all_channels2(func):
def wrapper(channel1, channel2, *args, **kwargs):
try:
return func(channel1, channel2, *args, **kwargs)
except TypeError:
return tuple(func(c1, c2, *args, **kwargs) for c1,c2 in zip(channel1, channel2))
return wrapper
#all_channels2
def lerp(color1, color2, frac):
return color1 * (1 - frac) + color2 * frac
def perceptual_steps(color1, color2, steps):
gamma = .43
color1_lin = from_sRGB(color1)
bright1 = sum(color1_lin)**gamma
color2_lin = from_sRGB(color2)
bright2 = sum(color2_lin)**gamma
for step in range(steps):
intensity = lerp(bright1, bright2, step, steps) ** (1/gamma)
color = lerp(color1_lin, color2_lin, step, steps)
if sum(color) != 0:
color = [c * intensity / sum(color) for c in color]
color = to_sRGB(color)
yield color
Now for part 2 of your question. You need an equation to define the line that represents the midpoint of the gradient, and a distance from the line that corresponds to the endpoint colors of the gradient. It would be natural to put the endpoints at the farthest corners of the rectangle, but judging by your example in the question that is not what you did. I picked a distance of 71 pixels to approximate the example.
The code to generate the gradient needs to change slightly from what's shown above, to be a little more flexible. Instead of breaking the gradient into a fixed number of steps, it is calculated on a continuum based on the parameter t which ranges between 0.0 and 1.0.
class Line:
''' Defines a line of the form ax + by + c = 0 '''
def __init__(self, a, b, c=None):
if c is None:
x1,y1 = a
x2,y2 = b
a = y2 - y1
b = x1 - x2
c = x2*y1 - y2*x1
self.a = a
self.b = b
self.c = c
self.distance_multiplier = 1.0 / sqrt(a*a + b*b)
def distance(self, x, y):
''' Using the equation from
https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
modified so that the distance can be positive or negative depending
on which side of the line it's on.
'''
return (self.a * x + self.b * y + self.c) * self.distance_multiplier
class PerceptualGradient:
GAMMA = .43
def __init__(self, color1, color2):
self.color1_lin = from_sRGB(color1)
self.bright1 = sum(self.color1_lin)**self.GAMMA
self.color2_lin = from_sRGB(color2)
self.bright2 = sum(self.color2_lin)**self.GAMMA
def color(self, t):
''' Return the gradient color for a parameter in the range [0.0, 1.0].
'''
intensity = lerp(self.bright1, self.bright2, t) ** (1/self.GAMMA)
col = lerp(self.color1_lin, self.color2_lin, t)
total = sum(col)
if total != 0:
col = [c * intensity / total for c in col]
col = to_sRGB(col)
return col
def fill_gradient(im, gradient_color, line_distance=None, max_distance=None):
w, h = im.size
if line_distance is None:
def line_distance(x, y):
return x - ((w-1) / 2.0) # vertical line through the middle
ul = line_distance(0, 0)
ur = line_distance(w-1, 0)
ll = line_distance(0, h-1)
lr = line_distance(w-1, h-1)
if max_distance is None:
low = min([ul, ur, ll, lr])
high = max([ul, ur, ll, lr])
max_distance = min(abs(low), abs(high))
pix = im.load()
for y in range(h):
for x in range(w):
dist = line_distance(x, y)
ratio = 0.5 + 0.5 * dist / max_distance
ratio = max(0.0, min(1.0, ratio))
if ul > ur: ratio = 1.0 - ratio
pix[x, y] = gradient_color(ratio)
>>> w, h = 406, 101
>>> im = Image.new('RGB', [w, h])
>>> line = Line([w/2 - h/2, 0], [w/2 + h/2, h-1])
>>> grad = PerceptualGradient([252, 13, 27], [41, 253, 46])
>>> fill_gradient(im, grad.color, line.distance, 71)
And here's the result of the above:
I wanted to point out the common mistake that happens in color mixing when people try average the r, g, and b components:
R = (R1 + R2) / 2;
G = (G1 + G2) / 2;
B = (B1 + B2) / 2;
You can watch the excellent 4 Minute Physics video on the subject:
Computer Color is Broken
The short version is that trying to niavely mixing two colors by averaging the components is wrong:
R = R1*(1-mix) + R2*mix;
G = G1*(1-mix) + G2*mix;
B = B1*(1-mix) + B2*mix;
The problem is that RGB colors on computers are in the sRGB color space. And those numerical values have a gamma of approx 2.4 applied. In order to mix the colors correctly you must first undo this gamma adjustment:
undo the gamma adjustment
apply your r,g,b mixing algorithm above
reapply the gamma
Without applying the inverse gamma, the mixed colors are darker than they're supposed to be. This can be seen in a side-by-side color gradient experiment.
Top (wrong): without accounting for sRGB gamma
Bottom (right): with accounting for sRGB gamma
The algorithm
Rather than the naive:
//This is the wrong algorithm. Don't do this
Color ColorMixWrong(Color c1, Color c2, Single mix)
{
//Mix [0..1]
// 0 --> all c1
// 0.5 --> equal mix of c1 and c2
// 1 --> all c2
Color result;
result.r = c1.r*(1-mix) + c2.r*(mix);
result.g = c1.g*(1-mix) + c2.g*(mix);
result.b = c1.b*(1-mix) + c2.b*(mix);
return result;
}
The correct form is:
//This is the wrong algorithm. Don't do this
Color ColorMix(Color c1, Color c2, Single mix)
{
//Mix [0..1]
// 0 --> all c1
// 0.5 --> equal mix of c1 and c2
// 1 --> all c2
//Invert sRGB gamma compression
c1 = InverseSrgbCompanding(c1);
c2 = InverseSrgbCompanding(c2);
result.r = c1.r*(1-mix) + c2.r*(mix);
result.g = c1.g*(1-mix) + c2.g*(mix);
result.b = c1.b*(1-mix) + c2.b*(mix);
//Reapply sRGB gamma compression
result = SrgbCompanding(result);
return result;
}
The gamma adjustment of sRGB isn't quite just 2.4. They actually have a linear section near black - so it's a piecewise function.
Color InverseSrgbCompanding(Color c)
{
//Convert color from 0..255 to 0..1
Single r = c.r / 255;
Single g = c.g / 255;
Single b = c.b / 255;
//Inverse Red, Green, and Blue
if (r > 0.04045) r = Power((r+0.055)/1.055, 2.4) else r = r / 12.92;
if (g > 0.04045) g = Power((g+0.055)/1.055, 2.4) else g = g / 12.92;
if (b > 0.04045) b = Power((b+0.055)/1.055, 2.4) else b = b / 12.92;
//return new color. Convert 0..1 back into 0..255
Color result;
result.r = r*255;
result.g = g*255;
result.b = b*255;
return result;
}
And you re-apply the companding as:
Color SrgbCompanding(Color c)
{
//Convert color from 0..255 to 0..1
Single r = c.r / 255;
Single g = c.g / 255;
Single b = c.b / 255;
//Apply companding to Red, Green, and Blue
if (r > 0.0031308) r = 1.055*Power(r, 1/2.4)-0.055 else r = r * 12.92;
if (g > 0.0031308) g = 1.055*Power(g, 1/2.4)-0.055 else g = g * 12.92;
if (b > 0.0031308) b = 1.055*Power(b, 1/2.4)-0.055 else b = b * 12.92;
//return new color. Convert 0..1 back into 0..255
Color result;
result.r = r*255;
result.g = g*255;
result.b = b*255;
return result;
}
Update: Mark's right
I tested #MarkRansom comment that the color blending in linear RGB space is good when colors are equal RGB total value; but the linear blending scale does not seem linear - especially for the black-white case.
So i tried mixing in Lab color space, as my intuition suggested (as well as this photography stackexchange answer):
Mark's algorithm sometimes falls over
That's quite simple. Besides angle, you would actually need one more parameter, i.e. how tight/wide the gradient should be. Let's instead just work with two points:
__D
__--
__--
__--
__--
M
Where M is the middle point of the gradient (between red and green) and D shows the direction and distance. Therefore, the gradient becomes:
M'
| __D
| __--
| __--
| __--
| __--
M
__-- |
__-- |
__-- |
__-- |
D'-- |
M"
Which means, along the vector D'D, you change from red to green, linearly as you already know. Along the vector M'M", you keep the color constant.
That was the theory. Now implementation depends on how you actually draw the pixels. Let's assume nothing and say you want to decide the color pixel by pixel (so you can draw in any pixel order.)
That's simple! Let's take a point:
M'
| SA __D
__--| __--
P-- |__ A __--
| -- /| \ __--
| -- | |_--
| --M
|__-- |
__--CA |
__-- |
__-- |
D'-- |
M"
Point P, has angle A with the coordinate system defined by M and D. We know that along the vector M'M", the color doesn't change, so sin(A) doesn't have any significance. Instead, cos(A) shows relatively how far towards D or D' the pixels color should go to. The point CA shows |PM|cos(A) which means the mapping of P over the line defined by M and D, or in details the length of the line PM multiplied by cos(A).
So the algorithm becomes as follows
For every pixel
Calculate CA
If farther than D, definitely green. If before D', definitely red.
Else find the color from red to green based on the ratio of |D'CA|/|D'D|
Based on your comments, if you want to determine the wideness from the canvas size, you can easily calculate D based on your input angle and canvas size, although I personally advise using a separate parameter.
The way I solved this is first by being able to calculate L (lightness) for an RGB color: calculate only the Y (luminance) of CIE XYZ and use that to get L.
static private float rgbToL (float r, float g, float b) {
float Y = 0.21263900587151f * r + 0.71516867876775f * g + 0.072192315360733f * b;
return Y <= 0.0088564516f ? Y * 9.032962962f : 1.16f * (float)Math.pow(Y, 1 / 3f) - 0.16f;
}
That gives L as 0-1 for any RGB. Then to lerp RGB: first interpolate linear RGB, then fix lightness by lerping the start/end L and scale the RGB by targetL / resultL. I posted an Rgb class that does this.
The same library also has an Hsl class which stores a color as HSLuv. It does interpolation by converting to linear RGB, interpolating, converting back to HSLuv and then fixing the brightness by interpolating L from the start/end HSLuv colors.
The comment of #user2799037 is totally correct:
each line is moved by some pixels to the right compared to the previous one.
The actual constant can be computed as the tangent of the angle you specified.

Cubic to equirectangular projection algorithm

I have a cube map texture which defines a surrounding, however I need to pass it to a program which only works with latitude/longitude maps. I am really at lost here on how to do the translation. Any help here?
In other words, I need to come from here:
To this (I think that image has an aditional -90° rotation over the x axis):
update: I got the official names of the projections. By the way, I found the opposite projection here
A general procedure for projecting raster images like this is:
for each pixel of the destination image:
calculate the corresponding unit vector in 3-dimensional space
calculate the x,y coordinate for that vector in the source image
sample the source image at that coordinate and assign the value to the destination pixel
The last step is simply interpolation. We will focus on the other two steps.
The unit vector for a given latitude and longitude is (+z towards the north pole, +x towards the prime meridian):
x = cos(lat)*cos(lon)
y = cos(lat)*sin(lon)
z = sin(lat)
Assume the cube is +/- 1 unit around the origin (i.e. 2x2x2 overall size).
Once we have the unit vector, we can find the face of the cube it's on by looking at the element with the largest absolute value. For example, if our unit vector was <0.2099, -0.7289, 0.6516>, then the y element has the largest absolute value. It's negative, so the point will be found on the -y face of the cube. Normalize the other two coordinates by dividing by the y magnitude to get the location within that face. So, the point will be at x=0.2879, z=0.8939 on the -y face.
I'd like to share my MATLAB implementation of this conversion. I also borrowed from the OpenGL 4.1 specification, Chapter 3.8.10 (found here), as well as Paul Bourke's website (found here). Make sure you look under the subheading: Converting to and from 6 cubic environment maps and a spherical map.
I also used Sambatyon's post above as inspiration. It started off as a port from Python over to MATLAB, but I made the code so that it is completely vectorized (i.e. no for loops). I also take the cubic image and split it up into 6 separate images, as the application I'm building has the cubic image in this format. Also there is no error checking with the code, and that this assumes that all of the cubic images are of the same size (n x n). This also assumes that the images are in RGB format. If you'd like to do this for a monochromatic image, simply comment out those lines of code that require access to more than one channel. Here we go!
function [out] = cubic2equi(top, bottom, left, right, front, back)
% Height and width of equirectangular image
height = size(top, 1);
width = 2*height;
% Flags to denote what side of the cube we are facing
% Z-axis is coming out towards you
% X-axis is going out to the right
% Y-axis is going upwards
% Assuming that the front of the cube is towards the
% negative X-axis
FACE_Z_POS = 1; % Left
FACE_Z_NEG = 2; % Right
FACE_Y_POS = 3; % Top
FACE_Y_NEG = 4; % Bottom
FACE_X_NEG = 5; % Front
FACE_X_POS = 6; % Back
% Place in a cell array
stackedImages{FACE_Z_POS} = left;
stackedImages{FACE_Z_NEG} = right;
stackedImages{FACE_Y_POS} = top;
stackedImages{FACE_Y_NEG} = bottom;
stackedImages{FACE_X_NEG} = front;
stackedImages{FACE_X_POS} = back;
% Place in 3 3D matrices - Each matrix corresponds to a colour channel
imagesRed = uint8(zeros(height, height, 6));
imagesGreen = uint8(zeros(height, height, 6));
imagesBlue = uint8(zeros(height, height, 6));
% Place each channel into their corresponding matrices
for i = 1 : 6
im = stackedImages{i};
imagesRed(:,:,i) = im(:,:,1);
imagesGreen(:,:,i) = im(:,:,2);
imagesBlue(:,:,i) = im(:,:,3);
end
% For each co-ordinate in the normalized image...
[X, Y] = meshgrid(1:width, 1:height);
% Obtain the spherical co-ordinates
Y = 2*Y/height - 1;
X = 2*X/width - 1;
sphereTheta = X*pi;
spherePhi = (pi/2)*Y;
texX = cos(spherePhi).*cos(sphereTheta);
texY = sin(spherePhi);
texZ = cos(spherePhi).*sin(sphereTheta);
% Figure out which face we are facing for each co-ordinate
% First figure out the greatest absolute magnitude for each point
comp = cat(3, texX, texY, texZ);
[~,ind] = max(abs(comp), [], 3);
maxVal = zeros(size(ind));
% Copy those values - signs and all
maxVal(ind == 1) = texX(ind == 1);
maxVal(ind == 2) = texY(ind == 2);
maxVal(ind == 3) = texZ(ind == 3);
% Set each location in our equirectangular image, figure out which
% side we are facing
getFace = -1*ones(size(maxVal));
% Back
ind = abs(maxVal - texX) < 0.00001 & texX < 0;
getFace(ind) = FACE_X_POS;
% Front
ind = abs(maxVal - texX) < 0.00001 & texX >= 0;
getFace(ind) = FACE_X_NEG;
% Top
ind = abs(maxVal - texY) < 0.00001 & texY < 0;
getFace(ind) = FACE_Y_POS;
% Bottom
ind = abs(maxVal - texY) < 0.00001 & texY >= 0;
getFace(ind) = FACE_Y_NEG;
% Left
ind = abs(maxVal - texZ) < 0.00001 & texZ < 0;
getFace(ind) = FACE_Z_POS;
% Right
ind = abs(maxVal - texZ) < 0.00001 & texZ >= 0;
getFace(ind) = FACE_Z_NEG;
% Determine the co-ordinates along which image to sample
% based on which side we are facing
rawX = -1*ones(size(maxVal));
rawY = rawX;
rawZ = rawX;
% Back
ind = getFace == FACE_X_POS;
rawX(ind) = -texZ(ind);
rawY(ind) = texY(ind);
rawZ(ind) = texX(ind);
% Front
ind = getFace == FACE_X_NEG;
rawX(ind) = texZ(ind);
rawY(ind) = texY(ind);
rawZ(ind) = texX(ind);
% Top
ind = getFace == FACE_Y_POS;
rawX(ind) = texZ(ind);
rawY(ind) = texX(ind);
rawZ(ind) = texY(ind);
% Bottom
ind = getFace == FACE_Y_NEG;
rawX(ind) = texZ(ind);
rawY(ind) = -texX(ind);
rawZ(ind) = texY(ind);
% Left
ind = getFace == FACE_Z_POS;
rawX(ind) = texX(ind);
rawY(ind) = texY(ind);
rawZ(ind) = texZ(ind);
% Right
ind = getFace == FACE_Z_NEG;
rawX(ind) = -texX(ind);
rawY(ind) = texY(ind);
rawZ(ind) = texZ(ind);
% Concatenate all for later
rawCoords = cat(3, rawX, rawY, rawZ);
% Finally determine co-ordinates (normalized)
cubeCoordsX = ((rawCoords(:,:,1) ./ abs(rawCoords(:,:,3))) + 1) / 2;
cubeCoordsY = ((rawCoords(:,:,2) ./ abs(rawCoords(:,:,3))) + 1) / 2;
cubeCoords = cat(3, cubeCoordsX, cubeCoordsY);
% Now obtain where we need to sample the image
normalizedX = round(cubeCoords(:,:,1) * height);
normalizedY = round(cubeCoords(:,:,2) * height);
% Just in case.... cap between [1, height] to ensure
% no out of bounds behaviour
normalizedX(normalizedX < 1) = 1;
normalizedX(normalizedX > height) = height;
normalizedY(normalizedY < 1) = 1;
normalizedY(normalizedY > height) = height;
% Place into a stacked matrix
normalizedCoords = cat(3, normalizedX, normalizedY);
% Output image allocation
out = uint8(zeros([size(maxVal) 3]));
% Obtain column-major indices on where to sample from the
% input images
% getFace will contain which image we need to sample from
% based on the co-ordinates within the equirectangular image
ind = sub2ind([height height 6], normalizedCoords(:,:,2), ...
normalizedCoords(:,:,1), getFace);
% Do this for each channel
out(:,:,1) = imagesRed(ind);
out(:,:,2) = imagesGreen(ind);
out(:,:,3) = imagesBlue(ind);
I've also made the code publicly available through github and you can go here for it. Included is the main conversion script, a test script to show its use and a sample set of 6 cubic images pulled from Paul Bourke's website. I hope this is useful!
Project changed name to libcube2cyl. Same goodness, better working examples both in C and C++.
Now also available in C.
I happened to solve the exact same problem as you described.
I wrote this tiny C++ lib called "Cube2Cyl", you can find the detailed explanation of algorithm here: Cube2Cyl
Please find the source code from github: Cube2Cyl
It is released under MIT licence, use it for free!
So, I found a solution mixing this article on spherical coordinates from wikipedia and the Section 3.8.10 from the OpenGL 4.1 specification (plus some hacks to make it work). So, assuming that the cubic image has a height h_o and width w_o, the equirectangular will have a height h = w_o / 3 and a width w = 2 * h. Now for each pixel (x, y) 0 <= x <= w, 0 <= y <= h in the equirectangular projection, we want to find the corresponding pixel in the cubic projection, I solve it using the following code in python (I hope I didn't make mistakes while translating it from C)
import math
# from wikipedia
def spherical_coordinates(x, y):
return (math.pi*((y/h) - 0.5), 2*math.pi*x/(2*h), 1.0)
# from wikipedia
def texture_coordinates(theta, phi, rho):
return (rho * math.sin(theta) * math.cos(phi),
rho * math.sin(theta) * math.sin(phi),
rho * math.cos(theta))
FACE_X_POS = 0
FACE_X_NEG = 1
FACE_Y_POS = 2
FACE_Y_NEG = 3
FACE_Z_POS = 4
FACE_Z_NEG = 5
# from opengl specification
def get_face(x, y, z):
largest_magnitude = max(x, y, z)
if largest_magnitude - abs(x) < 0.00001:
return FACE_X_POS if x < 0 else FACE_X_NEG
elif largest_magnitude - abs(y) < 0.00001:
return FACE_Y_POS if y < 0 else FACE_Y_NEG
elif largest_magnitude - abs(z) < 0.00001:
return FACE_Z_POS if z < 0 else FACE_Z_NEG
# from opengl specification
def raw_face_coordinates(face, x, y, z):
if face == FACE_X_POS:
return (-z, -y, x)
elif face == FACE_X_NEG:
return (-z, y, -x)
elif face == FACE_Y_POS:
return (-x, -z, -y)
elif face == FACE_Y_NEG:
return (-x, z, -y)
elif face == FACE_Z_POS:
return (-x, y, -z)
elif face == FACE_Z_NEG:
return (-x, -y, z)
# computes the topmost leftmost coordinate of the face in the cube map
def face_origin_coordinates(face):
if face == FACE_X_POS:
return (2*h, h)
elif face == FACE_X_NEG:
return (0, 2*h)
elif face == FACE_Y_POS:
return (h, h)
elif face == FACE_Y_NEG:
return (h, 3*h)
elif face == FACE_Z_POS:
return (h, 0)
elif face == FACE_Z_NEG:
return (h, 2*h)
# from opengl specification
def raw_coordinates(xc, yc, ma):
return ((xc/abs(ma) + 1) / 2, (yc/abs(ma) + 1) / 2)
def normalized_coordinates(face, x, y):
face_coords = face_origin_coordinates(face)
normalized_x = int(math.floor(x * h + 0.5))
normalized_y = int(math.floor(y * h + 0.5))
# eliminates black pixels
if normalized_x == h:
--normalized_x
if normalized_y == h:
--normalized_y
return (face_coords[0] + normalized_x, face_coords[1] + normalized_y)
def find_corresponding_pixel(x, y):
spherical = spherical_coordinates(x, y)
texture_coords = texture_coordinates(spherical[0], spherical[1], spherical[2])
face = get_face(texture_coords[0], texture_coords[1], texture_coords[2])
raw_face_coords = raw_face_coordinates(face, texture_coords[0], texture_coords[1], texture_coords[2])
cube_coords = raw_coordinates(raw_face_coords[0], raw_face_coords[1], raw_face_coords[2])
# this fixes some faces being rotated 90°
if face in [FACE_X_NEG, FACE_X_POS]:
cube_coords = (cube_coords[1], cube_coords[0])
return normalized_coordinates(face, cube_coords[0], cube_coords[1])
at the end we just call find_corresponding_pixel for each pixel in the equirectangular projection
I think from your algorithm in Python you might have inverted x and y in the calculation of theta and phi.
def spherical_coordinates(x, y):
return (math.pi*((y/h) - 0.5), 2*math.pi*x/(2*h), 1.0)
from Paul Bourke's website here
theta = x pi
phi = y pi / 2
and in your code you are using y in the theta calculation and x in the phi calculation.
Correct me if I am wrong.

Resources