├── requirements.txt ├── images ├── 250.bmp ├── urdead.png └── image pepega.png ├── TSP_Solver ├── __init__.py ├── disjoint_sets.py ├── Graph_TSP.py ├── bounds.py └── algorithms.py ├── .gitignore ├── config2.ini ├── config.ini ├── config_animation.ini ├── image_to_anchors.py ├── basic_converter.py ├── README.md ├── images_to_animation ├── converter.py ├── smart_converter.py └── fill_converter.py /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pillow~=8.2.0 3 | scipy 4 | networkx~=2.5.1 5 | opencv-python -------------------------------------------------------------------------------- /images/250.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliBomby/Image-to-Anchors/HEAD/images/250.bmp -------------------------------------------------------------------------------- /images/urdead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliBomby/Image-to-Anchors/HEAD/images/urdead.png -------------------------------------------------------------------------------- /images/image pepega.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OliBomby/Image-to-Anchors/HEAD/images/image pepega.png -------------------------------------------------------------------------------- /TSP_Solver/__init__.py: -------------------------------------------------------------------------------- 1 | from .algorithms import * 2 | from .bounds import * 3 | from .Graph_TSP import * 4 | from .disjoint_sets import * 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Virtual env files 2 | venv 3 | venv/* 4 | 5 | # Project files 6 | .idea 7 | .idea/* 8 | 9 | # Test images 10 | images/* 11 | !images/urdead.png 12 | !images/image pepega.png 13 | !images/250.bmp 14 | 15 | # Output code 16 | output.txt 17 | 18 | __pycache__/ -------------------------------------------------------------------------------- /config2.ini: -------------------------------------------------------------------------------- 1 | [SETTINGS] 2 | PIXEL_SPACING = 3 3 | ROTATE = False 4 | LAYER_2_OFFSET = True 5 | BRIGHT_BG = False 6 | E_MODE = False 7 | VERBOSE = True 8 | 9 | SLIDER_MAX_WIDTH = 750 10 | SLIDER_MAX_HEIGHT = 388 11 | 12 | [LAYER 1] 13 | 0: R_R_R 14 | 50: R_R 15 | 100: R 16 | 140: R 17 | 190: W 18 | 220: W 19 | 20 | [LAYER 2] 21 | 0: 22 | 50: 23 | 100: 24 | 140: 25 | 190: 26 | 220: -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [SETTINGS] 2 | PIXEL_SPACING = 7 3 | ROTATE = False 4 | LAYER_2_OFFSET = True 5 | BRIGHT_BG = True 6 | E_MODE = False 7 | VERBOSE = True 8 | 9 | SLIDER_MAX_WIDTH = 750 10 | SLIDER_MAX_HEIGHT = 388 11 | 12 | [LAYER 1] 13 | 0: RR_RR_RR_RR_RR_RR_RR 14 | 32: RR_R_RR_R_RR_R_RR 15 | 38: R_R_R_R_R_R_R 16 | 46: R_R_R__R_R_R 17 | 52: R_R__R_R__R 18 | 64: R__R__R__R 19 | 85: R 20 | 127: R 21 | 170: 22 | 222: 23 | 255: 24 | 25 | [LAYER 2] 26 | 0: 27 | 42: 28 | 85: 29 | 127: W 30 | 170: W 31 | 222: 32 | 255: -------------------------------------------------------------------------------- /config_animation.ini: -------------------------------------------------------------------------------- 1 | [SETTINGS] 2 | PIXEL_SPACING = 7 3 | ROTATE = False 4 | LAYER_2_OFFSET = True 5 | BRIGHT_BG = False 6 | E_MODE = False 7 | VERBOSE = False 8 | SCALE = 1.0666666666666666666666 9 | 10 | SLIDER_MAX_WIDTH = 512 11 | SLIDER_MAX_HEIGHT = 384 12 | 13 | [LAYER 1] 14 | 0: RR_RR_RR_RR_RR_RR_RR 15 | 32: RR_R_RR_R_RR_R_RR 16 | 38: R_R_R_R_R_R_R 17 | 46: R_R_R__R_R_R 18 | 52: R_R__R_R__R 19 | 64: R__R__R__R 20 | 85: R 21 | 127: R 22 | 170: 23 | 222: 24 | 255: 25 | 26 | [LAYER 2] 27 | 0: 28 | 42: 29 | 85: 30 | 127: W 31 | 170: W 32 | 222: 33 | 255: -------------------------------------------------------------------------------- /TSP_Solver/disjoint_sets.py: -------------------------------------------------------------------------------- 1 | class disjoint_set: 2 | def __init__(self,vertex): 3 | self.parent = self 4 | self.rank = 0 5 | self.vertex = vertex 6 | def find(self): 7 | if self.parent != self: 8 | self.parent = self.parent.find() 9 | return self.parent 10 | def joinSets(self,otherTree): 11 | root = self.find() 12 | otherTreeRoot = otherTree.find() 13 | if root == otherTreeRoot: 14 | return 15 | if root.rank < otherTreeRoot.rank: 16 | root.parent = otherTreeRoot 17 | elif otherTreeRoot.rank < root.rank: 18 | otherTreeRoot.parent = root 19 | else: 20 | otherTreeRoot.parent = root 21 | root.rank += 1 22 | 23 | -------------------------------------------------------------------------------- /image_to_anchors.py: -------------------------------------------------------------------------------- 1 | from basic_converter import BasicConverter 2 | from smart_converter import SmartConverter 3 | from fill_converter import FillConverter 4 | import configparser 5 | 6 | 7 | def main(): 8 | image_path = "images\\250.bmp" 9 | 10 | config = configparser.ConfigParser() 11 | config.read('config_animation.ini') 12 | 13 | converter = FillConverter(config) 14 | slidercode = converter.convert(image_path, 0) 15 | 16 | with open("output.txt", "w+") as f: 17 | f.write(slidercode) 18 | 19 | print("Done!") 20 | # input("Press enter to continue...") 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /TSP_Solver/Graph_TSP.py: -------------------------------------------------------------------------------- 1 | from .bounds import Bounds 2 | from .algorithms import Algorithms 3 | 4 | 5 | class Graph_TSP: 6 | # Nodes should be a dictionary of key value pairing : node num to xy coordinates 7 | # Edges are implied in the adjacency matrix 8 | # Adjacency matrix will be n x n; where n is the number of nodes 9 | def __init__(self, nodeDict, adjMatrix, instanceName, solution): 10 | self.nodeDict = nodeDict 11 | self.adjMatrix = adjMatrix 12 | self.counts = len(nodeDict) 13 | self.edgeDict = {} 14 | self.instanceName = instanceName 15 | self.solution = solution 16 | for i in range(self.counts): 17 | if self.counts > 1: 18 | for j in range(i + 1, self.counts): 19 | vertices = (i, j) 20 | self.edgeDict[vertices] = self.adjMatrix[i, j] 21 | self.Bounds = Bounds(self.nodeDict, self.adjMatrix) 22 | self.solutions = Algorithms(self.nodeDict, self.adjMatrix, self.counts, self.edgeDict) 23 | 24 | def HKLowerBoundCost(self): 25 | return self.Bounds.calculateHKLB() 26 | 27 | def oneTreeBound(self): 28 | return self.Bounds.calculateOTB(self.adjMatrix)[0] 29 | 30 | def upperBound(self): 31 | return self.Bounds.calculateMSTUpperBound() 32 | 33 | def randomSolution(self): 34 | return self.solutions.random() 35 | 36 | def nearestNeighbor(self): 37 | return self.solutions.nn() 38 | 39 | def greedy(self): 40 | return self.solutions.g() 41 | 42 | def convexhullInsert(self): 43 | return self.solutions.convHull() 44 | 45 | def christofides(self): 46 | return self.solutions.cf()[3] 47 | 48 | def cost(self, path): 49 | counter = 0 50 | for edge in path: 51 | checkEdge = edge 52 | if (checkEdge not in self.edgeDict): 53 | checkEdge = (edge[1], edge[0]) 54 | counter += self.edgeDict[checkEdge] 55 | return counter 56 | -------------------------------------------------------------------------------- /basic_converter.py: -------------------------------------------------------------------------------- 1 | from converter import * 2 | 3 | 4 | # Basic image converter for multi-colour images 5 | class BasicConverter(Converter): 6 | def __init__(self, config_file): 7 | super().__init__(config_file) 8 | 9 | def add_layer(self, anchors, layer, reverse): 10 | y_range = range(self.shape[0], 0, -1) if reverse else range(self.shape[0]) 11 | 12 | for y in y_range: 13 | line_first_anchors_on_pixel = [] 14 | for x in range(self.shape[1]): 15 | # If not in E mode, every other line reverse the scanning direction to create a zigzag pattern 16 | new_x = x 17 | direction = np.array([1, 0]) 18 | if not self.E_MODE and y % 2 == 1: 19 | new_x = self.shape[1] - x - 1 20 | direction = np.array([-1, 0]) 21 | 22 | osucoord = np.array([new_x, y]) * self.PIXEL_SPACING 23 | 24 | # Handle offset and rotate 25 | if layer == 2 and self.LAYER_2_OFFSET: 26 | osucoord += np.array([4, 4]) 27 | if self.ROTATE: 28 | osucoord -= np.array([1, 1]) 29 | 30 | pixel = self.osu_to_pixel(osucoord) 31 | 32 | next_pixel = self.osu_to_pixel(osucoord + direction * self.PIXEL_SPACING) 33 | next_colour = self.get_anchor_code(next_pixel, layer) 34 | next_white = ((next_pixel[1] < 128 or len(next_colour) == 0) and self.BRIGHT_BG) or next_colour == 'W' 35 | 36 | # Omit transparent pixels 37 | if pixel[1] < 128: 38 | continue 39 | 40 | # Find the corresponding anchor code for this luminosity level 41 | code = self.get_anchor_code(pixel, layer) 42 | 43 | if len(code) == 0: 44 | continue 45 | 46 | # Add anchors for this pixel based on the code 47 | anchors_on_pixel = get_anchors_from_code(code, osucoord, direction, next_white) 48 | 49 | if len(line_first_anchors_on_pixel) == 0 and len(anchors_on_pixel) > 0: 50 | line_first_anchors_on_pixel = anchors_on_pixel 51 | 52 | anchors += anchors_on_pixel 53 | 54 | # If in E mode, move back to the start of the line by repeating the line's first anchors 55 | if self.E_MODE: 56 | anchors += reversed(line_first_anchors_on_pixel) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image to Anchors 2 | Tool for converting images to slider anchors for view in the osu! editor. 3 | 4 | ## Usage 5 | To use, install the libraries from requirements.txt, open image_to_anchors.py, input the path to your image and run it. 6 | The .osu code of the slider will be in output.txt 7 | 8 | For animations use images_to_animation.py. Input the path to the folder with all the images and give it the 9 | start time and frame duration in milliseconds for timing the frames. Multiple slidercodes will be generated in output.txt 10 | 11 | ## Config 12 | You can tweak how the conversion works by editing the config files or reading a different config file. 13 | 14 | The file config.ini has settings for (what I think) the best quality image in the editor and 15 | it's meant to be viewed at 800x504 custom resolution (which you can set by manually editing your osu! config file). 16 | 17 | The file config2.ini has settings for a decent result which can be viewed at 1080p and most other resolutions. 18 | 19 | ### Explanation of all config settings 20 | - PIXEL_SPACING: The distance between every anchor in osu! pixels. 21 | - ROTATE: To adjust some offsets and rotate the bounding box, so the result looks good after rotating it 90 degrees. 22 | - LAYER_2_OFFSET: To offset the second layer by 4 osu! pixels which results in a seamingly higher resolution in the result. 23 | - BRIGHT_BG: To make the result look better on bright backgrounds by adding some white anchors to hide red anchors outside the bounds of the image. 24 | - E_MODE: To generate the anchors in an E pattern instead of the usual back-and-forth lines. Might look better on some edges. 25 | - VERBOSE: To print extra debug information. 26 | - SLIDER_MAX_WIDTH: Maximum width in osu! pixels for the result. 27 | - SLIDER_MAX_HEIGHT: Maximum height in osu! pixels for the result. 28 | 29 | ### Colour programming 30 | There are two anchor layers and for both layers you can define entirely in 31 | the config file how to generate anchors for each luminosity level. 32 | 33 | The luminosity ranges from 0 to 255. 34 | You configure the anchors by adding key-value pairs in the [Layer 1] or [Layer 2] categories. 35 | The key is the luminosity level and the value is a string of 'R' 'W' and '\_' which defines a pattern of anchors. 36 | 'R' adds a red anchor, 'W' adds a white anchor, and '\_' moves one osu! pixel further. 37 | 38 | For example "0: RR_RR_RR_RR_RR_RR_RR" generates 7 double stacked red anchors 1 pixel apart for any luminosity from 0 to the next. 39 | -------------------------------------------------------------------------------- /images_to_animation: -------------------------------------------------------------------------------- 1 | from smart_converter import SmartConverter 2 | from fill_converter import FillConverter 3 | import configparser 4 | import os 5 | import numpy as np 6 | from os import path 7 | import multiprocessing as mp 8 | 9 | results = [] 10 | 11 | 12 | def process_file(converter, dirname, filename, time): 13 | print("Processing file: %s" % filename) 14 | image_path = path.join(dirname, filename) 15 | slidercode = converter.convert(image_path, time, np.array((0, 0))) 16 | return time, slidercode 17 | 18 | 19 | def collect_code(result): 20 | global results 21 | if result[1] is not None: 22 | results.append(result) 23 | 24 | 25 | def main(): 26 | dir_name = "images/bad_apple" 27 | t_start = -66 28 | t_step = 40 * 1.5 # DT 29 | 30 | config = configparser.ConfigParser() 31 | config.read('config_animation.ini') 32 | 33 | # Get all the frames from a directory 34 | filelist = os.listdir(dir_name) 35 | for fichier in filelist[:]: # filelist[:] makes a copy of filelist. 36 | if not (fichier.endswith(".bmp") or fichier.endswith(".png") or fichier.endswith(".jpg")): 37 | filelist.remove(fichier) 38 | 39 | filelist.sort(key=lambda f: int(''.join(filter(str.isdigit, f)))) 40 | 41 | # Get a thread pool for the multi-processing 42 | pool = mp.Pool(mp.cpu_count()) 43 | 44 | # Use loop to parallelize 45 | for i, fn in enumerate(filelist): 46 | t = t_start + i * t_step 47 | # Animate in 2's 48 | if i % 2 != 0: 49 | continue 50 | pool.apply_async(process_file, args=(FillConverter(config), dir_name, fn, t), callback=collect_code) 51 | 52 | pool.close() 53 | pool.join() # postpones the execution of next line of code until all processes in the queue are done. 54 | 55 | # Sort results 56 | results.sort(key=lambda x: x[0]) 57 | codes = [r for i, r in results] 58 | 59 | # Single processor code 60 | # last_time = t_start 61 | # converter = SmartConverter2(config) 62 | # for fn in filelist: 63 | # print("Processing file: %s" % fn) 64 | # image_path = path.join(dir_name, fn) 65 | # 66 | # slidercode = converter.convert(image_path, last_time, np.array((0, 0))) 67 | # last_time += t_step 68 | # if slidercode is not None: 69 | # codes.append(slidercode) 70 | 71 | with open("output.txt", "w+") as f: 72 | f.writelines(codes) 73 | 74 | print("Done!") 75 | # input("Press enter to continue...") 76 | 77 | if __name__ == "__main__": 78 | main() 79 | 80 | -------------------------------------------------------------------------------- /converter.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import numpy as np 3 | 4 | 5 | def get_anchors_from_code(code, osucoord, direction, next_white): 6 | anchors_on_pixel = [] 7 | for i in range(len(code)): 8 | c = code[i] 9 | if c == 'R': 10 | if '_' not in code[i:] and '_' in code and next_white: 11 | anchors_on_pixel.append(osucoord.copy()) 12 | break 13 | 14 | anchors_on_pixel.append(osucoord.copy()) 15 | anchors_on_pixel.append(osucoord.copy()) 16 | elif c == 'W': 17 | anchors_on_pixel.append(osucoord.copy()) 18 | elif c == '_': 19 | osucoord += direction 20 | return anchors_on_pixel 21 | 22 | 23 | class Converter: 24 | def __init__(self, config): 25 | self.data = None 26 | self.imgshape = None 27 | self.downscale_factor = None 28 | self.shape = None 29 | 30 | self.CONFIG = config 31 | self.PIXEL_SPACING = int(config['SETTINGS']['PIXEL_SPACING']) 32 | self.ROTATE = config['SETTINGS'].getboolean('ROTATE') 33 | self.LAYER_2_OFFSET = config['SETTINGS'].getboolean('LAYER_2_OFFSET') 34 | self.BRIGHT_BG = config['SETTINGS'].getboolean('BRIGHT_BG') 35 | self.E_MODE = config['SETTINGS'].getboolean('E_MODE') 36 | self.VERBOSE = config['SETTINGS'].getboolean('VERBOSE') 37 | 38 | self.SLIDER_MAX_WIDTH = int(config['SETTINGS']['SLIDER_MAX_WIDTH']) 39 | self.SLIDER_MAX_HEIGHT = int(config['SETTINGS']['SLIDER_MAX_HEIGHT']) 40 | 41 | self.LAYER_1_LEVELS = list(map(int, config['LAYER 1'].keys())) 42 | self.LAYER_1_CODES = list(config['LAYER 1'].values()) 43 | self.LAYER_2_LEVELS = list(map(int, config['LAYER 2'].keys())) 44 | self.LAYER_2_CODES = list(config['LAYER 2'].values()) 45 | 46 | def convert(self, path, time=0, start_pos=None): 47 | self.load_image(path) 48 | self.prepare_image() 49 | 50 | if self.VERBOSE: 51 | print("Image resolution: ", self.imgshape) 52 | print("Slider resolution:", self.shape) 53 | print("Slider size: ", self.shape * self.PIXEL_SPACING) 54 | 55 | anchors = [] 56 | 57 | self.add_layer(anchors, 1, False) 58 | self.add_layer(anchors, 2, True) 59 | 60 | if len(anchors) < 2: 61 | print("Insufficient anchors generated for slider.") 62 | return 63 | 64 | if start_pos is not None: 65 | if not np.equal(start_pos, anchors[0]).all(): 66 | anchors.insert(0, start_pos) 67 | 68 | anchor1 = anchors.pop(0) 69 | slidercode = "%s,%s,%s,6,0,L" % (anchor1[0], anchor1[1], int(time)) 70 | 71 | for anchor in anchors: 72 | anchor_string = "|%s:%s" % (anchor[0], anchor[1]) 73 | slidercode += anchor_string 74 | 75 | slidercode += ",1,1\n" 76 | 77 | return slidercode 78 | 79 | def load_image(self, infilename): 80 | img = Image.open(infilename) 81 | img.load() 82 | self.data = np.asarray(img, dtype="int32") 83 | 84 | def prepare_image(self): 85 | # Handle grayscale and colour images 86 | if len(self.data.shape) > 2: 87 | data_color = self.data[:, :, :3] 88 | # Convert colour to gray scale with relative luminance 89 | data_gray = np.average(data_color, axis=2, weights=[0.2126, 0.7152, 0.0722]) 90 | 91 | # If there is no alpha channel, just make everything maximum opacity 92 | if self.data.shape[2] > 3: 93 | data_a = self.data[:, :, 3] 94 | else: 95 | data_a = np.full(data_gray.shape[:2], 255) 96 | else: 97 | data_gray = self.data 98 | data_a = np.full(data_gray.shape[:2], 255) 99 | 100 | self.data = np.dstack([data_gray, data_a]) 101 | self.imgshape = self.data.shape 102 | 103 | # Calculate a downscale factor such that the resulting slider will fit in the boundaries defined in the config 104 | self.downscale_factor = np.max([data_gray.shape[0] / self.SLIDER_MAX_WIDTH, 105 | data_gray.shape[1] / self.SLIDER_MAX_HEIGHT]) * self.PIXEL_SPACING if self.ROTATE else \ 106 | np.max([data_gray.shape[1] / self.SLIDER_MAX_WIDTH, 107 | data_gray.shape[0] / self.SLIDER_MAX_HEIGHT]) * self.PIXEL_SPACING 108 | 109 | self.shape = np.ceil(np.divide(data_gray.shape, self.downscale_factor)).astype(np.int32) 110 | 111 | def osu_to_pixel(self, coord): 112 | scaled = np.round(np.divide(coord, self.PIXEL_SPACING) * self.downscale_factor).astype(np.int32) 113 | try: 114 | return self.data[scaled[1], scaled[0]] 115 | except IndexError: 116 | return np.array([0, 0]) 117 | 118 | def get_anchor_code(self, pixel, layer): 119 | levels = self.LAYER_1_LEVELS if layer == 1 else self.LAYER_1_LEVELS 120 | codes = self.LAYER_1_CODES if layer == 1 else self.LAYER_1_CODES 121 | 122 | level = 0 123 | while level + 1 < len(levels) and levels[level + 1] <= pixel[0]: 124 | level += 1 125 | 126 | colour = codes[level] 127 | return colour 128 | 129 | def add_layer(self, anchors, layer, reverse): 130 | pass 131 | -------------------------------------------------------------------------------- /TSP_Solver/bounds.py: -------------------------------------------------------------------------------- 1 | import random 2 | import numpy as np 3 | from scipy.sparse.csgraph import minimum_spanning_tree 4 | 5 | 6 | class Bounds: 7 | def __init__(self, nodeDict, adjMatrix): 8 | self.nodeDict = nodeDict 9 | self.adjMatrix = adjMatrix 10 | self.counts = len(nodeDict) 11 | self.edgeDict = {} 12 | for i in range(self.counts): 13 | for j in range(i + 1, self.counts): 14 | vertices = (i, j) 15 | self.edgeDict[vertices] = self.adjMatrix[i, j] 16 | 17 | ''' 18 | Held-Karp Lower Bound 19 | An iterative estimation that provides the tightest lower bound for a TSP. The HKLB differs based on U (Target Value). 20 | One can determine the best HKLB through experimentations of U for each TSP instance. 21 | ''' 22 | 23 | def calculateHKLB(self): 24 | # Input Parameters 25 | # U our upper bound target value is selected as roughly 115% of the OTB lower bound 26 | U = 1.15 * self.calculateOTB(self.adjMatrix)[0] 27 | iterationFactor = 0.015 28 | maxChanges = 100 29 | hkBound = -10000000000000000 30 | tsmall = 0.001 31 | alpha = 1 32 | beta = 0.5 33 | nodeNumbers = np.zeros(self.counts) 34 | numIterations = int(round(iterationFactor * self.counts)) 35 | if numIterations == 0: 36 | numIterations += 1 37 | tVector = np.zeros(numIterations) 38 | newAdjMat = self.adjMatrix.copy() 39 | for i in range(0, maxChanges): 40 | for k in range(0, numIterations): 41 | # Calcuate the new edge weights based on nodeNumbers 42 | tempMatrix = self.calcNewMatrix(newAdjMat, nodeNumbers) 43 | result = self.calculateOptimalOTB(tempMatrix) 44 | oneTreeBound = result[0] 45 | oneTreeEdges = result[1] 46 | # HKBound is given as the sum of the OTB of the adjusted edges and 2* the sum of the nodeNumbers 47 | newHkBound = oneTreeBound + 2 * np.sum(nodeNumbers) 48 | # Improvement of hKBound 49 | if newHkBound > hkBound: 50 | hkBound = newHkBound 51 | # aTour contains a boolean that says if it's a tour and corresponding degDict 52 | aTour = self.isATourOT(oneTreeEdges) 53 | if aTour[0]: 54 | return hkBound 55 | degsVals = list(aTour[1].values()) 56 | sumAllDegs = float(np.sum(np.square(2 - np.array(degsVals)))) 57 | tVector[k] = alpha * (U - newHkBound) / sumAllDegs 58 | # Terminate when the stepSize is too small 59 | if tVector[k] < tsmall: 60 | return hkBound 61 | deltNode = tVector[k] * (2 - np.array(degsVals)) 62 | nodeNumbers = nodeNumbers + deltNode 63 | # Changes the decrement factor each loop 64 | alpha = beta * alpha 65 | return hkBound 66 | 67 | def calcNewMatrix(self, adjMatrix, nodeNumbers): 68 | temp = adjMatrix.copy() 69 | m = len(temp) 70 | # i is the index 71 | for i in range(0, m): 72 | temp[i] -= nodeNumbers[i] 73 | temp[:, i] -= nodeNumbers[i] 74 | temp[i][i] = 0 75 | return temp 76 | 77 | # This function only checks if each node in the 1-tree has degree 2. A 1-tree implies connectedness. If every node has degree 2, 78 | # a one-tree must be a tour. 79 | def isATourOT(self, oneTree): 80 | nodes = range(0, self.counts) 81 | degreeDict = {node: 0 for node in nodes} 82 | for edge in oneTree: 83 | x = edge[0] 84 | y = edge[1] 85 | degreeDict[x] += 1 86 | degreeDict[y] += 1 87 | for i in nodes: 88 | if degreeDict[i] != 2: 89 | return [False, degreeDict] 90 | return [True, degreeDict] 91 | 92 | ''' 93 | 1-tree Bound 94 | A form of lower bound that utilizes the 1-tree based on Chapter 7 of The Traveling Salesman Problem: A Computational Study by Cook 95 | 1. Pick a random node v0. 96 | 2. Get the length of the MST after disregarding the random node. 97 | 3. Let S be the sum of the cheapest two edges incident with the random node v0. 98 | 4. Output the sum of 2 and 3. 99 | The 1-Tree bound should approximately be 90.5% of the optimal cost. The best 1-Tree lower bound will be the maximum cost of the many MSTs we get. 100 | ''' 101 | 102 | def calculateOTB(self, adjMatrix): 103 | maxOTBLB = -10000000 104 | bestTree = [] 105 | for initNode in range(0, self.counts): 106 | MSTedges = self.OTBHelper(adjMatrix, initNode) 107 | r = self.calcCost(MSTedges) 108 | # s is the sum of the cheapest two edges incident with the random node v0. 109 | s = 0 110 | edgeLengths = adjMatrix[initNode] 111 | nodeNums = range(0, self.counts) 112 | twoNN = sorted(zip(edgeLengths, nodeNums))[1:3] 113 | s = twoNN[0][0] + twoNN[1][0] 114 | temp = r + s 115 | if temp > maxOTBLB: 116 | maxOTBLB = temp 117 | oneTreeEdges = MSTedges[:] 118 | oneTreeEdges.append((initNode, twoNN[0][1])) 119 | oneTreeEdges.append((initNode, twoNN[1][1])) 120 | bestTree = oneTreeEdges 121 | return [maxOTBLB, bestTree] 122 | 123 | def calculateOptimalOTB(self, adjMatrix): 124 | minOTBLB = 1000000 125 | bestTree = [] 126 | for initNode in range(0, self.counts): 127 | MSTedges = self.OTBHelper(adjMatrix, initNode) 128 | r = self.calcAdjustedCost(MSTedges, adjMatrix) 129 | # s is the sum of the cheapest two edges incident with the random node v0. 130 | s = 0 131 | edgeLengths = adjMatrix[initNode] 132 | nodeNums = range(0, self.counts) 133 | twoNN = sorted(zip(edgeLengths, nodeNums))[1:3] 134 | s = twoNN[0][0] + twoNN[1][0] 135 | temp = r + s 136 | if temp < minOTBLB: 137 | minOTBLB = temp 138 | oneTreeEdges = MSTedges[:] 139 | oneTreeEdges.append((initNode, twoNN[0][1])) 140 | oneTreeEdges.append((initNode, twoNN[1][1])) 141 | bestTree = oneTreeEdges 142 | return [minOTBLB, bestTree] 143 | 144 | def OTBHelper(self, adjMatrix, initNode): 145 | # Create an AdjMatrix without the row & col containing the initNode 146 | newAdjMat = adjMatrix 147 | newAdjMat = np.delete(newAdjMat, initNode, axis=0) 148 | newAdjMat = np.delete(newAdjMat, initNode, axis=1) 149 | # Calculate MST length without the initNode 150 | mst = minimum_spanning_tree(newAdjMat) 151 | MSTedges = [] 152 | Z = mst.toarray().astype(float) 153 | for i in range(len(Z)): 154 | array = np.nonzero(Z[i])[0] 155 | for index in array: 156 | x = i 157 | y = index 158 | if i >= initNode: 159 | x += 1 160 | if index >= initNode: 161 | y += 1 162 | tuplex = (x, y) 163 | MSTedges.append(tuplex) 164 | return MSTedges 165 | 166 | def calcAdjustedCost(self, MSTedges, adjMatrix): 167 | r = 0 168 | for edge in MSTedges: 169 | r += adjMatrix[edge[0], edge[1]] 170 | return r 171 | 172 | def calcCost(self, MSTedges): 173 | # r is the length of the MST we have without the initNode 174 | r = 0 175 | for edge in MSTedges: 176 | checkEdge = edge 177 | if (checkEdge not in self.edgeDict): 178 | checkEdge = (edge[1], edge[0]) 179 | r += self.edgeDict[checkEdge] 180 | return r 181 | 182 | ''' 183 | MST Upper Bound 184 | Simply 2* the MST cost of the original dataSet 185 | ''' 186 | 187 | def calculateMSTUpperBound(self): 188 | mst = minimum_spanning_tree(self.adjMatrix) 189 | MSTedges = [] 190 | Z = mst.toarray().astype(float) 191 | for i in range(len(Z)): 192 | array = np.nonzero(Z[i])[0] 193 | for index in array: 194 | tuplex = (i, index) 195 | MSTedges.append(tuplex) 196 | cost = 0 197 | for edge in MSTedges: 198 | checkEdge = edge 199 | if (checkEdge not in self.edgeDict): 200 | checkEdge = (edge[1], edge[0]) 201 | cost += self.edgeDict[checkEdge] 202 | return 2 * cost 203 | -------------------------------------------------------------------------------- /smart_converter.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | from converter import * 3 | from TSP_Solver import Graph_TSP 4 | 5 | 6 | class Line: 7 | def __init__(self, x1, x2, y): 8 | self.x1 = x1 9 | self.x2 = x2 10 | self.y = y 11 | self.i = -1 12 | 13 | def get_hashable(self): 14 | return self.x1, self.x2, self.y, self.i 15 | 16 | def __str__(self): 17 | return '%s:%s,%s,%s' % (self.x1, self.x2, self.y, self.i) 18 | 19 | 20 | def line_distance(line1, line2): 21 | return abs(line2[2] - line1[2]) if line2[0] <= line1[1] and line2[1] >= line1[0] else \ 22 | min(abs(line2[0] - line1[1]) + abs(line2[2] - line1[2]), 23 | abs(line2[1] - line1[0]) + abs(line2[2] - line1[2])) 24 | 25 | 26 | # Image converter specifically made for Bad Apple!! 27 | # Only places white anchors on bright pixels and leaves the rest as black as possible 28 | class SmartConverter(Converter): 29 | def add_layer(self, anchors, layer, reverse): 30 | # - Calculate horizontal lines of adjacent anchors. 31 | # - Calculate adjacencies and distances between the lines. 32 | # - Solve the travelling salesman problem on all the points. 33 | # - Traverse the solution cycle and place anchors for each traversed node. 34 | 35 | if layer > 1: 36 | return 37 | 38 | # Create horizontal scan lines for the graph 39 | if self.VERBOSE: 40 | print("Creating scan lines...") 41 | 42 | lines_dict = {} 43 | g = nx.Graph() 44 | counter = 0 45 | prev_line_lines = [] 46 | for y in range(self.shape[0]): 47 | x1 = -1 48 | line_lines = [] 49 | for x in range(self.shape[1]): 50 | osucoord = np.array([x, y]) * self.PIXEL_SPACING 51 | pixel = self.osu_to_pixel(osucoord) 52 | 53 | # Omit transparent pixels 54 | if pixel[0] < 128: 55 | if x - x1 > 1: 56 | line_lines.append(Line(x1 + 1, x - 1, y)) 57 | x1 = x 58 | elif x == self.shape[1] - 1: 59 | line_lines.append(Line(x1 + 1, x, y)) 60 | 61 | for line in line_lines: 62 | line.i = counter 63 | lines_dict[counter] = line 64 | g.add_node(line.get_hashable(), weight=1) 65 | counter += 1 66 | 67 | # Add direct neighbors 68 | for prev_line in prev_line_lines: 69 | if prev_line.x1 <= line.x2 and prev_line.x2 >= line.x1: 70 | # Lines are touching 71 | g.add_edge(line.get_hashable(), prev_line.get_hashable()) 72 | 73 | prev_line_lines = line_lines 74 | 75 | if self.VERBOSE: 76 | print("counter: %s" % counter) 77 | 78 | if counter == 0: 79 | return 80 | 81 | # Detect disconnected fragments and connect them 82 | if self.VERBOSE: 83 | print("Connecting components...") 84 | 85 | components = list(nx.connected_components(g)) 86 | while len(components) > 1: 87 | if self.VERBOSE: 88 | print("number of components: %s" % len(components)) 89 | # d contains disconnected subgraphs 90 | for component in components: 91 | # Find the closest connection between this component and another component 92 | closest = None 93 | closest_dist = 99999999999999 94 | for node in component: 95 | for other_component in components: 96 | if other_component == component: 97 | continue 98 | for other_node in other_component: 99 | dist = line_distance(node, other_node) 100 | if node[2] == other_node[2] == 0: 101 | dist = min(dist, 2) 102 | if node[0] == other_node[0] == 0: 103 | dist = min(dist, 2) 104 | if dist <= closest_dist: 105 | closest_dist = dist 106 | closest = (node, other_node) 107 | # Add the connection 108 | if closest is not None: 109 | g.add_edge(closest[0], closest[1]) 110 | 111 | components = list(nx.connected_components(g)) 112 | 113 | # Use dijkstra's algorithm to calculate distance matrix with pathing 114 | # Each node stores for each other node the distance (int) and the previous node in the path (int) 115 | # This is enough to know the nearest nodes and how to get there 116 | # This can be stored in a 3D int array 117 | if self.VERBOSE: 118 | print("Creating distance matrix...") 119 | len_path = dict(nx.all_pairs_dijkstra(g)) 120 | 121 | # Make fully connected adjacency matrix 122 | adj = np.empty((counter, counter)) 123 | for n in len_path: 124 | dist = len_path[n][0] 125 | for d in dist: 126 | adj[n[3], d[3]] = dist[d] 127 | 128 | # Solve traveling salesman 129 | if self.VERBOSE: 130 | print("Solving TSP...") 131 | instance_graph = Graph_TSP(lines_dict, adj, "frame", -1) 132 | christofides = instance_graph.christofides() 133 | 134 | result = christofides 135 | 136 | # Get the part of the cycle which is in the top right corner 137 | min_y = 99999999999 138 | min_x = 99999999999 139 | min_index = 0 140 | for i in range(len(result)): 141 | line = lines_dict[result[i][0]] 142 | if line.y < min_y: 143 | min_y = line.y 144 | min_x = line.x1 145 | min_index = i 146 | elif line.y == min_y and line.x1 < min_x: 147 | min_x = line.x1 148 | min_index = i 149 | 150 | result = result[min_index:] + result[:min_index] 151 | 152 | # Draw anchors 153 | if self.VERBOSE: 154 | print("Drawing anchors...") 155 | prev_line = None 156 | for t in result: 157 | line = lines_dict[t[0]].get_hashable() 158 | 159 | if prev_line is not None: 160 | paths = len_path[prev_line][1] 161 | self.draw_path_to_line(prev_line, line, paths[line], anchors) 162 | 163 | self.draw_line(line, anchors) 164 | 165 | prev_line = line 166 | 167 | def draw_line(self, line, anchors): 168 | for x in range(line[0], line[1] + 1): 169 | anchors.append(np.array((x, line[2])) * self.PIXEL_SPACING) 170 | 171 | def draw_path_to_line(self, start_line, end_line, path, anchors): 172 | pos = (start_line[1], start_line[2]) 173 | lbound = None 174 | rbound = None 175 | prev = None 176 | last_dir_up = None 177 | for line in path: 178 | dir_up = prev[0][2] < line[2] if prev is not None else None 179 | 180 | # Interpolate and check if its inside the line 181 | if lbound is None or rbound is None: 182 | lbound = line[0] 183 | rbound = line[1] 184 | elif pos[1] != line[2]: 185 | lbi = (line[0] - pos[0]) / abs(line[2] - pos[1]) + pos[0] 186 | rbi = (line[1] - pos[0]) / abs(line[2] - pos[1]) + pos[0] 187 | 188 | lbound = max(lbi, lbound) 189 | rbound = min(rbi, rbound) 190 | 191 | # If this is the last line then it will add a line straight to the start of the end line 192 | # we do this check to make sure this doesn't pass through any black area 193 | if line == end_line and lbi < lbound: 194 | lbound = rbound + 1 195 | 196 | if lbound > rbound or dir_up != last_dir_up: 197 | if prev[0] != start_line and prev[0] != end_line: 198 | nx = prev[2] if (line[0]+line[1])/2 > pos[0] else prev[1] 199 | pos2 = (round((nx - pos[0]) * abs(prev[0][2] - pos[1]) + pos[0]), prev[0][2]) 200 | if pos2 != pos and not (pos2[0] == end_line[0] and pos2[1] == end_line[2])\ 201 | and not (pos2[0] == start_line[1] and pos2[1] == start_line[2]): 202 | anchors.append(np.array(pos2) * self.PIXEL_SPACING) 203 | pos = pos2 204 | lbound = line[0] 205 | rbound = line[1] 206 | 207 | # Make sure component connections go through the same two points every time 208 | # so only one connection line is visible (all overlap) 209 | if prev is not None and line_distance(prev[0], line) > 1: 210 | # Add an anchor at the closest point 211 | nx = prev[0][1] if line[0] > prev[0][1] else prev[0][0] if line[1] < prev[0][0] else max(line[0], prev[0][0]) 212 | pos2 = (nx, prev[0][2]) 213 | if pos2 != pos and not (pos2[0] == end_line[0] and pos2[1] == end_line[2])\ 214 | and not (pos2[0] == start_line[1] and pos2[1] == start_line[2]): 215 | anchors.append(np.array(pos2) * self.PIXEL_SPACING) 216 | pos = pos2 217 | 218 | nx = line[0] if line[0] > prev[0][1] else line[1] if line[1] < prev[0][0] else max(line[0], prev[0][0]) 219 | pos2 = (nx, line[2]) 220 | if pos2 != pos and not (pos2[0] == end_line[0] and pos2[1] == end_line[2])\ 221 | and not (pos2[0] == start_line[1] and pos2[1] == start_line[2]): 222 | anchors.append(np.array(pos2) * self.PIXEL_SPACING) 223 | pos = pos2 224 | 225 | last_dir_up = dir_up 226 | prev = (line, lbound, rbound) 227 | 228 | -------------------------------------------------------------------------------- /TSP_Solver/algorithms.py: -------------------------------------------------------------------------------- 1 | import random 2 | import operator 3 | import numpy as np 4 | from scipy.spatial import ConvexHull 5 | from scipy.sparse.csgraph import minimum_spanning_tree 6 | import networkx.algorithms as naa 7 | import networkx as nx 8 | 9 | from .disjoint_sets import disjoint_set 10 | 11 | 12 | class Algorithms: 13 | def __init__(self, nodeDict, adjMatrix, counts, edgeDict): 14 | self.nodeDict = nodeDict 15 | self.adjMatrix = adjMatrix 16 | self.counts = counts 17 | self.edgeDict = edgeDict 18 | 19 | # Random solution formed by shuffling nodes 20 | # Meant to provide bad solutions 21 | def random(self): 22 | unvisitedNodes = [i for i in range(0, self.counts)] 23 | random.shuffle(unvisitedNodes) 24 | edgePath = [] 25 | for i in range(0, len(unvisitedNodes)): 26 | if i < self.counts - 1: 27 | edgePath.append((unvisitedNodes[i], unvisitedNodes[i + 1])) 28 | else: 29 | edgePath.append((unvisitedNodes[i], unvisitedNodes[0])) 30 | return self.listConverter(edgePath) 31 | 32 | ''' 33 | NearestNeighbor 34 | Input: counts (an integer that describes the number of nodes in your adjacency matrix) 35 | adjMatrix (counts x counts adjacency Matrix that has edge lengths) 36 | Output: edgePath (list of edges that is the nearest neighbor algorithm's solution) 37 | 3. Based on the minimum edge weight, find the index of that weight in the original matrix. 38 | 4. If that index is NOT in the visitedNodes, remove it from the unvistedNodes list and add it 39 | to the visitedNodes. 40 | 5. Else, remove the minIndex from the edges array and start over from step 2. 41 | 6. Once you remove all the elements of unvisitedNode, terminate and return the sequence of 42 | vertices you will follow. 43 | ''' 44 | 45 | def nn(self): 46 | # Initialize visitedNodes to ensure no cycle is created 47 | visitedNodes = [] 48 | edgePath = [] 49 | # unvisitedNodes: list of unvisited nodes 50 | unvisitedNodes = [i for i in range(0, self.counts)] 51 | # Pick a random node to visit 52 | random.shuffle(unvisitedNodes) 53 | node = unvisitedNodes.pop() 54 | visitedNodes.append(node) 55 | while unvisitedNodes: 56 | # Select current node's closest neighbor (minimum edge weight) 57 | edges = np.copy(self.adjMatrix[node]) 58 | sortedIndices = np.argsort(edges) 59 | for index in sortedIndices: 60 | if index not in visitedNodes: 61 | minIndex = index 62 | break 63 | unvisitedNodes.remove(minIndex) 64 | visitedNodes.append(minIndex) 65 | node = minIndex 66 | for i in range(0, self.counts): 67 | if i < self.counts - 1: 68 | edgePath.append((visitedNodes[i], visitedNodes[i + 1])) 69 | else: 70 | edgePath.append((visitedNodes[i], visitedNodes[0])) 71 | return edgePath 72 | 73 | ''' 74 | Greedy Search 75 | 1. Sort the edges by weight values. 76 | 2. Select the least-valued edge. 77 | 3. Make sure it does not form a cycle if added. I do this by checking if both vertices are in 78 | visitedNodes. This is accomplished with a small helper function isCycle. 79 | 4. Check also if the two nodes have less than degree 2. 80 | 5. If both constraints apply, add 1 to each degree and also change visitedNodes. Make sure to 81 | remove the edge that we added. 82 | 6. Start from the next least-value again and check each to make sure no cycle is formed and all degrees 83 | are less than 2. 84 | ''' 85 | 86 | def g(self): 87 | allNodes = [] 88 | edgePath = [] 89 | for node in range(0, self.counts): 90 | allNodes.append(disjoint_set(node)) 91 | sorted_edges = sorted(self.edgeDict.items(), key=operator.itemgetter(1)) 92 | degreeDict = {element: 0 for element in allNodes} 93 | numEdges = 0 94 | startNode = allNodes[sorted_edges[0][0][0]] 95 | while numEdges < self.counts - 1: 96 | for edge in sorted_edges: 97 | vertices = edge[0] 98 | ds1 = allNodes[vertices[0]] 99 | ds2 = allNodes[vertices[1]] 100 | if not (self.isCycle(ds1, ds2)) and self.nodeLessTwo(ds1, ds2, degreeDict): 101 | ds1.joinSets(ds2) 102 | degreeDict[ds1] += 1 103 | degreeDict[ds2] += 1 104 | numEdges += 1 105 | edgePath.append(vertices) 106 | lastTwo = [allNodes.index(x) for x in degreeDict.keys() if degreeDict[x] == 1] 107 | edgePath.append((lastTwo[0], lastTwo[1])) 108 | return edgePath 109 | 110 | def isCycle(self, ds1, ds2): 111 | return ds1.find() == ds2.find() 112 | 113 | def nodeLessTwo(self, d1, d2, degreeDict): 114 | return (degreeDict[d1] < 2) and (degreeDict[d2] < 2) 115 | 116 | ''' 117 | Convex Hull Insertion 118 | 1. Form a convex hull of our current graph. This forms our initial cycle. 119 | 2. For each node not in our current convex hull, find an edge e_ij = {n_i, n_j} in our current convex hull such that w_i,r + w_r,j - w_i,j 120 | is minimal and keep track of this minimal triplet. 121 | 3. For all triplets, find the minimal triplet (n_i', n_j',n_r') such that (w_i,r' + w_r,j')/ w_i,j' is minimal. 122 | 4. Insert n_r' between n_i' and n_j' by adding the edges e_r,i & e_r,j while removing edge e_i,j 123 | 5. Repeat step 2-4 until all nodes have been added to our cycle. 124 | ''' 125 | 126 | def convHull(self): 127 | # Initial Subtour composed of Convex Hull 128 | allPoints = np.array(list(self.nodeDict.values())) 129 | convHull = ConvexHull(allPoints) 130 | listofHullEdges = convHull.simplices.tolist() 131 | listofHullIndices = convHull.vertices.tolist() 132 | allTours = [listofHullEdges] 133 | unvisitedNodes = [z for z in self.nodeDict.keys() if z not in listofHullIndices] 134 | visitedNodes = listofHullIndices[:] 135 | listOfCurrentEdges = listofHullEdges[:] 136 | while unvisitedNodes: 137 | triplets = [] 138 | listOfCurrentEdges = listOfCurrentEdges[:] 139 | # Go through each node not in the current Cycle 140 | for node in unvisitedNodes: 141 | neighborVals = self.adjMatrix[node] 142 | minVal = 1000000000000 143 | triplet = (-10, -10, -10) 144 | # Find the minimal triplet for each node that adheres to the minimal w_ir + w_jr - w_ij 145 | for edge in listOfCurrentEdges: 146 | nodeI = edge[0] 147 | nodeJ = edge[1] 148 | cost = neighborVals[nodeI] + neighborVals[nodeJ] - self.adjMatrix[nodeI][nodeJ] 149 | if cost < minVal: 150 | minVal = cost 151 | triplet = (nodeI, nodeJ, node) 152 | triplets.append(triplet) 153 | # From all these triplets, find the most optimal one based on the ratio! 154 | minRatio = 1000000000000 155 | chosenTrip = (-10, -10, -10) 156 | for triple in triplets: 157 | ratio = (self.adjMatrix[triple[0]][triple[2]] + self.adjMatrix[triple[1]][triple[2]]) / \ 158 | self.adjMatrix[triple[0]][triple[1]] 159 | if minRatio > ratio: 160 | minRatio = ratio 161 | chosenTrip = triple 162 | # Insert node_r between node_i and node_j 163 | node_i = chosenTrip[0] 164 | node_j = chosenTrip[1] 165 | node_r = chosenTrip[2] 166 | currEdge = [x for x in listOfCurrentEdges if all([node_i in x, node_j in x])][0] 167 | listOfCurrentEdges.append([node_i, node_r]) 168 | listOfCurrentEdges.append([node_j, node_r]) 169 | listOfCurrentEdges.remove(currEdge) 170 | unvisitedNodes.remove(node_r) 171 | visitedNodes.append(node_r) 172 | # Alltours is for visualization Purposes 173 | allTours.append(listOfCurrentEdges) 174 | return self.listConverter(listOfCurrentEdges), allTours 175 | 176 | ''' 177 | Christofides Algorithm 178 | 1. Form minimum spanning tree T of G. 179 | 2. Generate an Minimum perfect matching of the vertices in the MST that have odd degrees. 180 | 3. Form an Eulerian path on the multigraph formed by the union (keep duplicates) of the MST and minimum weight perfect matching. 181 | 4. Perform shortcutting and skip repeated vertices in the Eulerian path to get a Hamiltonian circuit. 182 | ''' 183 | 184 | def cf(self): 185 | # Create a minimum spanning Tree of Graph G 186 | Tcsr = minimum_spanning_tree(self.adjMatrix) 187 | MSTedges = [] 188 | degreeDict = dict(zip(self.nodeDict.keys(), [0] * len(self.nodeDict.keys()))) 189 | Z = Tcsr.toarray().astype(float) 190 | for i in range(len(Z)): 191 | array = np.nonzero(Z[i])[0] 192 | for index in array: 193 | if index.size != 0: 194 | degreeDict[i] += 1 195 | degreeDict[index] += 1 196 | tuplex = (i, index) 197 | MSTedges.append(tuplex) 198 | # STEP 2: Isolate the vertices of the MST with odd degree 199 | OddVerts = [x for x in degreeDict.keys() if degreeDict[x] % 2 != 0] 200 | # STEP 3: Only Consider the values in OddVerts and form a min-weight perfect matching 201 | H = nx.Graph() 202 | H.add_nodes_from(self.nodeDict.keys()) 203 | for i in range(len(OddVerts)): 204 | for j in range(len(OddVerts)): 205 | if i != j: 206 | H.add_edge(OddVerts[i], OddVerts[j], weight=-self.adjMatrix[OddVerts[i]][OddVerts[j]]) 207 | minWeight = list(naa.max_weight_matching(H, maxcardinality=True)) 208 | uniqueMW = [] 209 | # Prune out redundant Tuples 210 | for edge in minWeight: 211 | if edge not in uniqueMW and (edge[1], edge[0]) not in uniqueMW: 212 | uniqueMW.append(edge) 213 | unionMW_MST = MSTedges[:] 214 | for tup in uniqueMW: 215 | # Only add first index since both edges are returned for instance: (0,1) & (1,0) are returned 216 | unionMW_MST.append(tup) 217 | degreeDict[tup[0]] += 1 218 | degreeDict[tup[1]] += 1 219 | # Retrieve the Eulerian Circuit 220 | eulerianCircuit = self.eulerianTour(unionMW_MST, self.nodeDict) 221 | shortCut = [] 222 | unvisitedPath = [] 223 | totalPath = [i for sub in eulerianCircuit for i in sub] 224 | for node in totalPath: 225 | if node not in unvisitedPath: 226 | shortCut.append(node) 227 | unvisitedPath.append(node) 228 | return [MSTedges, minWeight, eulerianCircuit, self.pathEdges(shortCut)] 229 | 230 | # Make sure to connect the first and last vertex to get a hamiltonian cycle! 231 | def pathEdges(self, visitedNodes): 232 | solution = [] 233 | for i in range(0, len(visitedNodes)): 234 | if i < len(visitedNodes) - 1: 235 | solution.append((visitedNodes[i], visitedNodes[i + 1])) 236 | else: 237 | solution.append((visitedNodes[i], visitedNodes[0])) 238 | return solution 239 | 240 | def eulerianTour(self, setOfEdges, vertDict): 241 | tempGraph = nx.MultiGraph() 242 | tempGraph.add_nodes_from(vertDict.keys()) 243 | tempGraph.add_edges_from(setOfEdges) 244 | return list(nx.eulerian_circuit(tempGraph)) 245 | 246 | def listConverter(self, edgeList): 247 | tupleSol = [] 248 | for listElem in edgeList: 249 | tupleSol.append((listElem[0], listElem[1])) 250 | return tupleSol 251 | -------------------------------------------------------------------------------- /fill_converter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | 4 | from converter import * 5 | import cv2 6 | import networkx as nx 7 | from TSP_Solver import Graph_TSP 8 | 9 | 10 | class Line: 11 | def __init__(self, x1, x2, y): 12 | self.x1 = x1 13 | self.x2 = x2 14 | self.y = y 15 | self.i = -1 16 | 17 | def get_hashable(self): 18 | return self.x1, self.x2, self.y, self.i 19 | 20 | def __str__(self): 21 | return '%s:%s,%s,%s' % (self.x1, self.x2, self.y, self.i) 22 | 23 | 24 | def line_distance(line1, line2): 25 | return abs(line2[2] - line1[2]) if line2[0] <= line1[1] and line2[1] >= line1[0] else \ 26 | min(abs(line2[0] - line1[1]) + abs(line2[2] - line1[2]), 27 | abs(line2[1] - line1[0]) + abs(line2[2] - line1[2])) 28 | 29 | 30 | def draw_line(line, anchors): 31 | anchors.append(np.array((line[0], line[2]))) 32 | anchors.append(np.array((line[1], line[2]))) 33 | 34 | 35 | def draw_path_to_line(start_line, end_line, path, anchors): 36 | pos = (start_line[1], start_line[2]) 37 | lbound = None 38 | rbound = None 39 | prev = None 40 | last_dir_up = None 41 | for line in path: 42 | dir_up = prev[0][2] < line[2] if prev is not None else None 43 | 44 | # Interpolate and check if its inside the line 45 | if lbound is None or rbound is None: 46 | lbound = line[0] 47 | rbound = line[1] 48 | elif pos[1] != line[2]: 49 | lbi = (line[0] - pos[0]) / abs(line[2] - pos[1]) + pos[0] 50 | rbi = (line[1] - pos[0]) / abs(line[2] - pos[1]) + pos[0] 51 | 52 | lbound = max(lbi, lbound) 53 | rbound = min(rbi, rbound) 54 | 55 | # If this is the last line then it will add a line straight to the start of the end line 56 | # we do this check to make sure this doesn't pass through any black area 57 | if line == end_line and lbi < lbound: 58 | lbound = rbound + 1 59 | 60 | if lbound > rbound or dir_up != last_dir_up: 61 | if prev[0] != start_line and prev[0] != end_line: 62 | nx = prev[2] if (line[0]+line[1])/2 > pos[0] else prev[1] 63 | pos2 = (round((nx - pos[0]) * abs(prev[0][2] - pos[1]) + pos[0]), prev[0][2]) 64 | if pos2 != pos and not (pos2[0] == end_line[0] and pos2[1] == end_line[2])\ 65 | and not (pos2[0] == start_line[1] and pos2[1] == start_line[2]): 66 | anchors.append(np.array(pos2)) 67 | pos = pos2 68 | lbound = line[0] 69 | rbound = line[1] 70 | 71 | # Make sure component connections go through the same two points every time 72 | # so only one connection line is visible (all overlap) 73 | if prev is not None and line_distance(prev[0], line) > 1: 74 | # Add an anchor at the closest point 75 | nx = prev[0][1] if line[0] > prev[0][1] else prev[0][0] if line[1] < prev[0][0] else max(line[0], prev[0][0]) 76 | pos2 = (nx, prev[0][2]) 77 | if pos2 != pos and not (pos2[0] == end_line[0] and pos2[1] == end_line[2])\ 78 | and not (pos2[0] == start_line[1] and pos2[1] == start_line[2]): 79 | anchors.append(np.array(pos2)) 80 | pos = pos2 81 | 82 | nx = line[0] if line[0] > prev[0][1] else line[1] if line[1] < prev[0][0] else max(line[0], prev[0][0]) 83 | pos2 = (nx, line[2]) 84 | if pos2 != pos and not (pos2[0] == end_line[0] and pos2[1] == end_line[2])\ 85 | and not (pos2[0] == start_line[1] and pos2[1] == start_line[2]): 86 | anchors.append(np.array(pos2)) 87 | pos = pos2 88 | 89 | last_dir_up = dir_up 90 | prev = (line, lbound, rbound) 91 | 92 | 93 | def triangle_area(a, b, c): 94 | return abs(0.5 * (a[0] * (b[1] - c[1]) + 95 | b[0] * (c[1] - a[1]) + 96 | c[0] * (a[1] - b[1]))) 97 | 98 | 99 | def collinear(a, b, c): 100 | return abs((a[1] - b[1]) * (a[0] - c[0]) - (a[1] - c[1]) * (a[0] - b[0])) <= 1e-6 101 | 102 | 103 | # Basic image converter for multi-colour images 104 | class FillConverter(Converter): 105 | def __init__(self, config_file): 106 | super().__init__(config_file) 107 | 108 | def convert(self, path, time=0, start_pos=None): 109 | self.load_image(path) 110 | self.prepare_image() 111 | 112 | if self.VERBOSE: 113 | print("Image resolution: ", self.imgshape) 114 | print("Slider resolution:", self.shape) 115 | print("Slider size: ", self.shape * self.PIXEL_SPACING) 116 | 117 | try: 118 | return self.process_image(time) 119 | except Exception: 120 | print("Exception in user code:") 121 | print("-" * 60) 122 | traceback.print_exc(file=sys.stdout) 123 | print("-" * 60) 124 | 125 | def process_image(self, time): 126 | SCALE = float(self.CONFIG['SETTINGS']['SCALE']) 127 | WINDOW_WIDTH = 70 * 4 / SCALE 128 | 129 | windowsize = int(np.ceil(WINDOW_WIDTH * 0.05)) 130 | radius = int(np.ceil(windowsize / 2)) 131 | 132 | circle_window = np.zeros((windowsize, windowsize), np.uint8) 133 | cv2.circle(circle_window, (radius, radius), windowsize, (255, 255, 255), -1) 134 | 135 | circle_window2 = np.zeros((windowsize // 3, windowsize // 3), np.uint8) 136 | cv2.circle(circle_window2, (radius // 3, radius // 3), windowsize // 3, (255, 255, 255), -1) 137 | 138 | img = self.data[:, :, 0].astype(np.uint8) 139 | #img = cv2.copyMakeBorder(img, 1, 1, 1, 1, cv2.BORDER_CONSTANT, 0) 140 | (thresh, img) = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) 141 | 142 | # Erode the image 143 | img = cv2.erode(img, circle_window) 144 | 145 | contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) 146 | 147 | if len(contours) == 0: 148 | return None 149 | 150 | hierarchy = hierarchy[0] 151 | contours = [contour.reshape(-1, 2) for contour in contours] 152 | 153 | # Erode again for the scan lines 154 | img = cv2.erode(img, circle_window2) 155 | line_width = int(windowsize * 2 / 3) 156 | 157 | # Create horizontal scan lines for the graph 158 | if self.VERBOSE: 159 | print("Creating scan lines...") 160 | 161 | lines_dict = {} 162 | g = nx.Graph() 163 | counter = 0 164 | prev_line_lines = [] 165 | for y in range(int(time % line_width), img.shape[0], line_width): 166 | x1 = -1 167 | line_lines = [] 168 | for x in range(img.shape[1]): 169 | pixel = img[y, x] 170 | 171 | # Omit black pixels 172 | if pixel < 128: 173 | if x - x1 > 1: 174 | line_lines.append(Line(x1 + 1, x - 1, y)) 175 | x1 = x 176 | elif x == img.shape[1] - 1: 177 | line_lines.append(Line(x1 + 1, x, y)) 178 | 179 | for line in line_lines: 180 | line.i = counter 181 | lines_dict[counter] = line 182 | g.add_node(line.get_hashable(), weight=1) 183 | counter += 1 184 | 185 | # Add direct neighbors 186 | for prev_line in prev_line_lines: 187 | if prev_line.x1 <= line.x2 and prev_line.x2 >= line.x1: 188 | # Lines are touching 189 | g.add_edge(line.get_hashable(), prev_line.get_hashable()) 190 | 191 | prev_line_lines = line_lines 192 | 193 | if self.VERBOSE: 194 | print("counter: %s" % counter) 195 | 196 | components = [g.subgraph(c).copy() for c in nx.connected_components(g)] 197 | 198 | fillings = [] 199 | for c in components: 200 | counter = 0 201 | lines_dict2 = {} 202 | for n in c: 203 | lines_dict2[counter] = lines_dict[n[3]] 204 | counter += 1 205 | 206 | if counter == 0: 207 | continue 208 | 209 | # Use dijkstra's algorithm to calculate distance matrix with pathing 210 | # Each node stores for each other node the distance (int) and the previous node in the path (int) 211 | # This is enough to know the nearest nodes and how to get there 212 | # This can be stored in a 3D int array 213 | if self.VERBOSE: 214 | print("Creating distance matrix...") 215 | len_path = dict(nx.all_pairs_dijkstra(c)) 216 | 217 | # Make fully connected adjacency matrix 218 | adj = np.empty((counter, counter)) 219 | ni = 0 220 | for n in len_path: 221 | total_dist = len_path[n][0] 222 | di = 0 223 | for d in total_dist: 224 | adj[ni, di] = total_dist[d] 225 | di += 1 226 | ni += 1 227 | 228 | # Solve traveling salesman 229 | if self.VERBOSE: 230 | print("Solving TSP...") 231 | instance_graph = Graph_TSP(lines_dict2, adj, "frame", -1) 232 | christofides = instance_graph.christofides() 233 | 234 | result = christofides 235 | 236 | # Get the part of the cycle which is in the top right corner 237 | min_y = 99999999999 238 | min_x = 99999999999 239 | min_index = 0 240 | for i in range(len(result)): 241 | line = lines_dict2[result[i][0]] 242 | if line.y < min_y: 243 | min_y = line.y 244 | min_x = line.x1 245 | min_index = i 246 | elif line.y == min_y and line.x1 < min_x: 247 | min_x = line.x1 248 | min_index = i 249 | 250 | result = result[min_index:] + result[:min_index] 251 | 252 | # Draw anchors 253 | if self.VERBOSE: 254 | print("Drawing anchors...") 255 | 256 | anchors = [] 257 | if len(result) > 0: 258 | prev_line = None 259 | for t in result: 260 | line = lines_dict2[t[0]].get_hashable() 261 | 262 | if prev_line is not None: 263 | paths = len_path[prev_line][1] 264 | draw_path_to_line(prev_line, line, paths[line], anchors) 265 | 266 | draw_line(line, anchors) 267 | 268 | prev_line = line 269 | 270 | # Add path to the start again 271 | line = lines_dict2[result[0][0]].get_hashable() 272 | if prev_line is not None: 273 | paths = len_path[prev_line][1] 274 | draw_path_to_line(prev_line, line, paths[line], anchors) 275 | anchors.append(np.array((line[0], line[2]))) 276 | else: 277 | line = lines_dict2[0].get_hashable() 278 | draw_line(line, anchors) 279 | anchors.append(np.array((line[0], line[2]))) 280 | 281 | fillings.append(anchors) 282 | 283 | # Make sliders 284 | if self.VERBOSE: 285 | print("Making sliders...") 286 | 287 | used_fillings = [] 288 | slidercode = "" 289 | # Loop through all outer contours using the hierarchy 290 | next_outer = 0 291 | while next_outer != -1: 292 | current_outer = next_outer 293 | contour = contours[current_outer] 294 | 295 | # Get the index of the next outer contour from the hierarchy 296 | next_outer = hierarchy[current_outer][0] 297 | 298 | if len(contour) < 2: 299 | continue 300 | 301 | # Get all the contours that are child of this contour 302 | child_contours = [] 303 | next_child = hierarchy[current_outer][2] 304 | while next_child != -1: 305 | child_contours.append(contours[next_child]) 306 | next_child = hierarchy[next_child][0] 307 | 308 | # Get the filling that fits inside this contour and the place to connect them 309 | c_fillings = [] 310 | max_dist = windowsize * 1.5 311 | c_start_indices = [] 312 | for i in range(len(contour)): 313 | coord = contour[i] 314 | 315 | # Find the filling for this contour start pos 316 | for filling in fillings: 317 | pos = filling[0] 318 | dist = np.linalg.norm(pos - coord, ord=np.inf) 319 | if dist <= max_dist and list(pos) not in used_fillings: 320 | c_fillings.append(filling) 321 | c_start_indices.append(i) 322 | used_fillings.append(list(pos)) 323 | 324 | # Find the bet way to connect the child contours to the outer contour 325 | # or add child contours to other child contours 326 | # First we add any children to other children 327 | added_children = [] 328 | for k, child in enumerate(child_contours): 329 | # This is the top left point of the child contour 330 | pos = child[0] 331 | # Find the closest point on the outer contour that is to the top left of this point 332 | best_dist = np.inf 333 | best_i = -1 334 | for i, coord in enumerate(contour): 335 | dist = np.linalg.norm(pos - coord) 336 | if dist <= best_dist and coord[0] <= pos[0] and coord[1] <= pos[1]: 337 | best_dist = dist 338 | best_i = i 339 | # Check if any other child is closer than the outer contour 340 | best_child = -1 341 | for j, other_child in enumerate(child_contours): 342 | if k == j: 343 | continue 344 | for i, coord in enumerate(other_child): 345 | dist = np.linalg.norm(pos - coord) 346 | if dist <= best_dist and coord[0] <= pos[0] and coord[1] <= pos[1]: 347 | best_dist = dist 348 | best_i = i 349 | best_child = j 350 | if best_child != -1: 351 | child_contours[best_child] = np.vstack((child_contours[best_child][0:best_i], child, child[0], child_contours[best_child][best_i:-1])) 352 | added_children.append(k) 353 | 354 | # Add the remaining children to the outer contour 355 | for k, child in enumerate(child_contours): 356 | if k in added_children: 357 | continue 358 | # This is the top left point of the child contour 359 | pos = child[0] 360 | # Find the closest point on the outer contour that is to the top left of this point 361 | best_dist = np.inf 362 | best_i = -1 363 | for i, coord in enumerate(contour): 364 | dist = np.linalg.norm(pos - coord) 365 | if dist <= best_dist and coord[0] <= pos[0] and coord[1] <= pos[1]: 366 | best_dist = dist 367 | best_i = i 368 | 369 | # Add the index of the best connection point to the list of start indices 370 | # and add the child contour to the list of fillings so it gets added 371 | c_fillings.append(list(child) + [child[0]]) 372 | c_start_indices.append(best_i) 373 | 374 | # Combine everything into a list of anchors 375 | anchors = [] 376 | for i, coord in enumerate(contour): 377 | anchors.append(coord) 378 | 379 | while i in c_start_indices: 380 | f_index = c_start_indices.index(i) 381 | anchors += c_fillings.pop(f_index) 382 | c_start_indices.pop(f_index) 383 | anchors.append(coord) 384 | 385 | if len(contour) > 0: 386 | anchors.append(contour[0]) 387 | 388 | anchor1 = anchors.pop(0) 389 | anchor1_rounded = np.round(anchors.pop(0) * SCALE) 390 | slidercode += "%s,%s,%s,6,0,L" % (int(anchor1_rounded[0]), int(anchor1_rounded[1]), int(time)) 391 | 392 | total_dist = 0 393 | last_anchor_rounded = anchor1_rounded 394 | last_anchor = anchor1 395 | lastlast_anchor = anchor1 396 | for i, anchor in enumerate(anchors): 397 | round_anchor = np.round(anchor * SCALE) 398 | 399 | # We skip some anchors if they are ugly 400 | if 0 < i < len(anchors) - 1: 401 | next_anchor = anchors[i + 1] 402 | nextnext_anchor = anchors[i + 2] if i + 2 < len(anchors) else next_anchor 403 | big_space = np.linalg.norm(next_anchor - anchor) > 10 404 | next_big_space = np.linalg.norm(next_anchor - nextnext_anchor) > 10 405 | if ((np.linalg.norm(last_anchor - lastlast_anchor) > np.linalg.norm(anchor - next_anchor) or not big_space) and 406 | np.linalg.norm(last_anchor - anchor, ord=np.inf) <= 1) or\ 407 | (np.linalg.norm(last_anchor - anchor) < np.linalg.norm(nextnext_anchor - next_anchor) and next_big_space and 408 | np.linalg.norm(next_anchor - anchor, ord=np.inf) <= 1) or\ 409 | (triangle_area(last_anchor, anchor, next_anchor) < 1e-6 and 410 | np.linalg.norm(last_anchor - anchor) <= np.linalg.norm(next_anchor - last_anchor)): 411 | continue 412 | 413 | if last_anchor_rounded is not None: 414 | dist = np.linalg.norm(round_anchor - last_anchor_rounded) 415 | total_dist += dist 416 | 417 | anchor_string = "|%s:%s" % (int(round_anchor[0]), int(round_anchor[1])) 418 | slidercode += anchor_string 419 | 420 | lastlast_anchor = last_anchor 421 | last_anchor_rounded = round_anchor 422 | last_anchor = anchor 423 | 424 | slidercode += ",1,%s\n" % total_dist 425 | 426 | if self.VERBOSE: 427 | print("num fillings: %s" % len(fillings)) 428 | print("num used fillings: %s" % len(used_fillings)) 429 | print("num contours: %s" % len(contours)) 430 | 431 | return slidercode 432 | 433 | --------------------------------------------------------------------------------