commit 238f967a0a9b3087e3e0175f192a86fb61d06c6b Author: Lynx Date: Sun Aug 11 16:54:51 2024 +0200 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bd52bb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/localvenv \ No newline at end of file diff --git a/0.png b/0.png new file mode 100644 index 0000000..e7a207d Binary files /dev/null and b/0.png differ diff --git a/1.png b/1.png new file mode 100644 index 0000000..0fdc3f7 Binary files /dev/null and b/1.png differ diff --git a/2.png b/2.png new file mode 100644 index 0000000..22a7da0 Binary files /dev/null and b/2.png differ diff --git a/3.png b/3.png new file mode 100644 index 0000000..a8b3db4 Binary files /dev/null and b/3.png differ diff --git a/4.png b/4.png new file mode 100644 index 0000000..a9c0b4b Binary files /dev/null and b/4.png differ diff --git a/5.png b/5.png new file mode 100644 index 0000000..fad2d6c Binary files /dev/null and b/5.png differ diff --git a/6.png b/6.png new file mode 100644 index 0000000..6a9574d Binary files /dev/null and b/6.png differ diff --git a/7.png b/7.png new file mode 100644 index 0000000..ba70d60 Binary files /dev/null and b/7.png differ diff --git a/8.png b/8.png new file mode 100644 index 0000000..10a7f0f Binary files /dev/null and b/8.png differ diff --git a/9.png b/9.png new file mode 100644 index 0000000..88500da Binary files /dev/null and b/9.png differ diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..e8c5121 --- /dev/null +++ b/License.txt @@ -0,0 +1,19 @@ +modified silly license + ------------- + + copyright (c) :3 + + permission is granted to the entity reading this text ("that one") + to use this project for silly and mischievous purposes, + subject to the following conditions: + + - say "be gay, do crime" at least once during use of this project + + - if that one represents a for-profit endeavour, all previously granted + permissions are immediately and permanently retracted + + - there's no warranty of any kind + that one should expect the project to crash and burn in the worst possible ways + no further warnings will be issued + + - no attribution required \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..081d1de --- /dev/null +++ b/Readme.md @@ -0,0 +1,33 @@ +# Purpose + +- It was fun to mess around with :3 +- Making this public so cqql can also mess around with it + +# Dependencies +- opencv-python + - all of the image stuff +- matplotlib + - color lookup table +- (numpy) + - used internally by opencv, but part of stdlib + +# Running +- python -m venv localvenv +- ./localvenv/bin/pip install opencv-python +- ./localvenv/bin/pip install matplotlib +- ./localvenv/bin/python match.py + + +# Modifications +- If you have more/less images, change MAX global +- If you want to test rotational stuff, uncomment the line in main +- It is assumed that the shape is a darker shape on a ligher background + - The image gets converted into grayscale and assumes + - the shape has an intensity below 200 + - the background has an intensity above 200 +- Depending on how dissimilar the shapes are, NormUpperBound should be increased + +# Attributions + +- The test images were generated using + - https://moonlit.technology/cqql/frost_patterns diff --git a/match.py b/match.py new file mode 100644 index 0000000..e234bf8 --- /dev/null +++ b/match.py @@ -0,0 +1,168 @@ +# import required libraries +import cv2 +import numpy as np +import matplotlib +import math + +MAX = 10 # should currently be 10 +NormUpperBound = 2.0 #Highest Expected Number +GrayscaleThreshhold = 200 +RotationAngle = 45 + + +### Read Images from Disk, Assumed Naming: 0.png 1.png ... n.png depending on MAX + +#Turns Images into Contours based on Greyscale Threshhold +def GetContoursFromFiles(): + cnt = [] + for i in range (0, MAX, 1): + # Read image as grayscale images + img = cv2.imread(str(i)+'.png',0) + # Apply thresholding on the images to convert to binary images + ret, thresh1 = cv2.threshold(img, GrayscaleThreshhold, 255,cv2.THRESH_BINARY_INV) + # find the contours in the binary image + contours,hierarchy = cv2.findContours(thresh1,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + print("Number of Shapes detected in Image "+str(i)+":",len(contours)) + cnt.append(contours[0]) + return cnt + + +#Does N to N matching, resulting in a 1d array that should be interpreted as a 2d array +# Could easily be modified for 1 to N matching +def MatchAll(cnt): + mat = [] + for i in range(0, MAX, 1): + for j in range(0, MAX, 1): + mat.append(cv2.matchShapes(cnt[i],cnt[j],1,0.0)) + return mat + + +def PrintMatchingMatrix(mat): + print("Similarity of Images: \n") + for i in range(0, MAX, 1): + for j in range(0, MAX, 1): + print(f"{mat[i*MAX+j]:.2f}", end='') + print("\t", end='') + print("\n") + +def PrintMatchingMatrixNormalized(mat): + print("Similarity of Images Normalized: \n") + norm = matplotlib.colors.Normalize(vmin=0.0, vmax=NormUpperBound) + for i in range(0, MAX, 1): + for j in range(0, MAX, 1): + print(f"{norm(mat[i*MAX+j]):.2f}", end='') + print("\t", end='') + print("\n") + + + +# Does all of the heavy lifting when it comes to displaying it in a nice graphical way +# Compact but also quickly hacked together +# Builds the Visual matrix using images, 1 image = 1 coordinate unit +def CreateMatchingColorMatrix(mat): + + im_res = cv2.imread(str(0)+'.png',cv2.IMREAD_COLOR) + height, width, channels = im_res.shape + # (Coordinate 0/0) Color (white) + im_temp = np.full((height, width, 3), 255, np.uint8) + + norm = matplotlib.colors.Normalize(vmin=0.0, vmax=NormUpperBound) + + # Build Topmost row (just iterate through all images and concat them sequentially) + for i in range(0, MAX, 1): + img = cv2.imread(str(i)+'.png',cv2.IMREAD_COLOR) + im_temp = cv2.hconcat([im_temp, img]) + # This top row is now our first row + im_res = im_temp + # Build The matrix row by row + for i in range(0, MAX, 1): + im_temp = cv2.imread(str(i)+'.png',cv2.IMREAD_COLOR) + img = np.full((height, width, 3), 255, np.uint8) + img[:] = (0, 0, 255) + # Individual row here, current sequential image gets chosen above, so here, we can do square coloring + for j in range(0, MAX, 1): + cmap = matplotlib.cm.get_cmap('brg_r') + cmap.set_over((0.0,0.0,0.0)) + # Gets current weight, normalises it, looks it up in color map, converts it to full scale, colors + img[:] = NtoF(cmap(norm(mat[i*MAX+j]))[:-1]) + # build up row + im_temp = cv2.hconcat([im_temp, img]) + #build up matrix + im_res = cv2.vconcat([im_res, im_temp]) + DebugDrawImage(im_res) + + +# Helper to convert Normalized color to Full-scale (255) color +def NtoF(rgb): + return tuple([255*x for x in rgb]) + + + + +def DebugDrawImage(img): + cv2.imshow("Image", img) + cv2.waitKey(0) + +### Rotates Image 0.png 4 times to test how rotations affect outcome (they don't) + +def RotationTest(): + global MAX + MAX = math.ceil(360/RotationAngle) + + cnt = [] + img = cv2.imread(str(0)+'.png',0) + for i in range (0, MAX, 1): + ret, thresh1 = cv2.threshold(img, GrayscaleThreshhold, 255,cv2.THRESH_BINARY_INV) + contours,hierarchy = cv2.findContours(thresh1,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + cnt.append(contours[0]) + print("Number of Shapes detected in Rotation "+str(i)+":",len(contours)) + img = rotate_image(img, RotationAngle) + + mat = MatchAll(cnt) + PrintMatchingMatrix(mat) + CreateMatchingColorMatrixRotation(mat) + +# Quick hack of above function to work with rotation instead of multiple images +def CreateMatchingColorMatrixRotation(mat): + im_rot = cv2.imread(str(0)+'.png',cv2.IMREAD_COLOR) + im_res = cv2.imread(str(0)+'.png',cv2.IMREAD_COLOR) + height, width, channels = im_res.shape + im_temp = np.full((height, width, 3), 255, np.uint8) + norm = matplotlib.colors.Normalize(vmin=0.0, vmax=NormUpperBound) + for i in range(0, MAX, 1): + img = im_rot + im_temp = cv2.hconcat([im_temp, img]) + im_rot = rotate_image(img, RotationAngle) + im_res = im_temp + #reset + im_rot = cv2.imread(str(0)+'.png',cv2.IMREAD_COLOR) + for i in range(0, MAX, 1): + im_temp = cv2.imread(str(0)+'.png',cv2.IMREAD_COLOR) + im_temp = im_rot + img = np.full((height, width, 3), 255, np.uint8) + img[:] = (0, 0, 255) + for j in range(0, MAX, 1): + cmap = matplotlib.cm.get_cmap('brg_r') + cmap.set_over((0.0,0.0,0.0)) + img[:] = NtoF(cmap(norm(mat[i*MAX+j]))[:-1]) + im_temp = cv2.hconcat([im_temp, img]) + im_res = cv2.vconcat([im_res, im_temp]) + im_rot = rotate_image(im_rot, RotationAngle) + DebugDrawImage(im_res) + +# Stolen from Stackoverflow and modified: https://stackoverflow.com/a/9042907 +# NOTE: this function assumes the background is a lighter shade than the form to detect +# NOTE: INTER_NEAREST or other interpolation strategies might work better, depending on rotation and some other factors +def rotate_image(image, angle): + image_center = tuple(np.array(image.shape[1::-1]) / 2) + rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0) + result = cv2.warpAffine(image, rot_mat, image.shape[1::-1], flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=(255,255,255)) + return result + + +if __name__ == "__main__": + mat = MatchAll(GetContoursFromFiles()) + PrintMatchingMatrix(mat) + PrintMatchingMatrixNormalized(mat) + CreateMatchingColorMatrix(mat) + #RotationTest() \ No newline at end of file