diff --git a/.gitignore b/.gitignore index 92afa22..c7e232c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__/ venv/ +stringArt/resources/string_pics/* diff --git a/stringArt/__main__.py b/stringArt/__main__.py index cba5eae..2548f34 100644 --- a/stringArt/__main__.py +++ b/stringArt/__main__.py @@ -1,34 +1,36 @@ - import cv2 as cv import time - +from helpers.edgeImage import EdgeImg +from helpers.pinsImg import PinsImg from helpers.pins_graph import PinsGraph -from helpers.image import Image +from helpers.stringImg import StringImg -from tests.test_graph import test_graph_neighbors +WINDOW_NAME = 'String Art' -def main(): - start_time = time.time() +IMAGE_NAME = 'odri.jpg' +IMAGE_PATH = 'stringArt/resources/pics/' + IMAGE_NAME - WINDOW_NAME = 'String Art' +STRING_IMAGE_NAME = '{}.jpg'.format(time.strftime("%m.%d_%H:%M")) +STRING_IMAGE_PATH = 'stringArt/resources/string_pics/' + STRING_IMAGE_NAME - IMAGE_NAME = 'odri.jpg' - IMAGE_PATH = 'stringArt/resources/pics/' + IMAGE_NAME - STRING_IMAGE_NAME = '{}.jpg'.format(time.strftime("%m.%d_%H:%M")) - STRING_IMAGE_PATHE = 'stringArt/resources/string_pics/' + STRING_IMAGE_NAME +def main(): + edge_img = EdgeImg(IMAGE_PATH) + edge_img.create_edge_img() - img = Image(IMAGE_PATH, WINDOW_NAME) - graph = PinsGraph(img) + pin_img = PinsImg(edge_img.edge_coords, edge_img.edge_img.shape) + pin_img.create_pin_img() - img.generate_string_img(graph) + graph = PinsGraph(pin_img.pin_arr, pin_img.distance) + graph.create_graph() - print('@@@@@@@ {}'.format(time.time()-start_time)) + string_img = StringImg(graph.nodes, edge_img.img) + string_img.generate_string_img() - cv.imwrite(STRING_IMAGE_PATHE, img.img_out) - img.show_img(img.img_out, 0) + cv.imwrite(STRING_IMAGE_PATH, string_img.img_out) + string_img.show_img('String Image',string_img.img_out, 0) diff --git a/stringArt/helpers/edgeImage.py b/stringArt/helpers/edgeImage.py new file mode 100644 index 0000000..fde33bc --- /dev/null +++ b/stringArt/helpers/edgeImage.py @@ -0,0 +1,34 @@ +import cv2 as cv +import numpy as np + +class EdgeImg: + + def __init__(self, image_path): + self.window_name = 'Edges Image' + self.img = cv.imread(image_path) + self.edge_img = None + self.edge_coords = None + + def create_edge_img(self): + min_threshold = 40 + max_threshold = 100 + trackbar_name = 'Threshold' + + cv.namedWindow(self.window_name) + cv.createTrackbar(trackbar_name, self.window_name, min_threshold, max_threshold, self.adjust_threshold) + + self.adjust_threshold(min_threshold) + cv.waitKey() + cv.destroyAllWindows() + self.edge_coords = np.argwhere(self.edge_img == 255) + + + def adjust_threshold(self, low_threshold): + ratio = 3 + kernel_size = 3 + + gray_img = cv.cvtColor(self.img, cv.COLOR_BGR2GRAY) + blur_img = cv.blur(gray_img, (3,3)) + self.edge_img = cv.Canny(blur_img, low_threshold, low_threshold*ratio, kernel_size) + + cv.imshow(self.window_name, self.edge_img) diff --git a/stringArt/helpers/image.py b/stringArt/helpers/image.py deleted file mode 100644 index 03bdd54..0000000 --- a/stringArt/helpers/image.py +++ /dev/null @@ -1,117 +0,0 @@ -import cv2 as cv -import numpy as np -from skimage.metrics import structural_similarity as ssim -import random - -import time - - -class Image: - - def __init__(self, IMAGE_PATH, WINDOW_NAME): - self.img = cv.imread(IMAGE_PATH) - self.WINDOW_NAME = WINDOW_NAME - self.img_height, self.img_width, _ = self.img.shape - self.img_out = np.zeros(self.img.shape, np.uint8) - self.edge_img = None - self.PIN_DISTANCE = None - self.pin_arr = [] - self.create_edge_img() - self.create_pin_img() - - def create_edge_img(self): - LOW_THRESHOLD = 40 - MAX_THRESHOLD = 100 - TRACKBAR_NAME = 'Threshold' - - cv.imshow(self.WINDOW_NAME, self.img) - cv.waitKey() - - cv.namedWindow(self.WINDOW_NAME) - cv.createTrackbar(TRACKBAR_NAME, self.WINDOW_NAME, LOW_THRESHOLD, MAX_THRESHOLD, self.adjust_threshold) - - self.adjust_threshold(LOW_THRESHOLD) - cv.waitKey() - cv.destroyAllWindows() - - - def adjust_threshold(self, low_threshold): - RATIO = 3 - KERNEL_SIZE = 3 - - gray_img = cv.cvtColor(self.img, cv.COLOR_BGR2GRAY) - blur_img = cv.blur(gray_img, (3,3)) - self.edge_img = cv.Canny(blur_img, low_threshold, low_threshold*RATIO, KERNEL_SIZE) - - cv.imshow(self.WINDOW_NAME, self.edge_img) - - - def create_pin_img(self): - TRACKBAR_NAME = 'PIN_DISTANCE' - - cv.namedWindow(self.WINDOW_NAME) - cv.createTrackbar(TRACKBAR_NAME, self.WINDOW_NAME, 15, 50, self.adjust_pin_distance) - - self.adjust_pin_distance(15) - cv.waitKey() - cv.destroyAllWindows() - - def adjust_pin_distance(self, distance): - self.PIN_DISTANCE = distance - pin_arr = [] - pin_pic = np.zeros(self.edge_img.shape, np.uint8) - for x in [i for i in range(self.img_width+1) if i % distance==0]: - for y in [i for i in range(self.img_height+1) if i % distance==0]: - coords = (x,y) - mask = np.zeros(self.edge_img.shape, np.uint8) - cv.circle(mask, coords, radius=distance//2, color=255, thickness=-1) - for circle_coords in np.argwhere(mask==255): - if np.any(self.edge_img[circle_coords[0], circle_coords[1]] !=0): - coords = (circle_coords[1], circle_coords[0]) - break - pin_arr.append(coords) - pin_pic = cv.circle(pin_pic, coords, radius=0, color=(255, 255, 255), thickness=-1) - self.pin_arr = pin_arr - cv.imshow(self.WINDOW_NAME, pin_pic) - - def generate_string_img(self, pin_graph): - current_pin = random.choice(pin_graph.nodes) - best_score = -1 - prev_score = -1 - - counter = 1 - alter_counter = 1 - - for _ in range(2000): - best_neighbor = None - - for neighbor in list(current_pin.neighbors.values()): #Is this really the nicest way to delete while iterating? I wonder - mask = self.img_out.copy() - cv.line(mask, current_pin.coords, neighbor.coords, color=(255,255,255), thickness=1) - (score, diff) = ssim(self.img, mask, full=True, multichannel=True) - if score > best_score : - best_score = score - best_neighbor = neighbor - elif score < prev_score: - print('Delete: {} {}'.format(current_pin.coords, neighbor.coords)) - del current_pin.neighbors[neighbor.id] - del neighbor.neighbors[current_pin.id] - if not best_neighbor: - print('@@@@@@@@@@@Alter root') - alter_counter += 1 - best_neighbor = random.choice(list(current_pin.strings.values())) - else: - current_pin.add_string(best_neighbor) - print('Current pin: {} Best neighbor: {} counter: {}'.format(current_pin.coords, best_neighbor.coords, counter)) - counter +=1 - cv.line(self.img_out, current_pin.coords, best_neighbor.coords, color=(255,255,255), thickness=1) - self.show_img(self.img_out, 5) - current_pin = best_neighbor - prev_score = best_score - print('Number of alter roots: {}'.format(alter_counter)) - - - def show_img(self, img, time=0): - - cv.imshow(self.WINDOW_NAME, img) - cv.waitKey(time) diff --git a/stringArt/helpers/pinsImg.py b/stringArt/helpers/pinsImg.py new file mode 100644 index 0000000..8256a0b --- /dev/null +++ b/stringArt/helpers/pinsImg.py @@ -0,0 +1,34 @@ +import cv2 as cv +import numpy as np +import time + +class PinsImg: + def __init__(self, edge_coords, shape): + self.edge_coords = edge_coords + self.shape = shape + self.window_name = 'Pins locations' + self.pin_arr = None + + + def create_pin_img(self): + trackbar_name = 'Pins distance' + + cv.namedWindow(self.window_name) + cv.createTrackbar(trackbar_name, self.window_name, 15, 50, self.adjust_pin_distance) + + self.adjust_pin_distance(15) + cv.waitKey() + cv.destroyAllWindows() + + def adjust_pin_distance(self, distance): + start_time = time.time() + self.distance = distance + self.pin_arr = self.edge_coords[0::distance] + for x in range(0, self.shape[0]+1, distance): + for y in range(0, self.shape[1]+1, distance): + self.pin_arr = np.append(self.pin_arr, [[x,y]], axis=0) + pin_pic = np.zeros(self.shape, np.uint8) + for coords in self.pin_arr: + cv.circle(pin_pic, (coords[1], coords[0]), radius=0, color=(255, 255, 255), thickness=-1) + print('adjust pin time: {}'.format(time.time() - start_time)) + cv.imshow(self.window_name, pin_pic) diff --git a/stringArt/helpers/pins_graph.py b/stringArt/helpers/pins_graph.py index 3e8911b..7f28c41 100644 --- a/stringArt/helpers/pins_graph.py +++ b/stringArt/helpers/pins_graph.py @@ -1,26 +1,31 @@ import math +import time class PinsGraph: - def __init__(self, image): + def __init__(self, pin_arr, distance): self.nodes = [] - self.create_graph(image) + self.pin_arr = pin_arr + self.distance = distance - def create_graph(self, image): - for index, pin in enumerate(image.pin_arr): + def create_graph(self): + start_time = time.time() + for index, pin in enumerate(self.pin_arr): self.nodes.append(PinNode(pin, index)) - for i in range(len(self.nodes)-1): - pin = self.nodes[i] - for next_pin in self.nodes[i+1:]: - if pin.is_neighbor(next_pin, image.PIN_DISTANCE): + for index, pin in enumerate(self.nodes): + for next_pin in self.nodes[index+1:]: + # i like the naming of these methods, it's very clear what's going on + # I will keep this comment here for good luck + if pin.is_neighbor(next_pin, self.distance): pin.add_neighbor(next_pin) + print('Create graph time: {}'.format(time.time()-start_time)) class PinNode: def __init__(self, coords, id): + self.coords = (coords[1],coords[0]) self.id = id - self.coords = coords self.neighbors = {} self.strings = {} @@ -33,9 +38,10 @@ def add_string(self, neighbor_node): self.strings[neighbor_node.id] = neighbor_node del neighbor_node.neighbors[self.id] neighbor_node.strings[self.id] = self - - def is_neighbor(self, next_pin, PIN_DISTANCE): - max_distance = PIN_DISTANCE * 3 - distance = math.sqrt((self.coords[0] - next_pin.coords[0])**2 + (self.coords[1] - next_pin.coords[1])**2) + + def is_neighbor(self, next_pin, pin_distance): + magic_number = 3 + max_distance = (pin_distance * magic_number) ** 2 # ;) + distance = (self.coords[0] - next_pin.coords[0])**2 + (self.coords[1] - next_pin.coords[1])**2 return distance <= max_distance diff --git a/stringArt/helpers/stringImg.py b/stringArt/helpers/stringImg.py new file mode 100644 index 0000000..732f7b4 --- /dev/null +++ b/stringArt/helpers/stringImg.py @@ -0,0 +1,57 @@ +import cv2 as cv +import numpy as np +import random +from skimage.metrics import structural_similarity as ssim +import time + +class StringImg: + + def __init__(self, pin_nodes, img): + self.pin_nodes = pin_nodes + self.img = img + self.img_out = np.zeros(img.shape, np.uint8) + + def generate_string_img(self): ########Working on it + start_time = time.time() + current_pin = random.choice(self.pin_nodes) + best_score = -1 + prev_score = -1 + + counter = 1 + alter_counter = 1 + + for _ in range(2000): + best_neighbor = None + + for neighbor in list(current_pin.neighbors.values()): #Is this really the nicest way to delete while iterating? I wonder + mask = self.img_out.copy() + cv.line(mask, current_pin.coords, neighbor.coords, color=(255,255,255), thickness=1) + # this is probably what makes the whole thing slow... i think it should be possible to + # make it faster by only running it on a single channel (since this is all grayscale) and + # not running with full=True (the result of which is discarded anyway)... + score = ssim(self.img, mask, multichannel=True) + if score > best_score : + best_score = score + best_neighbor = neighbor + elif score < prev_score: + print('Delete: {} {}'.format(current_pin.coords, neighbor.coords)) + del current_pin.neighbors[neighbor.id] + del neighbor.neighbors[current_pin.id] + if not best_neighbor: + print('@@@@@@@@@@@Alter root') + alter_counter += 1 + best_neighbor = random.choice(list(current_pin.strings.values())) + else: + current_pin.add_string(best_neighbor) + print('Current pin: {} Best neighbor: {} counter: {}'.format(current_pin.coords, best_neighbor.coords, counter)) + counter +=1 + cv.line(self.img_out, current_pin.coords, best_neighbor.coords, color=(255,255,255), thickness=1) + self.show_img('String Image', self.img_out, 5) + current_pin = best_neighbor + prev_score = best_score + print('Number of alter roots: {}'.format(alter_counter)) + print('String time: {}'.format(time.time()-start_time)) + + def show_img(self, window_name, img, time=0): + cv.imshow(window_name, img) + cv.waitKey(time) diff --git a/stringArt/resources/string_pics/img_out2.jpg b/stringArt/resources/string_pics/img_out2.jpg new file mode 100644 index 0000000..7c382b5 Binary files /dev/null and b/stringArt/resources/string_pics/img_out2.jpg differ diff --git a/stringArt/resources/string_pics/out.jpeg b/stringArt/resources/string_pics/out.jpeg new file mode 100644 index 0000000..1d2bab1 Binary files /dev/null and b/stringArt/resources/string_pics/out.jpeg differ diff --git a/stringArt/tests/test_graph.py b/stringArt/tests/test_graph.py deleted file mode 100644 index 8ec0e07..0000000 --- a/stringArt/tests/test_graph.py +++ /dev/null @@ -1,23 +0,0 @@ -import numpy as np -import cv2 as cv - -def naive_find_neighbors(pin, pin_pic, PIN_DISTANCE): - neighbors = [] - mask = np.zeros(pin_pic.shape, np.uint8) - cv.circle(mask, pin, radius=PIN_DISTANCE*3, color=255, thickness=-1) - for circle_coords in np.argwhere(mask==255): - if np.any(pin_pic[circle_coords[0], circle_coords[1]] !=0) and (circle_coords[1] != pin[0] or circle_coords[0] != pin[1]): - neighbors.append((circle_coords[1], circle_coords[0])) - return neighbors - -def test_graph_neighbors(graph, image): - pin_img = image.create_pin_img() - for node in graph.nodes: - naive_neighbors = np.array(sorted(naive_find_neighbors(node.coords, pin_img, image.PIN_DISTANCE))) - graph_neighbors = np.array(sorted([neighbor.coords for neighbor in node.neighbors])) - print('pin: {}'.format(node.coords)) - compare = (naive_neighbors == graph_neighbors).all() - if(compare == False): - print('Naive neighbors: {}'.format(naive_neighbors)) - print('Graph neighbors: {}'.format(graph_neighbors)) - break