Create 2d image from point cloud - python

I am trying to project a point cloud into a 2d image as if it were a satellite image.
I have six files I want to project and the point clouds are quite big.
For the biggest one, I have len(las.X) = 37_763_608, max(las.X) - min(las.X) = 122_124, and max(las.X) - min(las.X) = 273_683, so sometimes when calculate the size I have an overflow error.
My first try was this, but this was quite slow and took about 28 minutes to run.
Here I added the loops with k_x and k_y because the image I got was mostly black, and I wanted to have colour everywhere. I tried looping around each point/pixel to make them 5 times bigger, but this is the slow part.
see pictures
Colour version with the k padding
Black and white version without the padding
Ideally, I would like to have the colour from one point/pixel shift to the colour of their neighbours, so that there is a gradient between them, and no have any black leftover from me initialize the image as np.zeros
import laspy
import numpy as np
from PIL import Image
import cv2
from tqdm import tqdm
las = laspy.read("area1.las")
def las_to_rgb(las):
x, y = las.X, las.Y
delta_x = max(x) - min(x)
delta_y = max(y) - min(y)
re_x = x - min(x)
re_y = y - min(y)
# las.red, green and blue are stored as 16bit
r, g, b = (las.red/256).astype(np.uint8), (las.green/256).astype(np.uint8), (las.blue/256).astype(np.uint8)
image = np.zeros((delta_y+1, delta_x+1, 3))
for i, val in enumerate(zip(tqdm(re_x), re_y)):
for k_x in range(-5, 6):
for k_y in range(-5, 6):
if val[0] + k_x < 0 or val[0] + k_x >= delta_x + 1:
k_x = 0
if val[1] + k_y < 0 or val[1] + k_y >= delta_y + 1:
k_y = 0
image[val[1]+k_y, val[0]+k_x] = [b[i], g[i], r[i]]
cv2.imwrite("test.png", image)
cv2.waitKey(0)
I found how to do it faster in numpy, but it can only do one colour at a time, so I decided to loop for multiple color but I think I am doing something wrong when I change the type to np.unit8 as python takes up to 50GB of RAM.
With numpy:
One colour:
def nu_pro(las):
x, y = las.X, las.Y
delta_x = max(x) - min(x)
delta_y = max(y) - min(y)
xs = x - min(x)
ys = y - min(y)
img_size = (delta_y+1, delta_x+1) # +1 for ravel_multi_index
bgr = np.array([(las.blue/256).astype(np.uint8), (las.green/256).astype(np.uint8), (las.red/256).astype(np.uint8)])
coords = np.stack((ys, xs))
abs_coords = np.ravel_multi_index(coords, img_size)
image = np.bincount(abs_coords, weights=color, minlength=img_size[1]*img_size[0])
image = image.reshape(img_size))
cv2.imwrite("test.png", image)
cv2.waitKey(0)
For rgb
def nu_pro_rgb(las):
x, y = las.X, las.Y
delta_x = max(x) - min(x)
delta_y = max(y) - min(y)
xs = x - min(x)
ys = y - min(y)
img_size = (delta_y+1, delta_x+1) # +1 for ravel_multi_index
rgb = np.array([(las.red/256).astype(np.uint8), (las.green/256).astype(np.uint8), (las.blue/256).astype(np.uint8)])
image = []
coords = np.stack((ys, xs))
abs_coords = np.ravel_multi_index(coords, img_size)
for i, color in enumerate(tqdm(rgb)):
img = np.bincount(abs_coords, weights=color, minlength=img_size[1]*img_size[0])
image.append(img.reshape(img_size))
image = np.uint8(np.array(image))
# I am probably messing up this transpose but I'll figure it out eventually
im = Image.fromarray(image.T, "RGB")
im.save("pil.png")
Any indication would be welcome :)
EDIT for clarification about the colours.
When there is overlapping, it should be the point with the highest z coordinates that should be displayed.
For the colouring, in the picture below, the points between A and B should be a colour gradient from A to B.
If it is like the yellow point, then an average of the neighbouring colour (without the black if present)
I hope I am making some sense.

To interpolate, there are lots of libraries.
This uses cubic interpolation, but it only works inside the convex hull, so the points outside the convex hull are taken from the nearest neighbor.
If you are interpolating GIS data, you may look on Krigging interpolation, which should interpolate outside the convex hull.
This code does not check that a point with lower Z is under one with higher Z. You have to delete those points to avoid having them interpolated.
from scipy.interpolate import griddata
import numpy as np
import matplotlib.pyplot as plt
import cv2
# create data
height, width = 256, 256
# generate a random sample of 1000 (x,y) coordinates and colors
x, y, z = np.random.randint(0, 256, size=(3, 1000))
color = np.random.randint(0, 256, size=(1000, 3))
# sort x,y,z by z in ascending order so the highest z is plotted over the lowest z
zSort = z.argsort()
x, y, z, color = x[zSort], y[zSort], z[zSort], color[zSort]
# interpolation
# generate a grid where the interpolation will be calculated
X, Y = np.meshgrid(np.arange(width), np.arange(height))
R = griddata(np.vstack((x, y)).T, color[:, 0], (X, Y), method='cubic')
Rlinear= griddata(np.vstack((x, y)).T, color[:, 0], (X, Y), method='nearest')
G = griddata(np.vstack((x, y)).T, color[:, 1], (X, Y), method='cubic')
Glinear= griddata(np.vstack((x, y)).T, color[:, 1], (X, Y), method='nearest')
B = griddata(np.vstack((x, y)).T, color[:, 2], (X, Y), method='cubic')
Blinear= griddata(np.vstack((x, y)).T, color[:, 2], (X, Y), method='nearest')
#Fill empty values with nearest neighbor
R[np.isnan(R)] = Rlinear[np.isnan(R)]
G[np.isnan(G)] = Glinear[np.isnan(G)]
B[np.isnan(B)] = Blinear[np.isnan(B)]
R = R/np.max(R)
G = G/np.max(G)
B = B/np.max(B)
interpolated = cv2.merge((R, G, B))
plt.imshow(interpolated)
plt.scatter(x, y, c=color/255, marker="s",s=1)
plt.show()

I do not have access to the format you use, so I show you how to rapidly plot points at x,y coordinates, and enlarge them with a kernel mask, and a color for each point
import numpy as np
import cv2
height, width = 256, 256
# generate a random sample of 1000 (x,y) coordinates and colors
x, y, = np.random.randint(0, 256, size=(2, 1000))
color = np.random.randint(0, 256, size=(1000, 3))
# generate a blank image
# int16 to manage overflow colors when convolving
pointsPlotted = np.zeros((height, width, 3), np.uint16)
# plot x,y,color into blankImage
pointsPlotted[y, x] = color
cv2.imshow("points", pointsPlotted.astype(np.uint8))
# convlove the image with a kernel of ones, size k
k = 5
kernel = np.ones((k, k), np.int16)
largerSquares = cv2.filter2D(src=pointsPlotted, ddepth=-1, kernel=kernel)
# limit max color to 255
largerSquares[largerSquares > 255] = 255
# Convert to uint8
largerSquares = largerSquares.astype(np.uint8)
cv2.imshow("Larger Squares", largerSquares)
Is this what you want?
On the overlaps, adds the colors (capped to 255)

Related

How do I use Piecewise Affine Transformation to straighten curved text line/ contour?

Consider the following image:
and the following bounding contour( which is a smooth version of the output of a text-detection neural network of the above image ), so this contour is a given.
I need to warp both images so that I end up with a straight enough textline, so that it can be fed to a text recognition neural network:
using Piecewise Affine Transformation, or some other method. with an implementation if possible or key points of implementation in python.
I know how to find the medial axis, order its points, simplify it (e.g using Douglas-Peucker algorithm), and find the corresponding points on a straight line.
EDIT: the question can be rephrased -naively- as the following :
have you tried the "puppet warp" feature in Adobe Photoshop? you specify "joint" points on an image , and you move these points to the desired place to perform the image warping, we can calculate the source points using a simplified medial axis (e.g 20 points instead of 200 points), and calculate the corresponding target points on a straight line, how to perform Piecewise Affine Transformation using these two sets of points( source and target)?
EDIT: modified the images, my bad
Papers
Here's a paper that does the needed result:
A Novel Technique for Unwarping Curved Handwritten Texts Using Mathematical Morphology and Piecewise Affine Transformation
another paper: A novel method for straightening curved text-lines in stylistic documents
Similar questions:
Straighten B-Spline
Challenge : Curved text extraction using python
How to convert curves in images to lines in Python?
Deforming an image so that curved lines become straight lines
Straightening a curved contour
Full code also available in this notebook , runtime -> run all to reproduce the result.
import cv2
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from scipy import interpolate
from scipy.spatial import distance
from shapely.geometry import LineString, GeometryCollection, MultiPoint
from skimage.morphology import skeletonize
from sklearn.decomposition import PCA
from warp import PiecewiseAffineTransform # https://raw.githubusercontent.com/TimSC/image-piecewise-affine/master/warp.py
# Helper functions
def extendline(line, length):
a = line[0]
b = line[1]
lenab = distance.euclidean(a, b)
cx = b[0] + ((b[0] - a[0]) / lenab * length)
cy = b[1] + ((b[1] - a[1]) / lenab * length)
return [cx, cy]
def XYclean(x, y):
xy = np.concatenate((x.reshape(-1, 1), y.reshape(-1, 1)), axis=1)
# make PCA object
pca = PCA(2)
# fit on data
pca.fit(xy)
# transform into pca space
xypca = pca.transform(xy)
newx = xypca[:, 0]
newy = xypca[:, 1]
# sort
indexSort = np.argsort(x)
newx = newx[indexSort]
newy = newy[indexSort]
# add some more points (optional)
f = interpolate.interp1d(newx, newy, kind='linear')
newX = np.linspace(np.min(newx), np.max(newx), 100)
newY = f(newX)
# #smooth with a filter (optional)
# window = 43
# newY = savgol_filter(newY, window, 2)
# return back to old coordinates
xyclean = pca.inverse_transform(np.concatenate((newX.reshape(-1, 1), newY.reshape(-1, 1)), axis=1))
xc = xyclean[:, 0]
yc = xyclean[:, 1]
return np.hstack((xc.reshape(-1, 1), yc.reshape(-1, 1))).astype(int)
def contour2skeleton(cnt):
x, y, w, h = cv2.boundingRect(cnt)
cnt_trans = cnt - [x, y]
bim = np.zeros((h, w))
bim = cv2.drawContours(bim, [cnt_trans], -1, color=255, thickness=cv2.FILLED) // 255
sk = skeletonize(bim > 0)
#####
skeleton_yx = np.argwhere(sk > 0)
skeleton_xy = np.flip(skeleton_yx, axis=None)
xx, yy = skeleton_xy[:, 0], skeleton_xy[:, 1]
skeleton_xy = XYclean(xx, yy)
skeleton_xy = skeleton_xy + [x, y]
return skeleton_xy
mm = cv2.imread('cont.png', cv2.IMREAD_GRAYSCALE)
plt.imshow(mm)
cnts, _ = cv2.findContours(mm.astype('uint8'), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cont = cnts[0].reshape(-1, 2)
# find skeleton
sk = contour2skeleton(cont)
mm = np.zeros_like(mm)
cv2.polylines(mm, [sk], False, 255, 2)
plt.imshow(mm)
# simplify the skeleton
ln = LineString(sk).simplify(2)
sk_simp = np.int0(ln.coords)
mm = np.zeros_like(mm)
for pt in sk_simp:
cv2.circle(mm, pt, 5, 255, -1)
plt.imshow(mm)
# extend both ends of the skeleton
print(len(sk_simp))
a, b = sk_simp[1], sk_simp[0]
c1 = np.int0(extendline([a, b], 50))
sk_simp = np.vstack([c1, sk_simp])
a, b = sk_simp[-2], sk_simp[-1]
c2 = np.int0(extendline([a, b], 50))
sk_simp = np.vstack([sk_simp, c2])
print(len(sk_simp))
cv2.circle(mm, c1, 10, 255, -1)
cv2.circle(mm, c2, 10, 255, -1)
plt.imshow(mm)
########
# find the target points
########
pts1 = sk_simp.copy()
dists = [distance.euclidean(p1, p2) for p1, p2 in zip(pts1[:-1], pts1[1:])]
zip1 = list(zip(pts1[:-1], dists))
# find the first 2 target points
a = pts1[0]
b = a - (dists[0], 0)
pts2 = [a, b, ]
for z in zip1[1:]:
lastpt = pts2[-1]
pt, dst = z
ln = [a, lastpt]
c = extendline(ln, dst)
pts2.append(c)
pts2 = np.int0(pts2)
ln1 = LineString(pts1)
ln2 = LineString(pts2)
GeometryCollection([ln1.buffer(5), ln2.buffer(5),
MultiPoint(pts2), MultiPoint(pts1)])
########
# create translated copies of source and target points
# 50 is arbitary
pts1 = np.vstack([pts1 + [0, 50], pts1 + [0, -50]])
pts2 = np.vstack([pts2 + [0, 50], pts2 + [0, -50]])
MultiPoint(pts1)
########
# performing the warping
im = Image.open('orig.png')
dstIm = Image.new(im.mode, im.size, color=(255, 255, 255))
# Perform transform
PiecewiseAffineTransform(im, pts1, dstIm, pts2)
plt.figure(figsize=(10, 10))
plt.imshow(dstIm)
1- find medial axis , e.g using skimage.morphology.skeletonize and simplify it ,e.g using shapely object.simplify , I used a tolerance of 2 , the medial axis points are in white:
2- find the corresponding points on a straight line, using the distance between each point and the next:
3 - also added extra points on the ends, colored blue, so that the points fit the entire contour length
4- create 2 copies of the source and target points, one copy translated up and the other translated down (I choose an offset of 50 here), so the source points are now like this, please note that simple upward/downward displacement may not be the best approach for all contours, e.g if the contour is curving with degrees > 45:
5- using the code here , perform PiecewiseAffineTransform using the source and target points, here's the result, it's straight enough:
If the goal is to just unshift each column, then:
import numpy as np
from PIL import Image
source_img = Image.open("73614379-input-v2.png")
contour_img = Image.open("73614379-map-v3.png").convert("L")
assert source_img.size == contour_img.size
contour_arr = np.array(contour_img) != 0 # convert to boolean array
col_offsets = np.argmax(
contour_arr, axis=0
) # find the first non-zero row for each column
assert len(col_offsets) == source_img.size[0] # sanity check
min_nonzero_col_offset = np.min(
col_offsets[col_offsets > 0]
) # find the minimum non-zero row
target_img = Image.new("RGB", source_img.size, (255, 255, 255))
for x, col_offset in enumerate(col_offsets):
offset = col_offset - min_nonzero_col_offset if col_offset > 0 else 0
target_img.paste(
source_img.crop((x, offset, x + 1, source_img.size[1])), (x, 0)
)
target_img.save("unshifted3.png")
with the new input and the new contour from OP outputs this image:

Why is my code only working on part of my image?

I created code to equalize the luminosity values of pixels in an image so that when the image is further edited I do not have dark or light spots in my final image. However, the code seems to stop short and only equalize part of my image. Any ideas as to why the code is stopping early?
Here is my code:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
img = mpimg.imread('EXP_0159-2_8b.tif')
imgOut = img.copy()
for i in range(0, len(img[0, :])):
imgLine1 = (img[:, i] < 165) * img[:, i]
p = imgLine1.nonzero()
if len(p[0]) < 1:
imgOut[:, i] == 0
else:
imgLine2 = imgLine1[p[0]]
def curvefitting(lineFunction):
x = np.arange(0, len(lineFunction))
y = lineFunction
curve = np.polyfit(x, y, deg = 2)
a = curve[0]
b = curve[1]
c = curve[2]
curveEquation = (a*(x**2)) + (b*(x**1)) + (c)
curveCorrected = lineFunction - curveEquation + 200
return curveCorrected
imgLine1[p[0]] = curvefitting(imgLine2)
imgOut[:, i] = imgLine1
plt.imshow(imgOut, cmap = 'gray')
The for loop takes the individual columns of pixels in my image and restricts the endpoints of that column to (0, 165), so that pixels outside of that range are turned into zero and ignored by the nonzero() function. The if condition just finalizes the conversion of values outside (0, 165) to zero. Additionally, I converted the image to gray so I would not have to deal with colors and could focus only on luminosity.
This is my corrected image. The program works to average the luminosity values across the entire surface. However, you can see that it stops before reaching the end. The initial image was darker on the sides and lighter in the middle, but the file is too large to upload.
Any help is greatly appreciated.
If you are not interested in color you can convert input image to grayscale. That would simplified the matrix multiplications. The simplified version would be
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
def rgb2gray(rgb):
return np.dot(rgb[...,:3], [0.2989, 0.5870, 0.1140])
def curvefitting(lineFunction):
x = np.arange(0, len(lineFunction))
y = lineFunction
curve = np.polyfit(x, y, deg = 2)
a = curve[0]
b = curve[1]
c = curve[2]
curveEquation = [(a*(x_**2)) + (b*(x_**1)) + (c) for x_ in x]
curveCorrected = lineFunction - curveEquation + 200
return curveCorrected
img = mpimg.imread('EXP_0159-2_8b.tif')
img = rgb2gray(img)
imgOut = img.copy()
for i in range(0, len(img[0, :])):
imgLine1 = (img[:, i] < 165) * img[:, i]
p = imgLine1.nonzero()
if len(p) < 1:
imgOut[:, i] == 0
else:
imgLine2 = imgLine1[p]
imgLine1[p] = curvefitting(imgLine2)
imgOut[:, i] = imgLine1
plt.imshow(imgOut, cmap = 'gray')
plt.show()

Find patches of given size in circular shape from numpy array having no zero values

Let say, I have a grayscale image that has some black pixels as shown below:
In this image, I am trying to find out patches having no zero values. For simplicity, let's assume that overlapping patches are allowed. The challenge is that these patches aren't rectangular but circular in shape. Please see an example below:
Please note that there are many such patches possible in the image. However, for illustration purposes, I have just manually drawn a few.
It is possible to find such patches using for nested for loop but this doesn't look the optimal way.
# find one circular patch
for y in range(-radius, radius):
for x in range(-radius, radius):
if x**2 + y**2 < radius**2:
# this pixel in inside the circular patch
patch_x, patch_y = img_x + x, img_y + y
I am trying to use convolution operation but no luck so far
import cv2
import numpy as np
radius = 20
img = cv2.imread('img.png', cv2.CV_8UC1)
candidates = img != 0
patch_shape = (radius, radius)
out = np.lib.stride_tricks.as_strided(
candidates,
shape=(candidates.shape[0] - patch_shape[0] + 1, \
candidates.shape[1] - patch_shape[1] + 1, \
*patch_shape),
strides=2*img.strides,
writeable=False,
)
patches = np.argwhere(out.all(axis=(-2, -1)))
My goal is to find all (if not at least a few say 10) patches of given size in circular shape from Numpy array having no zero values.
I would go with convolution.
There is a nice trick to generate a circular kernel (mask)
def create_circular_kernel(radius):
center = (radius, radius)
h = 2*radius
w = 2*radius
Y, X = np.ogrid[:h, :w]
dist_from_center = np.sqrt((X - center[0])**2 + (Y-center[1])**2)
mask = dist_from_center <= radius
return mask
And then circle centers can be found using convolution:
In your case, it should be equal to zero.
from scipy.signal import correlate2d
radius = 20
kernel = create_circular_kernel(radius)
convolved_image = correlate2d(candidates, kernel, 'same')
patch_centers = np.where(convolved_image==0, 1,0)
This gives image with values 1, where you can draw a circle containing no zero values.

Converting Voronoi Diagram Cell Areas to Lists of Pixel Coordinates

I am using Voronoi diagrams for image processing (procedurally generated stippling).
In order to do this I need to create a list (cells) of a list (coords_within_cell) of tuples (x,y pixel locations).
I have developed a couple brute-force algorithms to accomplish this (see below), but they are too slow to process more than ~10 points. The scipy spatial utilities seem to be more than 1000x more efficient. Because of this, I would like to use scipy to generate the Voronoi diagram:
https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Voronoi.html
Using scipy to generate the Voronoi diagram is fairly simple but unfortunately I cannot figure out how to convert the cell areas into pixel coordinates. What is the best way to do this?
I found a related question, but it has no answers and it was deleted: https://web.archive.org/web/20200120151304/https://stackoverflow.com/questions/57703129/converting-a-voronoi-diagram-into-bitmap
Brute Force Algorithm 1 (too slow)
import math
import random
from PIL import Image
def distance(x1, y1, x2, y2):
return math.hypot(x2 - x1, y2 - y1)
# define the size of the x and y bounds
screen_width = 1260
screen_height = 1260
# define the number of points that should be used
number_of_points = 16
# randomly generate a list of n points within the given x and y bounds
point_x_coordinates = random.sample(range(0, screen_width), number_of_points)
point_y_coordinates = random.sample(range(0, screen_height), number_of_points)
points = list(zip(point_x_coordinates, point_y_coordinates))
# each point needs to have a corresponding list of pixels
point_pixels = []
for i in range(len(points)):
point_pixels.append([])
# for each pixel within bounds, determine which point it is closest to and add it to the corresponding list in point_pixels
for pixel_y_coordinate in range(screen_height):
for pixel_x_coordinate in range(screen_width):
distance_to_closest_point = float('inf')
closest_point_index = 1
for point_index, point in enumerate(points):
distance_to_point = distance(pixel_x_coordinate, pixel_y_coordinate, point[0], point[1])
if(distance_to_point < distance_to_closest_point):
closest_point_index = point_index
distance_to_closest_point = distance_to_point
point_pixels[closest_point_index].append((pixel_x_coordinate, pixel_y_coordinate))
# each point needs to have a corresponding centroid
point_pixels_centroid = []
for pixel_group in point_pixels:
x_sum = 0
y_sum = 0
for pixel in pixel_group:
x_sum += pixel[0]
y_sum += pixel[1]
x_average = x_sum / len(pixel_group)
y_average = y_sum / len(pixel_group)
point_pixels_centroid.append((round(x_average), round(y_average)))
# display the resulting voronoi diagram
display_voronoi = Image.new("RGB", (screen_width, screen_height), "white")
for pixel_group in point_pixels:
rgb = random.sample(range(0, 255), 3)
for pixel in pixel_group:
display_voronoi.putpixel( pixel, (rgb[0], rgb[1], rgb[2], 255) )
for centroid in point_pixels_centroid:
print(centroid)
display_voronoi.putpixel( centroid, (1, 1, 1, 255) )
display_voronoi.show()
Brute Force Algorithm 2 (also too slow):
Based on this concept.
import math
import random
from PIL import Image
def distance(x1, y1, x2, y2):
return math.hypot(x2 - x1, y2 - y1)
# define the size of the x and y bounds
screen_width = 500
screen_height = 500
# define the number of points that should be used
number_of_points = 4
# randomly generate a list of n points within the given x and y bounds
point_x_coordinates = random.sample(range(0, screen_width), number_of_points)
point_y_coordinates = random.sample(range(0, screen_height), number_of_points)
points = list(zip(point_x_coordinates, point_y_coordinates))
# each point needs to have a corresponding list of pixels
point_pixels = []
for i in range(len(points)):
point_pixels.append([])
# for each pixel within bounds, determine which point it is closest to and add it to the corresponding list in point_pixels
# do this by continuously growing circles outwards from the points
# if circles overlap then whoever was their first claims the location
# keep track of whether pixels have been used or not
# this is done via a 2D list of booleans
is_drawn_on = []
for i in range(screen_width):
is_drawn_on.append([])
for j in range(screen_height):
is_drawn_on[i].append(False)
circles_are_growing = True
radius = 1
while(circles_are_growing):
circles_are_growing = False
for point_index, point in enumerate(points):
for i in range(point[0] - radius, point[0] + radius):
for j in range(point[1] - radius, point[1] + radius):
# print(str(i)+" vs "+str(len(is_drawn_on)))
if(i >= 0 and i < len(is_drawn_on)):
if(j >= 0 and j < len(is_drawn_on[i])):
if(not is_drawn_on[i][j] and distance(i, j, point[0], point[1]) <= radius):
point_pixels[point_index].append((i, j))
circles_are_growing = True
is_drawn_on[i][j] = True
radius += 1
# each point needs to have a corresponding centroid
point_pixels_centroid = []
for pixel_group in point_pixels:
x_sum = 0
y_sum = 0
for pixel in pixel_group:
x_sum += pixel[0]
y_sum += pixel[1]
x_average = x_sum / len(pixel_group)
y_average = y_sum / len(pixel_group)
point_pixels_centroid.append((round(x_average), round(y_average)))
# display the resulting voronoi diagram
display_voronoi = Image.new("RGB", (screen_width, screen_height), "white")
for pixel_group in point_pixels:
rgb = random.sample(range(0, 255), 3)
for pixel in pixel_group:
display_voronoi.putpixel( pixel, (rgb[0], rgb[1], rgb[2], 255) )
for centroid in point_pixels_centroid:
print(centroid)
display_voronoi.putpixel( centroid, (1, 1, 1, 255) )
display_voronoi.show()
Rather than build and interrogate the Voronoi diagram directly, it is easier to build and query a standard search tree. Below is my modification of your code using scipy.spatial.KDTree to determine the closest point for each pixel location followed by an image of the result (a 500x500 image with 500 Voronoi points).
The code is still a little slow but now scales well in the number of Voronoi points. This could be faster if you avoid building the list of pixel locations for each Voronoi cell and instead just directly set data in the image.
The fastest solution may involve building the Voronoi diagam and walking through it one pixel at a time and associating the closest Voronoi cell, looking at neighboring Voronoi cells when needed (since the previous pixel gives a very good guess for finding the Voronoi cell for the next pixel). But that will involve writing a lot more code that using the KDTree naively like this and probably not yield huge gains: the slow part of the code at this point is building all the per-pixel arrays/data which can be cleaned up independently.
import math
import random
from PIL import Image
from scipy import spatial
import numpy as np
# define the size of the x and y bounds
screen_width = 500
screen_height = 500
# define the number of points that should be used
number_of_points = 500
# randomly generate a list of n points within the given x and y bounds
point_x_coordinates = random.sample(range(0, screen_width), number_of_points)
point_y_coordinates = random.sample(range(0, screen_height), number_of_points)
points = list(zip(point_x_coordinates, point_y_coordinates))
# each point needs to have a corresponding list of pixels
point_pixels = []
for i in range(len(points)):
point_pixels.append([])
# build a search tree
tree = spatial.KDTree(points)
# build a list of pixed coordinates to query
pixel_coordinates = np.zeros((screen_height*screen_width, 2));
i = 0
for pixel_y_coordinate in range(screen_height):
for pixel_x_coordinate in range(screen_width):
pixel_coordinates[i] = np.array([pixel_x_coordinate, pixel_y_coordinate])
i = i+1
# for each pixel within bounds, determine which point it is closest to and add it to the corresponding list in point_pixels
[distances, indices] = tree.query(pixel_coordinates)
i = 0
for pixel_y_coordinate in range(screen_height):
for pixel_x_coordinate in range(screen_width):
point_pixels[indices[i]].append((pixel_x_coordinate, pixel_y_coordinate))
i = i+1
# each point needs to have a corresponding centroid
point_pixels_centroid = []
for pixel_group in point_pixels:
x_sum = 0
y_sum = 0
for pixel in pixel_group:
x_sum += pixel[0]
y_sum += pixel[1]
x_average = x_sum / max(len(pixel_group),1)
y_average = y_sum / max(len(pixel_group),1)
point_pixels_centroid.append((round(x_average), round(y_average)))
# display the resulting voronoi diagram
display_voronoi = Image.new("RGB", (screen_width, screen_height), "white")
for pixel_group in point_pixels:
rgb = random.sample(range(0, 255), 3)
for pixel in pixel_group:
display_voronoi.putpixel( pixel, (rgb[0], rgb[1], rgb[2], 255) )
for centroid in point_pixels_centroid:
#print(centroid)
display_voronoi.putpixel( centroid, (1, 1, 1, 255) )
#display_voronoi.show()
display_voronoi.save("test.png")
scipy.interpolate.griddata does exactly that, and more or less by the same method as in Alex's answer
import numpy as np
from numpy.random import default_rng
from scipy.interpolate import griddata
# define the size of the x and y bounds
screen_width = 1260
screen_height = 1260
# define the number of points that should be used
number_of_points = 16
# randomly generate a list of n points within the given x and y bounds
rng = default_rng()
points = rng.random((number_of_points,2)) * [screen_width, screen_height]
grid_x, grid_y = np.mgrid[0:screen_width, 0:screen_height]
labels = griddata(points, np.arange(number_of_points), (grid_x, grid_y), method='nearest')
Then, you can use np.where(labels==10) to get the coordinates of all the pixels that belong to cell #10.
Or your can use all the machinery in scipy.ndimage to measure various properties of labeld regions. For instance the center of gravity.
If you want to display colorful cells:
from matplotlib.pyplot import imsave
rgb = rng.integers(0, 255, size=(number_of_points,3))
rgb_labels = rgb[labels]
imsave('test.png', rgb_labels)

Is it possible in OpenCV to plot local curvature as a heat-map representing an object's "pointiness"?

Given a thresholded image of blobs that you can detect and draw contours around, is it possible when drawing the contour to represent the local curvature as a heat-map?
i.e. is it (1) possible to determine local curvature on a open cv contour (2) map this curvature to a heat-map color space (3) draw the contour as a heatmap.
My goal is to measure the "pointiness" of an object so that I can draw a vector from the pointy side to the opposite non-pointy side. For my objects, I happen to know that the pointy side is the top.
If other techniques would be more effective at representing "pointiness" than curvature feel free to suggest.
EDIT: Fixed a bug in the previous version.
I used angle between the gradient vectors at the ith and (i + n)th point on the contour as the score to determine the pointiness of a point. Code and results below.
import numpy as np
import cv2
import pylab as pl
def compute_pointness(I, n=5):
# Compute gradients
# GX = cv2.Sobel(I, cv2.CV_32F, 1, 0, ksize=5, scale=1)
# GY = cv2.Sobel(I, cv2.CV_32F, 0, 1, ksize=5, scale=1)
GX = cv2.Scharr(I, cv2.CV_32F, 1, 0, scale=1)
GY = cv2.Scharr(I, cv2.CV_32F, 0, 1, scale=1)
GX = GX + 0.0001 # Avoid div by zero
# Threshold and invert image for finding contours
_, I = cv2.threshold(I, 100, 255, cv2.THRESH_BINARY_INV)
# Pass in copy of image because findContours apparently modifies input.
C, H = cv2.findContours(I.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
heatmap = np.zeros_like(I, dtype=np.float)
pointed_points = []
for contour in C:
contour = contour.squeeze()
measure = []
N = len(contour)
for i in xrange(N):
x1, y1 = contour[i]
x2, y2 = contour[(i + n) % N]
# Angle between gradient vectors (gx1, gy1) and (gx2, gy2)
gx1 = GX[y1, x1]
gy1 = GY[y1, x1]
gx2 = GX[y2, x2]
gy2 = GY[y2, x2]
cos_angle = gx1 * gx2 + gy1 * gy2
cos_angle /= (np.linalg.norm((gx1, gy1)) * np.linalg.norm((gx2, gy2)))
angle = np.arccos(cos_angle)
if cos_angle < 0:
angle = np.pi - angle
x1, y1 = contour[((2*i + n) // 2) % N] # Get the middle point between i and (i + n)
heatmap[y1, x1] = angle # Use angle between gradient vectors as score
measure.append((angle, x1, y1, gx1, gy1))
_, x1, y1, gx1, gy1 = max(measure) # Most pointed point for each contour
# Possible to filter for those blobs with measure > val in heatmap instead.
pointed_points.append((x1, y1, gx1, gy1))
heatmap = cv2.GaussianBlur(heatmap, (3, 3), heatmap.max())
return heatmap, pointed_points
def plot_points(image, pointed_points, radius=5, color=(255, 0, 0)):
for (x1, y1, _, _) in pointed_points:
cv2.circle(image, (x1, y1), radius, color, -1)
def main():
I = cv2.imread("glLqt.jpg", 0)
heatmap, pointed_points = compute_pointness(I, n=5)
pl.figure()
pl.imshow(heatmap, cmap=pl.cm.jet)
pl.colorbar()
I_color = cv2.cvtColor(I, cv2.COLOR_GRAY2RGB)
plot_points(I_color, pointed_points)
pl.figure()
pl.imshow(I_color)
if __name__ == '__main__':
main()
Notice that sharper points are brighter in the heatmap.
The point is that " if you approximate the contour to continues lines you can see that the pointiness is the point where maximum angle deviation for consecutive line occurs", based on this you can develop your algorithm.
You need to do
Find contour.
Find approxPolyDP() for the contour.
Calculate angle for each consecutive line and store the point where the maximum deviation occur.
You can calculate the angle of a line using the equation
double Angle = atan2(P2.y - P1.y, P2.x - P1.x) * 180.0 / CV_PI;

Categories