I blame cqql for this existing.
I largely know nothing about this topic and everything you see here is knowledge I gathered myself over the course of an afternoon.
This is the post that threw me down this rabbit hole and started it
all.
After doing a bit of searching around, for ways to maybe solve this problem I stumbled on OpenCV, which also happens to have a python port. Perfect for quickly hacking something together.
First I imported the image as grayscale and let OpenCV find the
contours for me. The contours themselves are represented as the points
at the edges as seen here:
Which we can trace to form the complete outline as seen here:
Actually Implementing this was as simple as
def GetContoursFromFiles():
= []
cnt for i in range (0, MAX, 1):
= cv2.imread(str(i)+'.png',0)
img = cv2.threshold(img, GrayscaleThreshhold, 255,cv2.THRESH_BINARY_INV)
ret, thresh1 = cv2.findContours(thresh1,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
contours,hierarchy print("Number of Shapes detected in Image "+str(i)+":",len(contours))
0])
cnt.append(contours[return cnt
Next, I had OpenCV compare the different outlines with each other,
which I used to build a NxN Matrix of weights.
(I love when libraries already do everything for me, especially all the
math I wouldn’t even know where to start with learning it). Then I just
had to pretty print the matrix and that could be considered Task
Complete:
Now that I’ve already come this far, why not present the information
in a format, that’s easy to visually parse?
So that’s exactly what I did. I stitched together all the source images
to form two axis and used the weights I previously got to color in all
the squares. (This might sound simple, but this is actually the part,
that I needed to write the most code for, turns out transforming weights
into a nice looking color matrix isn’t as easy as it sounds.)
But the result was well worth the effort:
So, what actually took all the effort? This:
def CreateMatchingColorMatrix(mat):
= cv2.imread(str(0)+'.png',cv2.IMREAD_COLOR)
im_res = im_res.shape
height, width, channels # (Coordinate 0/0) Color (white)
= np.full((height, width, 3), 255, np.uint8)
im_temp
= matplotlib.colors.Normalize(vmin=0.0, vmax=NormUpperBound)
norm # Build Topmost row (just iterate through all images and concat them sequentially)
for i in range(0, MAX, 1):
= cv2.imread(str(i)+'.png',cv2.IMREAD_COLOR)
img = cv2.imread(str(i)+'.png',0)
imgg = cv2.threshold(imgg, GrayscaleThreshhold, 255,cv2.THRESH_BINARY_INV)
ret, thresh1 # find the contours in the binary image
= cv2.findContours(thresh1,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
contours,hierarchy = cv2.drawContours(img,contours[0] , -1, (255, 0,0), 3)
img = cv2.hconcat([im_temp, img])
im_temp # This top row is now our first row
= im_temp
im_res # Build The matrix row by row
for i in range(0, MAX, 1):
= cv2.imread(str(i)+'.png',cv2.IMREAD_COLOR)
im_temp = np.full((height, width, 3), 255, np.uint8)
img = (0, 0, 255)
img[:] # Individual row here, current sequential image gets chosen above, so here, we can do square coloring
for j in range(0, MAX, 1):
= matplotlib.cm.get_cmap('brg_r')
cmap 0.0,0.0,0.0))
cmap.set_over((# Gets current weight, normalises it, looks it up in color map, converts it to full scale, colors
= NtoF(cmap(norm(mat[i*MAX+j]))[:-1])
img[:] # build up row
= cv2.hconcat([im_temp, img])
im_temp #build up matrix
= cv2.vconcat([im_res, im_temp])
im_res DebugDrawImage(im_res)
That’s because I’m assembling the Matrix piece by piece and since I
was learning as I went, I incrementally built it in such a way, that one
part completely worked before moving on to the rest.
Specifically, I first tried to build the row and column headers, by
filling all the other spaces with a dummy image 0.png. Then I had to
figure out how to color them white. Afterwards, I tried to color code
them, at which point I pulled in matplotlib for the colormaps. But
because it uses different value ranged, I needed to do a bit of back and
forth conversion, which led to this fun line which does all the heavy
lifting for the coloring right here:
# Gets current weight, normalises it, looks it up in color map, converts it to full scale, colors
= NtoF(cmap(norm(mat[i*MAX+j]))[:-1]) img[:]
How does it handle rotated shapes? Was the next question thrown my way. Turns out, better and worse than expected at the same time.
I expected rotation to not perform too well, as you can’t perfectly rotate pixellated images. But it also turns out, OpenCV can handle them quite well and the biggest error actually comes from the way rotation is performed.
To Demonstrate, here we have 45° Rotations of the same object, performed with different Interpolation algorithms with their associated weights.
The second algorithm seemed to perform more consistently when using 45° rotations. Sadly, it completely failed with 22.5° rotations (which may be something I could fix, but I’ve already spent enough time on a “let’s just do 10-15 minutes of research” project.)
There’s more pictures here than text, which means fedi isn’t the best place to post this with the way it handles pictures. And I really wanted to share the pictures I generated.
I used the Frost Pattern Generator written by cqql for my sample
images, as they provide the perfect base for this. Yep, the same person
who got me started on this. It can be found here:
https://moonlit.technology/cqql/frost_patterns
Now, keep in mind, this was quickly hacked together while I was
learning how to even do this thing. With this disclaimer, the code can
be found here:
https://moonlit.technology/NixLynx/shape_matching
(Make sure to follow the license :3)