├── .gitignore ├── README.md ├── TSPArt-logo.png ├── draw-tsp-path-concorde.py ├── draw-tsp-path-svg.py ├── draw-tsp-path.py ├── example-output ├── smileyface-inverted-1024-stipple.cyc ├── smileyface-inverted-1024-stipple.png ├── smileyface-inverted-1024-stipple.tsp ├── smileyface-inverted-1024-tsp (Concorde).png ├── smileyface-inverted-1024-tsp (Python).png ├── smileyface-inverted-1024-tsp.svg └── smileyface-inverted.png ├── images ├── australia.png ├── croissant-emoji.png └── smileyface-inverted.png ├── requirements.txt ├── stippling.py └── weighted-voronoi-stippler ├── LICENSE.txt ├── README.md ├── stippler.py └── voronoi.py /.gitignore: -------------------------------------------------------------------------------- 1 | weighted-voronoi-stippler/__pycache__ 2 | .Concorde/ 3 | __pycache__/ 4 | images -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](TSPArt-logo.png) 2 | 3 | # Travelling Salesman Problem (TSP) Art in Python 4 | 5 | The intention of this repo is to provide a beginner-programmer-friendly way to enable people to make their own TSP Art using Python, where you can put in any image and generate a version made purely out of dots, and then another version that connects all these dots in a single continuous line where the end points meet each other. 6 | 7 | The following is written assuming a system with Windows 10. Pull requests are welcome for Linux and MacOS. 8 | 9 | # Outline of Algorithm 10 | 11 | There are two major steps to the algorithm: 12 | 13 | 1. Stippling (or 'pointillism') - the image is represented by small black dots of identical size in a way such that darker areas have more dots clustered closely together than lighter areas. The algorithm used here is called 'weighted voronoi stippling'. 14 | 15 | 2. Determining and drawing the Travelling Salesman Problem Path - the [Travelling Salesman Problem](https://simple.wikipedia.org/wiki/Travelling_salesman_problem) is a classic mathematical optimisation problem where given a list of locations, we are to find a single path that travels through all the locations only once and returns to the starting point. Here we use the dots drawn in the first step as our locations and use an algorithm to determine then draw an appropriate path. 16 | 17 | # Requirements 18 | 19 | * [Python 3](https://www.python.org/downloads/) - you should also know how to use the console/command prompt, and run/execute a Python script. Note that command line options might be different for those using Anaconda. 20 | * Optional: [Concorde TSP Solver](http://www.math.uwaterloo.ca/tsp/concorde/index.html) 21 | * Optional: [Git](https://git-scm.com/) 22 | * Optional: Image editing program. Free/open-source ones: [Krita](https://krita.org/en/), [GIMP](https://www.gimp.org/) 23 | 24 | And lastly, the image(s) that you want to convert! 25 | 26 | ## Python Dependencies 27 | 28 | * Pillow 29 | * ortools (Note that this requires [Microsoft Visual C++ Redistributable for Visual Studio 2019, which can be found at the bottom of this link.](https://visualstudio.microsoft.com/downloads/?q=Visual+C%2B%2B+Redistributable+for+Visual+Studio)) 30 | * tqdm 31 | * imageio 32 | * scipy 33 | * matplotlib 34 | 35 | ## What kind of images should I use for best results? 36 | 37 | **Format:** This will work for the common image formats (`.jpg`, `.png`). More obscure image formats might have some issues, so I'd recommend converting them to `.jpg` or `.png` first. 38 | 39 | **Type**: Generally you'll want to use images that is a single object against a white background. Colour doesn't matter as much since the image is converted to grayscale as part of the stippling process. 40 | 41 | Three images are provided for you in the `images` folder for you to practise on and to observe results. The scripts are initially configured to use `smileyface-inverted.png` in the `images` folder, and you can get an idea of what the output might look like if you check the `example-output` folder. 42 | 43 | # Producing Your TSP Art 44 | 45 | For reference, we'll assume that the initial image is `figure.png` which is placed in the `images` folder, making the filename `images/figure.png'. Replace `figure.png` with whatever other images that you also place in the `images` folder. 46 | 47 | ## 0. Setup 48 | 49 | Download the repository by clicking the green 'Code' button, then select 'Download ZIP' and unzip to the folder of your choice. Alternatively if you have Git installed, `git clone https://github.com/matthras/tsp-art-python` into the folder of your choice. 50 | 51 | Install the required Python libraries by typing into the console: `pip install -r requirements.txt`. Alternatively, manually install the Python dependencies as listed above. If you know how to setup a Python environment feel free to do that first. 52 | 53 | ## 1. Image Preprocessing (and potential problems!) 54 | 55 | Skip this step if you're a first timer. This step will only be relevant after you've run through a few images and want to tweak things a little, or have run into certain problems. 56 | 57 | **Images with transparent backgrounds**: You'll want to colour these backgrounds white in your image-editing program, as some transparent backgrounds are set to black by default. 58 | 59 | **Difficult to distinguish sections of similar colour/shade**: You may have seen this when trying out `croissant-emoji.png`. There's two ways to deal with this: one is to increase the number of dots available. The other is to increase the contrast or recolour sections appropriately using your image editing program. 60 | 61 | **Compression noise/grain/artefacts**: Sometimes your initial image might not necessarily be smooth, or you'll see 'bits that aren't supposed to be there'. These vary a huge amount so there's no one surefire method for dealing with all of them. I know there are built-in methods in GIMP/Krita/Photoshop for dealing with them, but am no expert - usually the examples I work with are simple enough to manually clean using Brush and Fill tools. 62 | 63 | ## 2. Stippling 64 | 65 | Open up `stippling.py` in the editor of your choice, and change the `ORIGINAL_IMAGE` variable to the folder and image that you wish to stipple. So if our image is `figure.png` located in the `images` folder, you'd rename the variable to `"images/figure.png"` 66 | 67 | What should happen on your first time: 68 | 69 | * the console should show something similar to a progress bar, showing each iteration on a new line 70 | * a window will pop up and show the dots arranging themselves 71 | * closing aforementioned window will finish the script, and you should see two new files in the `images` folder: 72 | * `figure-1024-stipple.png` which is a stippled version of your original image, and 73 | * `figure-1024-stipple.tsp` which is a record of the coordinates of each of the points. This is the file we need for the next step. 74 | 75 | Note: `1024` refers to the number of dots used, assuming you use the initial settings as given. If you change this number, the resulting filenames will also have their numbers changed. This is to make it easier to experiment with different numbers of dots without constantly having to delete the old files. 76 | 77 | ## 3. Acquiring & Drawing the TSP Solution 78 | 79 | ### Using OR-Tools in Python 80 | 81 | Open `draw-tsp-path.py` in your editor, and change the variables as follows: 82 | 83 | * `ORIGINAL_IMAGE` should be the stippled image you obtained for Step 2: `images/figure-1024-stipple.png` 84 | * `IMAGE_TSP` should refer to the stipple `.tsp` file that is generated after Step 2: `images/figure-1024-stipple.tsp` 85 | 86 | Run the file, wait for Python to do its job and when it's done, the final image will be generated as `images/figure-1024-tsp.png`. 87 | 88 | ### Using Concorde (Windows GUI) 89 | 90 | Open Concorde either by double clicking on `figure-1024-stipple.tsp` or opening the program separately and then loading `figure-1024-stipple.tsp` file into it. Concorde should then display a series of dots that should resemble what you see in `figure-1024-stipple.png`. 91 | 92 | In the menu, click on 'Heuristics', select 'Lin Kernighan', then click OK. Concorde will then generate a tour that goes through all the points and returns to the starting point. 93 | 94 | Save the tour as a file by selecting in the menu: `File > Save Tour`. In our example we'll save it as `figure-tour.cyc`. 95 | 96 | Open `draw-tsp-path-concorde.py` in your editor and change the filenames at the top of the file 97 | 98 | * `ORIGINAL_IMAGE` should be the same initial image you used for Step 2: `images/figure-1024-stipple.png` 99 | * `IMAGE_TSP` should refer to the stipple `.tsp` file that is generated after Step 2: `images/figure-1024-stipple.tsp` 100 | * `IMAGE_CYC` should refer to the `.cyc` file that is generated from Concorde: `images/figure-tour.cyc` 101 | 102 | Run the file and the script should generate the final image at `images/figure-1024-tsp.png`. 103 | 104 | #### When do I use Concorde over OR-Tools? 105 | 106 | Generally speaking you'll only want to use Concorde over OR-Tools if you have an image that has 'gaps' where you don't want a path to cross. Sometimes the OR-Tools algorithm may result in paths that 'cross-over' areas where you don't want them to, whereas the Concorde solver is less likely to achieve such a result. 107 | 108 | To demonstrate as an example, in the `example-output` folder we have a `smileyface-inverted.png`: 109 | 110 |
111 | 112 | Now compare the two results (look closely at the mouth to see the difference): 113 | 114 | Python | Concorde 115 | :-----:|:--------: 116 | ![](/example-output/smileyface-inverted-1024-tsp%20%28Python%29.png) | ![](/example-output/smileyface-inverted-1024-tsp%20%28Concorde%29.png) 117 | 118 | # Acknowledgements 119 | 120 | Robert (Bob) Bosch ([Website](http://www.dominoartwork.com/), [Twitter](https://twitter.com/baabbaash/)) for the [original TSP art idea](http://www2.oberlin.edu/math/faculty/bosch/tspart-page.html). He's also written a book [Opt Art](https://www.amazon.com/Opt-Art-Mathematical-Optimization-Visual/dp/0691164061) which I highly recommend! 121 | 122 | Adrian Secord ([Twitter](https://twitter.com/ajsecord)) for the weighted voronoi stippling algorithm. Nicholas Rougier ([Website](https://www.labri.fr/perso/nrougier/), [Twitter](https://twitter.com/NPRougier), [Github](https://github.com/rougier)) for the Python implementation. 123 | 124 | See the Collection of Reference Links below for what I've found in my research while putting together this project. 125 | 126 | # Licenses 127 | 128 | * Google's OR-Tools is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) - I believe that means you can still use the library, but if you modify any of its internal code for usage, then you'll have to abide by the license terms. 129 | * [Concorde TSP Solver](http://www.math.uwaterloo.ca/tsp/concorde.html) is available for academic research use. 130 | * All code in the `weighted-voronoi-stippler` folder is obtained from https://github.com/ReScience-Archives/Rougier-2017, which has its own license: see `/weighted-voronoi-stippler/LICENSE.txt` for details. 131 | * My code: `draw-tsp-path.py`, `draw-tsp-path-concorde.py` and `stippling.py` is licensed under [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/) - basically if you use/remix my code, just make sure my name is in there somewhere for credit! 132 | * Images: The images in `images` and `example-output` are under [MIT](https://opensource.org/licenses/MIT) - feel free to use/remix/modify them without attribution. Any images you produce using my code, by my understanding, should fall under any licences they're under that involve any kind of remixing/modification. 133 | 134 | # Collection of Reference Links 135 | 136 | [Robert (Bob) Bosch's TSP Art Website](http://www2.oberlin.edu/math/faculty/bosch/tspart-page.html) 137 | 138 | [Robert Bosch's Outline of the TSPArt Creation Process](http://www2.oberlin.edu/math/faculty/bosch/making-tspart-page.html) 139 | 140 | [EvilMadScientist - StippleGen](https://www.evilmadscientist.com/2012/stipplegen-weighted-voronoi-stippling-and-tsp-paths-in-processing/) 141 | 142 | [EvilMadScientist - Generating TSP art from a stippled image](https://wiki.evilmadscientist.com/Generating_TSP_art_from_a_stippled_image) 143 | 144 | [Grant Trebbin - Voronoi Stippling](https://www.grant-trebbin.com/2017/02/voronoi-stippling.html) 145 | 146 | [Adrian Secord's pre-print paper on the Weighted Voronoi Stippling Algorithm](https://mrl.nyu.edu/~ajsecord/npar2002/npar2002_ajsecord_preprint.pdf) 147 | 148 | [Weighted Voronoi Stippling Repo that implements Adrian Secord's Weighted Voronoi Stippling Algorithm](https://github.com/ReScience-Archives/Rougier-2017) 149 | 150 | [Jack Morris - Creating Travelling Salesman Art with Weighted Voronoi Stippling](http://jackxmorris.com/posts/traveling-salesman-art) 151 | 152 | [Craig S. Kaplan - TSP Art](http://www.cgl.uwaterloo.ca/csk/projects/tsp/) 153 | 154 | [OR-Tools & TSP](https://developers.google.com/optimization/routing/tsp) -------------------------------------------------------------------------------- /TSPArt-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthras/tsp-art-python/e1ad6a5ae342171ca1571a732c48e33239397fd5/TSPArt-logo.png -------------------------------------------------------------------------------- /draw-tsp-path-concorde.py: -------------------------------------------------------------------------------- 1 | # Copyright Matthew Mack (c) 2020 under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/ 2 | from PIL import Image, ImageDraw 3 | import os.path 4 | 5 | ORIGINAL_IMAGE = "images/smileyface-inverted.png" 6 | IMAGE_TSP = "images/smileyface-inverted-1024-stipple.tsp" 7 | IMAGE_CYC = "images/smileyface-inverted-1024-stipple.cyc" 8 | 9 | list_of_nodes = [] 10 | 11 | with open(IMAGE_TSP) as f: 12 | for _ in range(6): 13 | next(f) 14 | for line in f: 15 | i,x,y = line.split() 16 | list_of_nodes.append((int(float(x)),int(float(y)))) 17 | 18 | tsp_path = [] 19 | 20 | with open(IMAGE_CYC) as g: 21 | for line in g: 22 | tsp_path.append(list_of_nodes[int(line)]) 23 | tsp_path.append(list_of_nodes[0]) 24 | 25 | original_image = Image.open(ORIGINAL_IMAGE) 26 | width, height = original_image.size 27 | 28 | tsp_image = Image.new("RGBA",(width,height),color='white') 29 | tsp_image_draw = ImageDraw.Draw(tsp_image) 30 | #tsp_image_draw.point(tsp_path,fill='black') 31 | tsp_image_draw.line(tsp_path,fill='black',width=1) 32 | tsp_image = tsp_image.transpose(Image.FLIP_TOP_BOTTOM) 33 | FINAL_IMAGE = IMAGE_TSP.replace("-stipple.tsp", "-tsp.png") 34 | tsp_image.save(FINAL_IMAGE) 35 | print("TSP solution has been drawn and can be viewed at", FINAL_IMAGE) -------------------------------------------------------------------------------- /draw-tsp-path-svg.py: -------------------------------------------------------------------------------- 1 | import cairo 2 | from PIL import Image 3 | 4 | # Change these file names to the relevant files. 5 | ORIGINAL_IMAGE = "example-output/smileyface-inverted.png" 6 | IMAGE_TSP = "example-output/smileyface-inverted-1024-stipple.tsp" 7 | IMAGE_CYC = "example-output/smileyface-inverted-1024-stipple.cyc" 8 | 9 | def obtain_list_of_nodes(): 10 | list_of_nodes = [] 11 | with open(IMAGE_TSP) as f: 12 | for _ in range(6): 13 | next(f) 14 | for line in f: 15 | i,x,y = line.split() 16 | list_of_nodes.append((int(float(x)),int(float(y)))) 17 | return list_of_nodes 18 | 19 | def obtain_concorde_solution(list_of_nodes): 20 | tsp_path = [] 21 | with open(IMAGE_CYC) as g: 22 | for line in g: 23 | tsp_path.append(list_of_nodes[int(line)]) 24 | tsp_path.append(list_of_nodes[0]) # The .cyc file does not include the starting node 25 | return tsp_path 26 | 27 | def draw_svg(tsp_path): 28 | im = Image.open(ORIGINAL_IMAGE) 29 | WIDTH, HEIGHT = im.size 30 | FINAL_IMAGE_SVG = IMAGE_TSP.replace("-stipple.tsp", "-tsp.svg") 31 | #surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,WIDTH,HEIGHT) 32 | surface = cairo.SVGSurface(FINAL_IMAGE_SVG,WIDTH,HEIGHT) 33 | ctx = cairo.Context(surface) 34 | #ctx.scale(WIDTH,HEIGHT) # Not sure about this, seems redundant if image dimensions are already specified by surface 35 | # Transform to normal cartesian coordinate system 36 | m = cairo.Matrix(yy=-1, y0=HEIGHT) 37 | ctx.transform(m) 38 | # Paint the background white 39 | ctx.save() 40 | ctx.set_source_rgb(1,1,1) 41 | ctx.paint() 42 | ctx.restore() 43 | # Draw lines 44 | ctx.move_to(tsp_path[0][0], tsp_path[0][1]) 45 | for node in range(1,len(tsp_path)): 46 | ctx.line_to(tsp_path[node][0],tsp_path[node][1]) 47 | ctx.save() 48 | ctx.set_source_rgb(0,0,0) 49 | ctx.set_line_width(1) 50 | ctx.stroke_preserve() 51 | ctx.restore() 52 | #FINAL_IMAGE_PNG = IMAGE_TSP.replace("-stipple.tsp", "-tsp.png") 53 | #surface.write_to_png(FINAL_IMAGE) 54 | surface.finish() 55 | 56 | def main(): 57 | list_of_nodes = obtain_list_of_nodes() 58 | tsp_path = obtain_concorde_solution(list_of_nodes) # Later change to if-no-concorde, then use OR-tools 59 | draw_svg(tsp_path) 60 | 61 | if __name__ == '__main__': 62 | main() -------------------------------------------------------------------------------- /draw-tsp-path.py: -------------------------------------------------------------------------------- 1 | """Modified code from https://developers.google.com/optimization/routing/tsp#or-tools """ 2 | # Copyright Matthew Mack (c) 2020 under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/ 3 | 4 | from __future__ import print_function 5 | import math 6 | from ortools.constraint_solver import routing_enums_pb2 7 | from ortools.constraint_solver import pywrapcp 8 | from PIL import Image, ImageDraw 9 | import os 10 | 11 | # Change these file names to the relevant files. 12 | ORIGINAL_IMAGE = "images/smileyface-inverted.png" 13 | IMAGE_TSP = "images/smileyface-inverted-1024-stipple.tsp" 14 | 15 | def create_data_model(): 16 | """Stores the data for the problem.""" 17 | # Extracts coordinates from IMAGE_TSP and puts them into an array 18 | list_of_nodes = [] 19 | with open(IMAGE_TSP) as f: 20 | for _ in range(6): 21 | next(f) 22 | for line in f: 23 | i,x,y = line.split() 24 | list_of_nodes.append((int(float(x)),int(float(y)))) 25 | data = {} 26 | # Locations in block units 27 | data['locations'] = list_of_nodes # yapf: disable 28 | data['num_vehicles'] = 1 29 | data['depot'] = 0 30 | return data 31 | 32 | def compute_euclidean_distance_matrix(locations): 33 | """Creates callback to return distance between points.""" 34 | distances = {} 35 | for from_counter, from_node in enumerate(locations): 36 | distances[from_counter] = {} 37 | for to_counter, to_node in enumerate(locations): 38 | if from_counter == to_counter: 39 | distances[from_counter][to_counter] = 0 40 | else: 41 | # Euclidean distance 42 | distances[from_counter][to_counter] = (int( 43 | math.hypot((from_node[0] - to_node[0]), 44 | (from_node[1] - to_node[1])))) 45 | return distances 46 | 47 | def print_solution(manager, routing, solution): 48 | """Prints solution on console.""" 49 | print('Objective: {}'.format(solution.ObjectiveValue())) 50 | index = routing.Start(0) 51 | plan_output = 'Route:\n' 52 | route_distance = 0 53 | while not routing.IsEnd(index): 54 | plan_output += ' {} ->'.format(manager.IndexToNode(index)) 55 | previous_index = index 56 | index = solution.Value(routing.NextVar(index)) 57 | route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) 58 | plan_output += ' {}\n'.format(manager.IndexToNode(index)) 59 | print(plan_output) 60 | plan_output += 'Objective: {}m\n'.format(route_distance) 61 | 62 | def get_routes(solution, routing, manager): 63 | """Get vehicle routes from a solution and store them in an array.""" 64 | # Get vehicle routes and store them in a two dimensional array whose 65 | # i,j entry is the jth location visited by vehicle i along its route. 66 | routes = [] 67 | for route_nbr in range(routing.vehicles()): 68 | index = routing.Start(route_nbr) 69 | route = [manager.IndexToNode(index)] 70 | while not routing.IsEnd(index): 71 | index = solution.Value(routing.NextVar(index)) 72 | route.append(manager.IndexToNode(index)) 73 | routes.append(route) 74 | return routes[0] 75 | 76 | def draw_routes(nodes, path): 77 | """Takes a set of nodes and a path, and outputs an image of the drawn TSP path""" 78 | tsp_path = [] 79 | for location in path: 80 | tsp_path.append(nodes[int(location)]) 81 | 82 | original_image = Image.open(ORIGINAL_IMAGE) 83 | width, height = original_image.size 84 | 85 | tsp_image = Image.new("RGBA",(width,height),color='white') 86 | tsp_image_draw = ImageDraw.Draw(tsp_image) 87 | #tsp_image_draw.point(tsp_path,fill='black') 88 | tsp_image_draw.line(tsp_path,fill='black',width=1) 89 | tsp_image = tsp_image.transpose(Image.FLIP_TOP_BOTTOM) 90 | FINAL_IMAGE = IMAGE_TSP.replace("-stipple.tsp","-tsp.png") 91 | tsp_image.save(FINAL_IMAGE) 92 | print("TSP solution has been drawn and can be viewed at", FINAL_IMAGE) 93 | 94 | def main(): 95 | """Entry point of the program.""" 96 | # Instantiate the data problem. 97 | print("Step 1/5: Initialising variables") 98 | data = create_data_model() 99 | 100 | # Create the routing index manager. 101 | manager = pywrapcp.RoutingIndexManager(len(data['locations']), 102 | data['num_vehicles'], data['depot']) 103 | 104 | # Create Routing Model. 105 | routing = pywrapcp.RoutingModel(manager) 106 | print("Step 2/5: Computing distance matrix") 107 | distance_matrix = compute_euclidean_distance_matrix(data['locations']) 108 | 109 | def distance_callback(from_index, to_index): 110 | """Returns the distance between the two nodes.""" 111 | # Convert from routing variable Index to distance matrix NodeIndex. 112 | from_node = manager.IndexToNode(from_index) 113 | to_node = manager.IndexToNode(to_index) 114 | return distance_matrix[from_node][to_node] 115 | 116 | transit_callback_index = routing.RegisterTransitCallback(distance_callback) 117 | 118 | # Define cost of each arc. 119 | routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) 120 | 121 | # Setting first solution heuristic. 122 | print("Step 3/5: Setting an initial solution") 123 | search_parameters = pywrapcp.DefaultRoutingSearchParameters() 124 | search_parameters.first_solution_strategy = ( 125 | routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC) 126 | 127 | # Solve the problem. 128 | print("Step 4/5: Solving") 129 | solution = routing.SolveWithParameters(search_parameters) 130 | 131 | # Print solution on console. 132 | if solution: 133 | #print_solution(manager, routing, solution) 134 | print("Step 5/5: Drawing the solution") 135 | routes = get_routes(solution, routing, manager) 136 | draw_routes(data['locations'],routes) 137 | else: 138 | print("A solution couldn't be found :(") 139 | 140 | 141 | if __name__ == '__main__': 142 | main() -------------------------------------------------------------------------------- /example-output/smileyface-inverted-1024-stipple.cyc: -------------------------------------------------------------------------------- 1 | 0 2 | 19 3 | 17 4 | 519 5 | 720 6 | 718 7 | 273 8 | 91 9 | 92 10 | 21 11 | 18 12 | 20 13 | 43 14 | 49 15 | 670 16 | 668 17 | 672 18 | 154 19 | 985 20 | 1008 21 | 759 22 | 1009 23 | 766 24 | 767 25 | 761 26 | 760 27 | 155 28 | 193 29 | 190 30 | 194 31 | 185 32 | 57 33 | 224 34 | 2 35 | 3 36 | 4 37 | 225 38 | 220 39 | 221 40 | 160 41 | 222 42 | 223 43 | 159 44 | 219 45 | 218 46 | 226 47 | 217 48 | 214 49 | 215 50 | 455 51 | 453 52 | 454 53 | 56 54 | 208 55 | 207 56 | 216 57 | 833 58 | 251 59 | 55 60 | 54 61 | 247 62 | 252 63 | 500 64 | 499 65 | 836 66 | 831 67 | 832 68 | 456 69 | 452 70 | 695 71 | 834 72 | 835 73 | 488 74 | 503 75 | 502 76 | 498 77 | 497 78 | 922 79 | 923 80 | 920 81 | 926 82 | 696 83 | 925 84 | 697 85 | 698 86 | 662 87 | 261 88 | 257 89 | 260 90 | 78 91 | 259 92 | 258 93 | 516 94 | 517 95 | 77 96 | 337 97 | 336 98 | 494 99 | 496 100 | 515 101 | 511 102 | 514 103 | 269 104 | 267 105 | 256 106 | 268 107 | 262 108 | 253 109 | 661 110 | 501 111 | 504 112 | 254 113 | 255 114 | 458 115 | 693 116 | 694 117 | 459 118 | 213 119 | 59 120 | 462 121 | 88 122 | 36 123 | 40 124 | 41 125 | 186 126 | 191 127 | 42 128 | 37 129 | 44 130 | 415 131 | 192 132 | 416 133 | 671 134 | 669 135 | 47 136 | 38 137 | 39 138 | 87 139 | 90 140 | 265 141 | 461 142 | 457 143 | 460 144 | 264 145 | 266 146 | 270 147 | 513 148 | 512 149 | 76 150 | 248 151 | 16 152 | 15 153 | 14 154 | 89 155 | 13 156 | 12 157 | 46 158 | 45 159 | 48 160 | 11 161 | 23 162 | 22 163 | 249 164 | 85 165 | 86 166 | 117 167 | 250 168 | 491 169 | 490 170 | 495 171 | 489 172 | 493 173 | 492 174 | 335 175 | 118 176 | 343 177 | 119 178 | 612 179 | 611 180 | 274 181 | 271 182 | 272 183 | 719 184 | 928 185 | 929 186 | 927 187 | 977 188 | 980 189 | 979 190 | 974 191 | 975 192 | 976 193 | 847 194 | 930 195 | 850 196 | 851 197 | 933 198 | 849 199 | 978 200 | 721 201 | 934 202 | 936 203 | 932 204 | 937 205 | 749 206 | 748 207 | 751 208 | 752 209 | 753 210 | 747 211 | 754 212 | 755 213 | 554 214 | 750 215 | 722 216 | 723 217 | 988 218 | 987 219 | 871 220 | 959 221 | 989 222 | 986 223 | 990 224 | 116 225 | 327 226 | 875 227 | 873 228 | 991 229 | 992 230 | 952 231 | 953 232 | 960 233 | 961 234 | 958 235 | 963 236 | 957 237 | 779 238 | 877 239 | 870 240 | 872 241 | 955 242 | 954 243 | 876 244 | 757 245 | 758 246 | 349 247 | 756 248 | 888 249 | 890 250 | 889 251 | 887 252 | 894 253 | 895 254 | 778 255 | 892 256 | 891 257 | 893 258 | 1010 259 | 332 260 | 1013 261 | 599 262 | 600 263 | 601 264 | 589 265 | 587 266 | 590 267 | 583 268 | 584 269 | 329 270 | 780 271 | 585 272 | 586 273 | 956 274 | 563 275 | 964 276 | 962 277 | 844 278 | 560 279 | 561 280 | 596 281 | 597 282 | 593 283 | 595 284 | 562 285 | 591 286 | 594 287 | 598 288 | 609 289 | 606 290 | 610 291 | 342 292 | 592 293 | 588 294 | 331 295 | 579 296 | 1011 297 | 1012 298 | 578 299 | 581 300 | 885 301 | 884 302 | 886 303 | 328 304 | 582 305 | 781 306 | 782 307 | 784 308 | 906 309 | 911 310 | 909 311 | 910 312 | 817 313 | 818 314 | 1000 315 | 904 316 | 651 317 | 650 318 | 413 319 | 605 320 | 701 321 | 508 322 | 699 323 | 700 324 | 510 325 | 509 326 | 507 327 | 603 328 | 602 329 | 965 330 | 967 331 | 1015 332 | 1021 333 | 1001 334 | 1023 335 | 912 336 | 907 337 | 783 338 | 655 339 | 654 340 | 348 341 | 340 342 | 330 343 | 334 344 | 580 345 | 333 346 | 341 347 | 339 348 | 346 349 | 345 350 | 614 351 | 347 352 | 608 353 | 607 354 | 344 355 | 613 356 | 658 357 | 659 358 | 819 359 | 1014 360 | 656 361 | 657 362 | 653 363 | 652 364 | 908 365 | 1022 366 | 905 367 | 1020 368 | 1018 369 | 1019 370 | 1016 371 | 1017 372 | 966 373 | 996 374 | 994 375 | 338 376 | 995 377 | 993 378 | 997 379 | 998 380 | 604 381 | 999 382 | 83 383 | 263 384 | 714 385 | 713 386 | 506 387 | 84 388 | 81 389 | 82 390 | 80 391 | 79 392 | 505 393 | 660 394 | 414 395 | 821 396 | 916 397 | 917 398 | 924 399 | 921 400 | 838 401 | 474 402 | 473 403 | 479 404 | 477 405 | 475 406 | 478 407 | 232 408 | 485 409 | 487 410 | 65 411 | 67 412 | 74 413 | 73 414 | 71 415 | 75 416 | 10 417 | 231 418 | 472 419 | 471 420 | 470 421 | 469 422 | 468 423 | 465 424 | 464 425 | 467 426 | 663 427 | 690 428 | 666 429 | 667 430 | 664 431 | 665 432 | 837 433 | 919 434 | 918 435 | 822 436 | 689 437 | 688 438 | 827 439 | 825 440 | 826 441 | 820 442 | 823 443 | 824 444 | 971 445 | 913 446 | 969 447 | 914 448 | 915 449 | 708 450 | 970 451 | 968 452 | 972 453 | 830 454 | 973 455 | 522 456 | 705 457 | 703 458 | 711 459 | 709 460 | 712 461 | 710 462 | 704 463 | 707 464 | 706 465 | 702 466 | 725 467 | 724 468 | 726 469 | 828 470 | 412 471 | 829 472 | 463 473 | 691 474 | 229 475 | 227 476 | 692 477 | 230 478 | 466 479 | 228 480 | 5 481 | 69 482 | 72 483 | 68 484 | 70 485 | 66 486 | 63 487 | 233 488 | 64 489 | 61 490 | 62 491 | 60 492 | 242 493 | 241 494 | 245 495 | 7 496 | 244 497 | 240 498 | 243 499 | 246 500 | 481 501 | 482 502 | 483 503 | 480 504 | 484 505 | 486 506 | 476 507 | 9 508 | 8 509 | 204 510 | 235 511 | 236 512 | 234 513 | 53 514 | 203 515 | 212 516 | 206 517 | 210 518 | 205 519 | 432 520 | 426 521 | 425 522 | 429 523 | 430 524 | 424 525 | 1 526 | 158 527 | 423 528 | 422 529 | 431 530 | 157 531 | 151 532 | 161 533 | 149 534 | 150 535 | 52 536 | 51 537 | 152 538 | 175 539 | 437 540 | 438 541 | 439 542 | 176 543 | 156 544 | 162 545 | 428 546 | 427 547 | 211 548 | 209 549 | 418 550 | 419 551 | 6 552 | 421 553 | 417 554 | 420 555 | 435 556 | 682 557 | 683 558 | 681 559 | 685 560 | 686 561 | 680 562 | 445 563 | 239 564 | 237 565 | 238 566 | 167 567 | 163 568 | 166 569 | 165 570 | 164 571 | 444 572 | 442 573 | 441 574 | 443 575 | 676 576 | 675 577 | 677 578 | 177 579 | 168 580 | 169 581 | 172 582 | 173 583 | 184 584 | 678 585 | 679 586 | 433 587 | 449 588 | 446 589 | 448 590 | 674 591 | 673 592 | 687 593 | 684 594 | 434 595 | 440 596 | 436 597 | 447 598 | 174 599 | 395 600 | 394 601 | 278 602 | 631 603 | 636 604 | 902 605 | 634 606 | 790 607 | 900 608 | 899 609 | 896 610 | 897 611 | 801 612 | 799 613 | 796 614 | 798 615 | 642 616 | 805 617 | 807 618 | 380 619 | 378 620 | 643 621 | 640 622 | 797 623 | 791 624 | 793 625 | 795 626 | 630 627 | 788 628 | 794 629 | 789 630 | 785 631 | 787 632 | 786 633 | 635 634 | 633 635 | 637 636 | 393 637 | 392 638 | 401 639 | 639 640 | 397 641 | 399 642 | 403 643 | 638 644 | 374 645 | 628 646 | 144 647 | 146 648 | 145 649 | 148 650 | 147 651 | 398 652 | 402 653 | 400 654 | 396 655 | 451 656 | 179 657 | 450 658 | 183 659 | 170 660 | 171 661 | 50 662 | 182 663 | 178 664 | 180 665 | 181 666 | 32 667 | 33 668 | 34 669 | 31 670 | 143 671 | 141 672 | 142 673 | 372 674 | 627 675 | 629 676 | 626 677 | 625 678 | 373 679 | 375 680 | 376 681 | 792 682 | 641 683 | 136 684 | 377 685 | 137 686 | 138 687 | 383 688 | 379 689 | 808 690 | 802 691 | 804 692 | 803 693 | 649 694 | 811 695 | 814 696 | 647 697 | 806 698 | 809 699 | 381 700 | 30 701 | 140 702 | 391 703 | 734 704 | 738 705 | 741 706 | 139 707 | 387 708 | 644 709 | 386 710 | 407 711 | 409 712 | 408 713 | 382 714 | 384 715 | 406 716 | 404 717 | 405 718 | 35 719 | 93 720 | 95 721 | 283 722 | 385 723 | 645 724 | 740 725 | 736 726 | 737 727 | 950 728 | 946 729 | 951 730 | 943 731 | 860 732 | 859 733 | 733 734 | 735 735 | 648 736 | 732 737 | 815 738 | 388 739 | 940 740 | 941 741 | 856 742 | 857 743 | 942 744 | 939 745 | 944 746 | 945 747 | 858 748 | 533 749 | 727 750 | 529 751 | 528 752 | 531 753 | 532 754 | 527 755 | 525 756 | 524 757 | 188 758 | 198 759 | 197 760 | 281 761 | 530 762 | 282 763 | 280 764 | 279 765 | 275 766 | 277 767 | 389 768 | 816 769 | 810 770 | 813 771 | 812 772 | 800 773 | 898 774 | 901 775 | 903 776 | 390 777 | 632 778 | 276 779 | 201 780 | 202 781 | 200 782 | 199 783 | 58 784 | 189 785 | 187 786 | 195 787 | 196 788 | 24 789 | 764 790 | 768 791 | 763 792 | 762 793 | 526 794 | 523 795 | 729 796 | 728 797 | 731 798 | 730 799 | 948 800 | 287 801 | 947 802 | 863 803 | 949 804 | 861 805 | 739 806 | 646 807 | 535 808 | 534 809 | 537 810 | 538 811 | 286 812 | 96 813 | 285 814 | 94 815 | 284 816 | 98 817 | 153 818 | 411 819 | 26 820 | 410 821 | 542 822 | 543 823 | 25 824 | 102 825 | 27 826 | 101 827 | 97 828 | 100 829 | 536 830 | 540 831 | 541 832 | 862 833 | 743 834 | 539 835 | 742 836 | 104 837 | 99 838 | 109 839 | 289 840 | 290 841 | 869 842 | 744 843 | 745 844 | 746 845 | 623 846 | 351 847 | 622 848 | 615 849 | 617 850 | 621 851 | 619 852 | 352 853 | 616 854 | 618 855 | 624 856 | 371 857 | 354 858 | 355 859 | 360 860 | 356 861 | 361 862 | 362 863 | 122 864 | 123 865 | 357 866 | 124 867 | 133 868 | 131 869 | 134 870 | 301 871 | 358 872 | 359 873 | 305 874 | 304 875 | 306 876 | 303 877 | 302 878 | 114 879 | 115 880 | 299 881 | 300 882 | 29 883 | 135 884 | 112 885 | 113 886 | 553 887 | 549 888 | 552 889 | 777 890 | 573 891 | 575 892 | 577 893 | 298 894 | 297 895 | 576 896 | 572 897 | 574 898 | 324 899 | 370 900 | 321 901 | 320 902 | 776 903 | 772 904 | 773 905 | 323 906 | 322 907 | 120 908 | 350 909 | 558 910 | 874 911 | 559 912 | 326 913 | 325 914 | 774 915 | 775 916 | 571 917 | 548 918 | 550 919 | 551 920 | 316 921 | 317 922 | 318 923 | 315 924 | 319 925 | 308 926 | 309 927 | 307 928 | 311 929 | 310 930 | 132 931 | 126 932 | 125 933 | 367 934 | 363 935 | 364 936 | 368 937 | 369 938 | 121 939 | 353 940 | 294 941 | 295 942 | 107 943 | 293 944 | 291 945 | 620 946 | 865 947 | 866 948 | 867 949 | 864 950 | 868 951 | 292 952 | 111 953 | 103 954 | 105 955 | 110 956 | 288 957 | 106 958 | 108 959 | 28 960 | 130 961 | 129 962 | 564 963 | 568 964 | 771 965 | 769 966 | 879 967 | 770 968 | 567 969 | 565 970 | 365 971 | 366 972 | 127 973 | 128 974 | 566 975 | 570 976 | 881 977 | 569 978 | 296 979 | 312 980 | 313 981 | 547 982 | 545 983 | 544 984 | 546 985 | 314 986 | 555 987 | 556 988 | 557 989 | 853 990 | 854 991 | 846 992 | 845 993 | 938 994 | 935 995 | 855 996 | 852 997 | 882 998 | 878 999 | 880 1000 | 765 1001 | 848 1002 | 883 1003 | 983 1004 | 982 1005 | 981 1006 | 1006 1007 | 1007 1008 | 1002 1009 | 1003 1010 | 931 1011 | 1004 1012 | 1005 1013 | 984 1014 | 840 1015 | 841 1016 | 716 1017 | 842 1018 | 839 1019 | 843 1020 | 521 1021 | 520 1022 | 518 1023 | 715 1024 | 717 1025 | -------------------------------------------------------------------------------- /example-output/smileyface-inverted-1024-stipple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthras/tsp-art-python/e1ad6a5ae342171ca1571a732c48e33239397fd5/example-output/smileyface-inverted-1024-stipple.png -------------------------------------------------------------------------------- /example-output/smileyface-inverted-1024-stipple.tsp: -------------------------------------------------------------------------------- 1 | NAME : sample-images/smileyface-inverted.png 2 | TYPE : TSP 3 | COMMENT: Stipple of sample-images/smileyface-inverted.png with 1024 points 4 | DIMENSION: 1024 5 | EDGE_WEIGHT_TYPE: ATT 6 | NODE_COORD_SECTION 7 | 1 311 266 8 | 2 489 374 9 | 3 401 371 10 | 4 410 354 11 | 5 425 346 12 | 6 649 200 13 | 7 619 368 14 | 8 626 354 15 | 9 568 323 16 | 10 578 312 17 | 11 620 258 18 | 12 349 264 19 | 13 380 287 20 | 14 368 270 21 | 15 381 253 22 | 16 364 250 23 | 17 357 234 24 | 18 290 248 25 | 19 325 243 26 | 20 308 249 27 | 21 328 259 28 | 22 318 228 29 | 23 338 231 30 | 24 344 248 31 | 25 320 399 32 | 26 176 601 33 | 27 171 618 34 | 28 194 613 35 | 29 191 492 36 | 30 42 405 37 | 31 365 641 38 | 32 608 567 39 | 33 637 532 40 | 34 627 542 41 | 35 619 556 42 | 36 316 675 43 | 37 414 331 44 | 38 378 331 45 | 39 379 308 46 | 40 396 321 47 | 41 397 339 48 | 42 388 353 49 | 43 368 351 50 | 44 330 275 51 | 45 360 332 52 | 46 346 300 53 | 47 364 292 54 | 48 362 313 55 | 49 348 281 56 | 50 327 291 57 | 51 643 517 58 | 52 527 473 59 | 53 527 457 60 | 54 610 352 61 | 55 552 323 62 | 56 538 315 63 | 57 518 360 64 | 58 406 391 65 | 59 410 410 66 | 60 434 330 67 | 61 685 355 68 | 62 681 316 69 | 63 685 335 70 | 64 674 285 71 | 65 682 299 72 | 66 654 282 73 | 67 675 269 74 | 68 659 268 75 | 69 665 236 76 | 70 656 217 77 | 71 670 253 78 | 72 642 245 79 | 73 650 231 80 | 74 654 251 81 | 75 640 265 82 | 76 629 247 83 | 77 378 233 84 | 78 414 194 85 | 79 431 181 86 | 80 480 164 87 | 81 463 154 88 | 82 443 128 89 | 83 447 147 90 | 84 452 110 91 | 85 462 130 92 | 86 334 214 93 | 87 320 208 94 | 88 398 303 95 | 89 415 313 96 | 90 388 269 97 | 91 396 286 98 | 92 298 232 99 | 93 305 217 100 | 94 296 673 101 | 95 218 647 102 | 96 276 668 103 | 97 256 663 104 | 98 227 608 105 | 99 213 623 106 | 100 209 582 107 | 101 229 588 108 | 102 209 602 109 | 103 192 593 110 | 104 208 560 111 | 105 223 570 112 | 106 218 544 113 | 107 197 519 114 | 108 185 508 115 | 109 204 502 116 | 110 192 573 117 | 111 199 540 118 | 112 190 556 119 | 113 54 393 120 | 114 68 381 121 | 115 86 408 122 | 116 71 400 123 | 117 129 272 124 | 118 323 192 125 | 119 328 177 126 | 120 304 199 127 | 121 52 257 128 | 122 136 498 129 | 123 118 497 130 | 124 124 481 131 | 125 120 459 132 | 126 136 468 133 | 127 138 449 134 | 128 161 443 135 | 129 163 426 136 | 130 189 465 137 | 131 202 478 138 | 132 119 427 139 | 133 140 433 140 | 134 117 443 141 | 135 103 417 142 | 136 39 384 143 | 137 465 665 144 | 138 441 672 145 | 139 424 676 146 | 140 351 628 147 | 141 370 623 148 | 142 586 588 149 | 143 576 603 150 | 144 600 580 151 | 145 569 585 152 | 146 588 567 153 | 147 571 568 154 | 148 611 537 155 | 149 600 551 156 | 150 551 437 157 | 151 543 453 158 | 152 526 441 159 | 153 542 475 160 | 154 200 637 161 | 155 309 357 162 | 156 314 376 163 | 157 572 407 164 | 158 519 426 165 | 159 483 389 166 | 160 465 390 167 | 161 429 418 168 | 162 538 426 169 | 163 559 420 170 | 164 666 386 171 | 165 683 394 172 | 166 685 375 173 | 167 670 368 174 | 168 653 374 175 | 169 658 463 176 | 170 668 472 177 | 171 639 491 178 | 172 651 503 179 | 173 659 488 180 | 174 649 476 181 | 175 574 481 182 | 176 555 467 183 | 177 577 423 184 | 178 672 451 185 | 179 614 505 186 | 180 596 503 187 | 181 605 519 188 | 182 623 522 189 | 183 631 506 190 | 184 622 489 191 | 185 632 475 192 | 186 389 388 193 | 187 381 370 194 | 188 374 410 195 | 189 374 430 196 | 190 392 406 197 | 191 350 385 198 | 192 364 371 199 | 193 347 363 200 | 194 332 378 201 | 195 370 390 202 | 196 356 405 203 | 197 336 401 204 | 198 401 445 205 | 199 392 426 206 | 200 410 426 207 | 201 421 438 208 | 202 413 474 209 | 203 415 457 210 | 204 592 353 211 | 205 582 334 212 | 206 546 352 213 | 207 562 341 214 | 208 542 335 215 | 209 529 347 216 | 210 567 374 217 | 211 559 360 218 | 212 573 389 219 | 213 576 355 220 | 214 450 322 221 | 215 455 339 222 | 216 467 326 223 | 217 524 328 224 | 218 443 350 225 | 219 463 355 226 | 220 470 372 227 | 221 435 382 228 | 222 425 398 229 | 223 443 400 230 | 224 452 384 231 | 225 418 378 232 | 226 428 364 233 | 227 449 367 234 | 228 628 171 235 | 229 638 186 236 | 230 619 156 237 | 231 614 186 238 | 232 608 246 239 | 233 622 273 240 | 234 664 301 241 | 235 619 336 242 | 236 596 318 243 | 237 601 335 244 | 238 632 391 245 | 239 635 374 246 | 240 648 390 247 | 241 652 338 248 | 242 657 357 249 | 243 670 348 250 | 244 669 331 251 | 245 636 341 252 | 246 642 357 253 | 247 658 319 254 | 248 556 304 255 | 249 367 220 256 | 250 350 216 257 | 251 341 196 258 | 252 533 299 259 | 253 547 288 260 | 254 460 236 261 | 255 458 269 262 | 256 471 276 263 | 257 451 250 264 | 258 451 206 265 | 259 435 214 266 | 260 432 198 267 | 261 449 189 268 | 262 466 198 269 | 263 450 224 270 | 264 469 109 271 | 265 426 275 272 | 266 413 292 273 | 267 408 273 274 | 268 443 264 275 | 269 439 237 276 | 270 433 253 277 | 271 418 259 278 | 272 269 218 279 | 273 252 211 280 | 274 278 233 281 | 275 287 212 282 | 276 402 503 283 | 277 416 491 284 | 278 401 520 285 | 279 527 489 286 | 280 394 487 287 | 281 395 465 288 | 282 383 447 289 | 283 379 477 290 | 284 286 652 291 | 285 231 633 292 | 286 239 653 293 | 287 266 648 294 | 288 305 523 295 | 289 211 526 296 | 290 176 581 297 | 291 173 562 298 | 292 166 531 299 | 293 178 544 300 | 294 183 527 301 | 295 155 517 302 | 296 170 513 303 | 297 170 386 304 | 298 38 363 305 | 299 54 370 306 | 300 61 413 307 | 301 44 423 308 | 302 98 433 309 | 303 80 424 310 | 304 64 432 311 | 305 52 456 312 | 306 67 450 313 | 307 49 440 314 | 308 141 399 315 | 309 118 407 316 | 310 132 415 317 | 311 149 416 318 | 312 161 402 319 | 313 153 383 320 | 314 136 381 321 | 315 141 364 322 | 316 107 381 323 | 317 90 373 324 | 318 86 388 325 | 319 102 398 326 | 320 122 390 327 | 321 62 286 328 | 322 47 276 329 | 323 65 267 330 | 324 79 274 331 | 325 53 305 332 | 326 106 286 333 | 327 98 273 334 | 328 115 271 335 | 329 240 72 336 | 330 137 139 337 | 331 242 92 338 | 332 190 116 339 | 333 147 126 340 | 334 217 124 341 | 335 227 105 342 | 336 345 174 343 | 337 396 172 344 | 338 413 176 345 | 339 427 142 346 | 340 248 132 347 | 341 248 111 348 | 342 234 125 349 | 343 227 143 350 | 344 308 181 351 | 345 295 146 352 | 346 278 112 353 | 347 264 122 354 | 348 279 134 355 | 349 261 98 356 | 350 65 223 357 | 351 58 240 358 | 352 100 547 359 | 353 122 515 360 | 354 140 515 361 | 355 65 486 362 | 356 59 471 363 | 357 92 469 364 | 358 109 471 365 | 359 100 452 366 | 360 83 445 367 | 361 76 466 368 | 362 83 485 369 | 363 103 488 370 | 364 162 479 371 | 365 180 479 372 | 366 171 461 373 | 367 153 461 374 | 368 144 482 375 | 369 172 494 376 | 370 154 499 377 | 371 42 295 378 | 372 70 501 379 | 373 561 615 380 | 374 514 643 381 | 375 553 567 382 | 376 498 652 383 | 377 481 656 384 | 378 451 660 385 | 379 433 656 386 | 380 399 661 387 | 381 416 661 388 | 382 381 649 389 | 383 383 665 390 | 384 405 677 391 | 385 388 679 392 | 386 305 655 393 | 387 324 658 394 | 388 339 643 395 | 389 389 551 396 | 390 399 535 397 | 391 420 527 398 | 392 370 604 399 | 393 559 502 400 | 394 543 509 401 | 395 543 493 402 | 396 558 484 403 | 397 577 498 404 | 398 573 532 405 | 399 593 533 406 | 400 582 548 407 | 401 572 513 408 | 402 557 520 409 | 403 587 517 410 | 404 565 550 411 | 405 353 677 412 | 406 334 678 413 | 407 370 679 414 | 408 340 663 415 | 409 367 661 416 | 410 353 654 417 | 411 157 609 418 | 412 184 629 419 | 413 594 122 420 | 414 438 41 421 | 415 498 174 422 | 416 347 345 423 | 417 328 356 424 | 418 601 387 425 | 419 585 374 426 | 420 602 369 427 | 421 588 396 428 | 422 617 384 429 | 423 510 410 430 | 424 499 396 431 | 425 505 377 432 | 426 537 391 433 | 427 551 378 434 | 428 557 396 435 | 429 545 409 436 | 430 518 394 437 | 431 523 378 438 | 432 528 410 439 | 433 536 367 440 | 434 623 460 441 | 435 593 429 442 | 436 591 413 443 | 437 581 454 444 | 438 570 466 445 | 439 562 451 446 | 440 570 437 447 | 441 587 442 448 | 442 667 419 449 | 443 682 414 450 | 444 677 433 451 | 445 670 402 452 | 446 655 404 453 | 447 601 474 454 | 448 587 469 455 | 449 601 458 456 | 450 615 473 457 | 451 606 489 458 | 452 589 487 459 | 453 494 339 460 | 454 483 357 461 | 455 501 357 462 | 456 475 341 463 | 457 511 341 464 | 458 443 294 465 | 459 460 289 466 | 460 452 306 467 | 461 444 279 468 | 462 427 294 469 | 463 433 312 470 | 464 608 140 471 | 465 621 216 472 | 466 637 210 473 | 467 624 198 474 | 468 607 208 475 | 469 635 227 476 | 470 620 234 477 | 471 606 226 478 | 472 592 234 479 | 473 593 251 480 | 474 570 257 481 | 475 576 243 482 | 476 598 283 483 | 477 590 298 484 | 478 581 278 485 | 479 605 267 486 | 480 586 265 487 | 481 627 303 488 | 482 645 310 489 | 483 636 323 490 | 484 616 319 491 | 485 615 288 492 | 486 634 284 493 | 487 607 303 494 | 488 646 295 495 | 489 486 280 496 | 490 376 190 497 | 491 362 203 498 | 492 356 187 499 | 493 363 172 500 | 494 380 172 501 | 495 394 187 502 | 496 380 209 503 | 497 397 203 504 | 498 518 270 505 | 499 502 275 506 | 500 529 283 507 | 501 538 271 508 | 502 485 249 509 | 503 499 260 510 | 504 483 265 511 | 505 468 255 512 | 506 493 152 513 | 507 477 142 514 | 508 464 91 515 | 509 468 66 516 | 510 479 83 517 | 511 490 71 518 | 512 409 231 519 | 513 394 239 520 | 514 401 254 521 | 515 420 243 522 | 516 395 220 523 | 517 426 226 524 | 518 415 212 525 | 519 264 273 526 | 520 273 254 527 | 521 248 274 528 | 522 252 290 529 | 523 544 109 530 | 524 312 465 531 | 525 357 426 532 | 526 340 420 533 | 527 315 447 534 | 528 322 422 535 | 529 349 461 536 | 530 330 463 537 | 531 371 461 538 | 532 359 445 539 | 533 337 441 540 | 534 359 478 541 | 535 264 612 542 | 536 275 631 543 | 537 246 597 544 | 538 244 617 545 | 539 253 635 546 | 540 245 572 547 | 541 261 587 548 | 542 279 597 549 | 543 144 599 550 | 544 161 592 551 | 545 113 350 552 | 546 107 365 553 | 547 129 353 554 | 548 123 369 555 | 549 108 334 556 | 550 77 350 557 | 551 95 342 558 | 552 93 357 559 | 553 81 333 560 | 554 73 365 561 | 555 122 287 562 | 556 150 349 563 | 557 158 365 564 | 558 173 369 565 | 559 73 251 566 | 560 91 259 567 | 561 198 209 568 | 562 201 195 569 | 563 187 170 570 | 564 189 186 571 | 565 203 455 572 | 566 183 447 573 | 567 176 413 574 | 568 182 431 575 | 569 203 437 576 | 570 188 379 577 | 571 183 398 578 | 572 95 326 579 | 573 50 332 580 | 574 61 318 581 | 575 39 319 582 | 576 66 335 583 | 577 38 344 584 | 578 57 352 585 | 579 205 89 586 | 580 189 99 587 | 581 208 107 588 | 582 223 84 589 | 583 258 77 590 | 584 167 147 591 | 585 153 142 592 | 586 156 162 593 | 587 170 167 594 | 588 192 141 595 | 589 201 127 596 | 590 178 131 597 | 591 181 153 598 | 592 198 158 599 | 593 210 142 600 | 594 218 177 601 | 595 213 160 602 | 596 203 177 603 | 597 215 194 604 | 598 231 182 605 | 599 230 162 606 | 600 158 105 607 | 601 174 112 608 | 602 162 125 609 | 603 434 75 610 | 604 453 77 611 | 605 442 93 612 | 606 446 59 613 | 607 260 162 614 | 608 278 154 615 | 609 262 143 616 | 610 245 171 617 | 611 244 150 618 | 612 271 199 619 | 613 289 190 620 | 614 314 142 621 | 615 296 124 622 | 616 94 520 623 | 617 106 509 624 | 618 109 529 625 | 619 89 502 626 | 620 129 530 627 | 621 148 533 628 | 622 118 544 629 | 623 88 535 630 | 624 108 561 631 | 625 78 517 632 | 626 528 634 633 | 627 544 626 634 | 628 558 599 635 | 629 551 583 636 | 630 543 608 637 | 631 527 616 638 | 632 525 506 639 | 633 419 509 640 | 634 531 540 641 | 635 516 553 642 | 636 531 559 643 | 637 520 523 644 | 638 538 524 645 | 639 546 551 646 | 640 553 536 647 | 641 456 629 648 | 642 462 646 649 | 643 440 621 650 | 644 442 642 651 | 645 319 640 652 | 646 296 636 653 | 647 288 617 654 | 648 387 600 655 | 649 378 583 656 | 650 435 595 657 | 651 425 56 658 | 652 415 37 659 | 653 297 83 660 | 654 295 102 661 | 655 278 92 662 | 656 277 76 663 | 657 330 113 664 | 658 311 109 665 | 659 317 126 666 | 660 336 136 667 | 661 509 157 668 | 662 475 236 669 | 663 467 218 670 | 664 596 197 671 | 665 591 215 672 | 666 575 225 673 | 667 581 188 674 | 668 576 205 675 | 669 319 322 676 | 670 343 322 677 | 671 329 308 678 | 672 330 337 679 | 673 309 339 680 | 674 616 447 681 | 675 603 441 682 | 676 645 438 683 | 677 662 435 684 | 678 655 449 685 | 679 641 460 686 | 680 632 447 687 | 681 651 421 688 | 682 621 415 689 | 683 605 405 690 | 684 619 400 691 | 685 608 424 692 | 686 637 407 693 | 687 636 424 694 | 688 623 433 695 | 689 590 164 696 | 690 574 170 697 | 691 595 180 698 | 692 604 156 699 | 693 610 171 700 | 694 477 294 701 | 695 469 309 702 | 696 484 324 703 | 697 491 235 704 | 698 484 222 705 | 699 482 208 706 | 700 481 53 707 | 701 501 61 708 | 702 460 46 709 | 703 547 87 710 | 704 520 90 711 | 705 507 80 712 | 706 535 96 713 | 707 533 79 714 | 708 520 70 715 | 709 525 109 716 | 710 498 115 717 | 711 496 92 718 | 712 509 103 719 | 713 484 102 720 | 714 494 133 721 | 715 480 122 722 | 716 279 277 723 | 717 308 283 724 | 718 292 266 725 | 719 257 233 726 | 720 237 224 727 | 721 255 253 728 | 722 180 282 729 | 723 137 286 730 | 724 145 271 731 | 725 567 106 732 | 726 557 97 733 | 727 579 111 734 | 728 338 481 735 | 729 311 503 736 | 730 316 484 737 | 731 325 519 738 | 732 331 500 739 | 733 371 565 740 | 734 342 590 741 | 735 351 609 742 | 736 360 587 743 | 737 315 609 744 | 738 320 590 745 | 739 332 606 746 | 740 299 601 747 | 741 309 624 748 | 742 332 624 749 | 743 234 556 750 | 744 262 565 751 | 745 147 583 752 | 746 131 587 753 | 747 120 574 754 | 748 131 318 755 | 749 150 320 756 | 750 149 308 757 | 751 134 301 758 | 752 144 331 759 | 753 135 340 760 | 754 123 333 761 | 755 113 318 762 | 756 118 303 763 | 757 74 209 764 | 758 91 215 765 | 759 79 231 766 | 760 277 371 767 | 761 296 371 768 | 762 287 386 769 | 763 306 433 770 | 764 302 415 771 | 765 303 393 772 | 766 239 393 773 | 767 255 386 774 | 768 271 390 775 | 769 288 404 776 | 770 224 405 777 | 771 194 416 778 | 772 212 421 779 | 773 86 303 780 | 774 87 288 781 | 775 102 300 782 | 776 96 314 783 | 777 70 299 784 | 778 78 317 785 | 779 129 164 786 | 780 143 173 787 | 781 142 155 788 | 782 269 60 789 | 783 267 44 790 | 784 289 63 791 | 785 286 43 792 | 786 520 584 793 | 787 535 575 794 | 788 535 593 795 | 789 518 602 796 | 790 503 588 797 | 791 512 569 798 | 792 489 622 799 | 793 478 637 800 | 794 497 636 801 | 795 499 606 802 | 796 510 622 803 | 797 465 597 804 | 798 468 618 805 | 799 450 607 806 | 800 481 606 807 | 801 451 583 808 | 802 484 590 809 | 803 407 626 810 | 804 424 610 811 | 805 406 606 812 | 806 425 630 813 | 807 389 617 814 | 808 418 644 815 | 809 400 644 816 | 810 386 633 817 | 811 411 569 818 | 812 418 589 819 | 813 433 574 820 | 814 425 557 821 | 815 399 586 822 | 816 391 569 823 | 817 410 549 824 | 818 375 35 825 | 819 394 36 826 | 820 357 134 827 | 821 561 156 828 | 822 517 172 829 | 823 565 186 830 | 824 555 170 831 | 825 538 169 832 | 826 579 137 833 | 827 576 152 834 | 828 591 148 835 | 829 583 123 836 | 830 596 135 837 | 831 568 124 838 | 832 506 304 839 | 833 504 322 840 | 834 520 311 841 | 835 489 308 842 | 836 496 292 843 | 837 515 289 844 | 838 562 218 845 | 839 558 237 846 | 840 285 309 847 | 841 302 319 848 | 842 308 302 849 | 843 291 290 850 | 844 270 296 851 | 845 187 221 852 | 846 160 334 853 | 847 169 350 854 | 848 235 295 855 | 849 242 375 856 | 850 203 298 857 | 851 219 307 858 | 852 211 323 859 | 853 202 352 860 | 854 188 361 861 | 855 185 345 862 | 856 201 336 863 | 857 385 509 864 | 858 372 495 865 | 859 352 497 866 | 860 334 574 867 | 861 352 570 868 | 862 299 583 869 | 863 279 577 870 | 864 281 556 871 | 865 147 559 872 | 866 138 546 873 | 867 127 558 874 | 868 137 571 875 | 869 160 548 876 | 870 159 571 877 | 871 127 180 878 | 872 174 233 879 | 873 117 191 880 | 874 109 241 881 | 875 91 245 882 | 876 110 257 883 | 877 96 230 884 | 878 140 189 885 | 879 213 382 886 | 880 208 403 887 | 881 226 387 888 | 882 199 391 889 | 883 205 369 890 | 884 227 368 891 | 885 227 60 892 | 886 208 70 893 | 887 248 54 894 | 888 88 177 895 | 889 80 192 896 | 890 103 183 897 | 891 94 198 898 | 892 110 148 899 | 893 125 151 900 | 894 120 135 901 | 895 99 164 902 | 896 113 168 903 | 897 477 563 904 | 898 470 579 905 | 899 457 564 906 | 900 492 573 907 | 901 497 555 908 | 902 443 554 909 | 903 511 539 910 | 904 431 539 911 | 905 403 55 912 | 906 317 92 913 | 907 305 37 914 | 908 305 55 915 | 909 312 72 916 | 910 339 39 917 | 911 355 33 918 | 912 323 34 919 | 913 324 54 920 | 914 527 156 921 | 915 510 139 922 | 916 515 123 923 | 917 513 190 924 | 918 530 186 925 | 919 546 187 926 | 920 555 202 927 | 921 515 238 928 | 922 545 220 929 | 923 528 255 930 | 924 514 254 931 | 925 532 204 932 | 926 501 223 933 | 927 502 243 934 | 928 238 261 935 | 929 220 237 936 | 930 238 243 937 | 931 236 312 938 | 932 254 308 939 | 933 180 313 940 | 934 198 313 941 | 935 186 297 942 | 936 190 327 943 | 937 169 299 944 | 938 164 315 945 | 939 174 331 946 | 940 358 531 947 | 941 371 546 948 | 942 380 529 949 | 943 365 514 950 | 944 352 551 951 | 945 338 535 952 | 946 345 515 953 | 947 312 554 954 | 948 296 542 955 | 949 318 537 956 | 950 297 564 957 | 951 316 571 958 | 952 332 555 959 | 953 124 215 960 | 954 130 202 961 | 955 111 224 962 | 956 109 206 963 | 957 174 182 964 | 958 156 182 965 | 959 163 210 966 | 960 159 224 967 | 961 141 216 968 | 962 149 202 969 | 963 176 215 970 | 964 165 195 971 | 965 182 200 972 | 966 415 73 973 | 967 381 135 974 | 968 397 75 975 | 969 546 133 976 | 970 528 139 977 | 971 534 123 978 | 972 544 151 979 | 973 561 139 980 | 974 555 119 981 | 975 211 271 982 | 976 217 288 983 | 977 230 277 984 | 978 222 257 985 | 979 198 283 986 | 980 192 266 987 | 981 205 252 988 | 982 228 326 989 | 983 217 339 990 | 984 220 355 991 | 985 288 330 992 | 986 290 351 993 | 987 143 246 994 | 988 162 244 995 | 989 154 258 996 | 990 147 232 997 | 991 133 259 998 | 992 125 246 999 | 993 129 231 1000 | 994 412 112 1001 | 995 405 136 1002 | 996 423 126 1003 | 997 393 118 1004 | 998 400 96 1005 | 999 421 93 1006 | 1000 433 110 1007 | 1001 383 57 1008 | 1002 362 52 1009 | 1003 256 343 1010 | 1004 249 327 1011 | 1005 268 320 1012 | 1006 273 337 1013 | 1007 235 343 1014 | 1008 244 357 1015 | 1009 271 355 1016 | 1010 259 368 1017 | 1011 130 122 1018 | 1012 172 92 1019 | 1013 188 80 1020 | 1014 144 111 1021 | 1015 349 115 1022 | 1016 380 83 1023 | 1017 380 102 1024 | 1018 370 118 1025 | 1019 350 80 1026 | 1020 360 96 1027 | 1021 338 95 1028 | 1022 366 71 1029 | 1023 330 75 1030 | 1024 344 59 1031 | -------------------------------------------------------------------------------- /example-output/smileyface-inverted-1024-tsp (Concorde).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthras/tsp-art-python/e1ad6a5ae342171ca1571a732c48e33239397fd5/example-output/smileyface-inverted-1024-tsp (Concorde).png -------------------------------------------------------------------------------- /example-output/smileyface-inverted-1024-tsp (Python).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthras/tsp-art-python/e1ad6a5ae342171ca1571a732c48e33239397fd5/example-output/smileyface-inverted-1024-tsp (Python).png -------------------------------------------------------------------------------- /example-output/smileyface-inverted-1024-tsp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example-output/smileyface-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthras/tsp-art-python/e1ad6a5ae342171ca1571a732c48e33239397fd5/example-output/smileyface-inverted.png -------------------------------------------------------------------------------- /images/australia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthras/tsp-art-python/e1ad6a5ae342171ca1571a732c48e33239397fd5/images/australia.png -------------------------------------------------------------------------------- /images/croissant-emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthras/tsp-art-python/e1ad6a5ae342171ca1571a732c48e33239397fd5/images/croissant-emoji.png -------------------------------------------------------------------------------- /images/smileyface-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthras/tsp-art-python/e1ad6a5ae342171ca1571a732c48e33239397fd5/images/smileyface-inverted.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | ortools 3 | tqdm 4 | imageio 5 | scipy 6 | matplotlib -------------------------------------------------------------------------------- /stippling.py: -------------------------------------------------------------------------------- 1 | # Copyright Matthew Mack (c) 2020 under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/ 2 | 3 | import os 4 | import sys 5 | cmd = sys.executable 6 | 7 | # The filename of the image you want to stipple goes here. 8 | ORIGINAL_IMAGE = "images/smileyface-inverted.png" 9 | 10 | # Enables saving of images. 11 | SAVE_IMAGE = True 12 | 13 | # Total number of points to stipple your image with 14 | NUMBER_OF_POINTS = 1024 15 | 16 | # Number of iterations for the algorithm to evenly spread out all the points. Increase if it looks like all the points haven't 'settled' after the last few iterations. 17 | NUMBER_OF_ITERATIONS = 25 18 | 19 | # Sets of the point size of dots to appear on the final iteration. Currently untested. 20 | POINT_SIZE = "1.0 1.0" 21 | 22 | # Size of the window that shows the points and their iterations. 23 | FIGURE_SIZE = 8 24 | 25 | # Sets a cutoff point X between black and white (0-255) where any value between X and 255 (white) is considered the 'background' and will not be 'covered' by a dot. 26 | THRESHOLD = 255 27 | 28 | # Forces recalculations. Currently untested, so best to laeve this on True. 29 | FORCE = True 30 | 31 | # Display a diagram that shows each iteration of the algorithm, showing the points being arranged into their positions. 32 | INTERACTIVE = True 33 | 34 | # Displays the plot of the final iteration. Usually disabled if INTERACTIVE = True, since the diagram will also show the final iteration. 35 | DISPLAY_FINAL_ITERATION = False 36 | 37 | # Save the image of the final iteration as a .png file. 38 | SAVE_AS_PNG = True 39 | 40 | # Saves the image of the final iteration as a .pdf file. 41 | SAVE_AS_PDF = False 42 | 43 | # Saves the position of all points as a numpy array. 44 | SAVE_AS_NPY = False 45 | 46 | # Saves the animation of the weighted voronoi algorithm which shows the stippling dots moving into position 47 | SAVE_ANIMATION = True 48 | 49 | full_command = " weighted-voronoi-stippler/stippler.py " + ORIGINAL_IMAGE 50 | 51 | if(SAVE_IMAGE): 52 | full_command += " --save" 53 | full_command += " --n_point " + str(NUMBER_OF_POINTS) 54 | full_command += " --n_iter " + str(NUMBER_OF_ITERATIONS) 55 | full_command += " --pointsize " + POINT_SIZE 56 | full_command += " --figsize " + str(FIGURE_SIZE) 57 | full_command += " --threshold " + str(THRESHOLD) 58 | if(FORCE): 59 | full_command += " --force" 60 | if(INTERACTIVE): 61 | full_command += " --interactive" 62 | if(DISPLAY_FINAL_ITERATION): 63 | full_command += " --display" 64 | if(SAVE_AS_PNG): 65 | full_command += " --png" 66 | if(SAVE_AS_PDF): 67 | full_command += " --pdf" 68 | if(SAVE_AS_NPY): 69 | full_command += " --npy" 70 | if(SAVE_ANIMATION): 71 | full_command += " --save_stippling_animation" 72 | 73 | os.system(cmd + full_command) -------------------------------------------------------------------------------- /weighted-voronoi-stippler/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Nicolas P. Rougier 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. The name of the author may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /weighted-voronoi-stippler/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Weighted Voronoi Stippling 3 | 4 | ![](../data/boots-stipple.png) 5 | 6 | This is a replication of the following article: 7 | 8 | *Weighted Voronoi Stippling*, Adrian Secord. In: Proceedings of the 2nd 9 | International Symposium on Non-photorealistic Animation and 10 | Rendering. NPAR ’02. ACM, 2002, pp. 37– 43. 11 | 12 | where the author introduced a *techniques for generating stipple drawings from 13 | grayscale images using weighted centroidal Voronoi diagrams* as in *the 14 | traditional artistic technique of stippling that places small dots of ink onto 15 | paper such that their density give the impression of tone*. 16 | 17 | 18 | ## Pre-requisites 19 | 20 | This replication has been written and tested on OSX 10.12 (Sierra) using the 21 | following packages: 22 | 23 | * Python 3.6.0 24 | * Numpy 1.12.0 25 | * Scipy 0.18.1 26 | * Matplotlib 2.0.0 27 | * tqdm 4.10 28 | 29 | Original data is in the data directory and you can also obtain it from 30 | [Adrian Secord homepage](http://cs.nyu.edu/~ajsecord/npar2002/StipplingOriginals.zip). 31 | 32 | ## Usage 33 | 34 | ``` 35 | usage: stippler.py [--n_iter n] [--n_point n] [--save] [--force] 36 | [--pointsize min,max] [--figsize w,h] 37 | [--display] [--interactive] file 38 | 39 | Weighted Vororonoi Stippler 40 | 41 | positional arguments: 42 | file Density image filename 43 | 44 | optional arguments: 45 | -h, --help show this help message and exit 46 | --n_iter n Maximum number of iterations 47 | --n_point n Number of points 48 | --pointsize (min,max) (min,max) 49 | Point mix/max size for final display 50 | --figsize w,h Figure size 51 | --force Force recomputation 52 | --save Save computed points 53 | --display Display final result 54 | --interactive Display intermediate results (slower) 55 | ``` 56 | -------------------------------------------------------------------------------- /weighted-voronoi-stippler/stippler.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # ----------------------------------------------------------------------------- 3 | # Weighted Voronoi Stippler 4 | # Copyright (2017) Nicolas P. Rougier - BSD license 5 | # Edited by Matthew Mack 6 | # 7 | # Implementation of: 8 | # Weighted Voronoi Stippling, Adrian Secord 9 | # Symposium on Non-Photorealistic Animation and Rendering (NPAR), 2002 10 | # ----------------------------------------------------------------------------- 11 | # Some usage examples 12 | # 13 | # stippler.py boots.jpg --save --force --n_point 20000 --n_iter 50 14 | # --pointsize 0.5 2.5 --figsize 8 --interactive 15 | # stippler.py plant.png --save --force --n_point 20000 --n_iter 50 16 | # --pointsize 0.5 1.5 --figsize 8 17 | # stippler.py gradient.png --save --force --n_point 5000 --n_iter 50 18 | # --pointsize 1.0 1.0 --figsize 6 19 | # ----------------------------------------------------------------------------- 20 | # usage: stippler.py [-h] [--n_iter n] [--n_point n] [--epsilon n] 21 | # [--pointsize min,max) (min,max] [--figsize w,h] [--force] 22 | # [--save] [--display] [--interactive] 23 | # image filename 24 | # 25 | # Weighted Vororonoi Stippler 26 | # 27 | # positional arguments: 28 | # image filename Density image filename 29 | # 30 | # optional arguments: 31 | # -h, --help show this help message and exit 32 | # --n_iter n Maximum number of iterations 33 | # --n_point n Number of points 34 | # --epsilon n Early stop criterion 35 | # --pointsize (min,max) (min,max) 36 | # Point mix/max size for final display 37 | # --figsize w,h Figure size 38 | # --force Force recomputation 39 | # --save Save computed points 40 | # --display Display final result 41 | # --interactive Display intermediate results (slower) 42 | # ----------------------------------------------------------------------------- 43 | import tqdm 44 | import voronoi 45 | import os.path 46 | import scipy.ndimage 47 | import imageio 48 | import numpy as np 49 | 50 | def normalize(D): 51 | Vmin, Vmax = D.min(), D.max() 52 | if Vmax - Vmin > 1e-5: 53 | D = (D-Vmin)/(Vmax-Vmin) 54 | else: 55 | D = np.zeros_like(D) 56 | return D 57 | 58 | 59 | def initialization(n, D): 60 | """ 61 | Return n points distributed over [xmin, xmax] x [ymin, ymax] 62 | according to (normalized) density distribution. 63 | 64 | with xmin, xmax = 0, density.shape[1] 65 | ymin, ymax = 0, density.shape[0] 66 | 67 | The algorithm here is a simple rejection sampling. 68 | """ 69 | 70 | samples = [] 71 | while len(samples) < n: 72 | # X = np.random.randint(0, density.shape[1], 10*n) 73 | # Y = np.random.randint(0, density.shape[0], 10*n) 74 | X = np.random.uniform(0, density.shape[1], 10*n) 75 | Y = np.random.uniform(0, density.shape[0], 10*n) 76 | P = np.random.uniform(0, 1, 10*n) 77 | index = 0 78 | while index < len(X) and len(samples) < n: 79 | x, y = X[index], Y[index] 80 | x_, y_ = int(np.floor(x)), int(np.floor(y)) 81 | if P[index] < D[y_, x_]: 82 | samples.append([x, y]) 83 | index += 1 84 | return np.array(samples) 85 | 86 | 87 | 88 | if __name__ == '__main__': 89 | import argparse 90 | import matplotlib.pyplot as plt 91 | from matplotlib.animation import FuncAnimation 92 | 93 | default = { 94 | "n_point": 5000, 95 | "n_iter": 50, 96 | "threshold": 255, 97 | "force": False, 98 | "save": False, 99 | "figsize": 6, 100 | "display": False, 101 | "interactive": False, 102 | "pointsize": (1.0, 1.0), 103 | "pdf": False, 104 | "png": False, 105 | "npy": False, 106 | "save_animation": False 107 | } 108 | 109 | description = "Weighted Vororonoi Stippler" 110 | parser = argparse.ArgumentParser(description=description) 111 | parser.add_argument('filename', metavar='image filename', type=str, 112 | help='Density image filename ') 113 | parser.add_argument('--n_iter', metavar='n', type=int, 114 | default=default["n_iter"], 115 | help='Maximum number of iterations') 116 | parser.add_argument('--n_point', metavar='n', type=int, 117 | default=default["n_point"], 118 | help='Number of points') 119 | parser.add_argument('--pointsize', metavar='(min,max)', type=float, 120 | nargs=2, default=default["pointsize"], 121 | help='Point mix/max size for final display') 122 | parser.add_argument('--figsize', metavar='w,h', type=int, 123 | default=default["figsize"], 124 | help='Figure size') 125 | parser.add_argument('--force', action='store_true', 126 | default=default["force"], 127 | help='Force recomputation') 128 | parser.add_argument('--threshold', metavar='n', type=int, 129 | default=default["threshold"], 130 | help='Grey level threshold') 131 | parser.add_argument('--save', action='store_true', 132 | default=default["save"], 133 | help='Save computed points') 134 | parser.add_argument('--display', action='store_true', 135 | default=default["display"], 136 | help='Display final result') 137 | parser.add_argument('--interactive', action='store_true', 138 | default=default["interactive"], 139 | help='Display intermediate results (slower)') 140 | parser.add_argument('--pdf', action='store_true', 141 | default=default["pdf"], 142 | help='Save image as pdf') 143 | parser.add_argument('--png', action='store_true', 144 | default=default["png"], 145 | help='Save image as png') 146 | parser.add_argument('--npy', action='store_true', 147 | default=default["npy"], 148 | help='Save points as npy file') 149 | parser.add_argument('--save_stippling_animation', action='store_true', 150 | default=default["save_animation"], 151 | help='Saves the animation of the weighted voronoi algorithm which shows the stippling dots moving into position') 152 | args = parser.parse_args() 153 | 154 | filename = args.filename 155 | density = imageio.imread(filename, as_gray=True, pilmode='L') # Flattens into a grayscale image, 8 bit pixels, black and white 156 | 157 | # We want (approximately) 500 pixels per voronoi region 158 | zoom = (args.n_point * 500) / (density.shape[0]*density.shape[1]) # Dividing # of pixels*points by image dimensions 159 | zoom = int(round(np.sqrt(zoom))) 160 | #density = scipy.ndimage.zoom(density, zoom, order=0) # This is the bit that resizes the image based on the calculations in the last two lines. 161 | # Apply threshold onto image 162 | # Any color > threshold will be white 163 | density = np.minimum(density, args.threshold) # Obtains minimum value for checking against threshold 164 | 165 | density = 1.0 - normalize(density) 166 | density = density[::-1, :] # Flips the image upside down? (why? Probably because image coordinate axes are upside down) 167 | density_P = density.cumsum(axis=1) 168 | density_Q = density_P.cumsum(axis=1) 169 | 170 | # Setting filenames 171 | dirname = os.path.dirname(filename) 172 | basename = (os.path.basename(filename).split('.'))[0] 173 | pdf_filename = os.path.join(dirname, basename + "-" + str(args.n_point) + "-stipple.pdf") 174 | png_filename = os.path.join(dirname, basename + "-" + str(args.n_point) + "-stipple.png") 175 | dat_filename = os.path.join(dirname, basename + "-" + str(args.n_point) + "-stipple.tsp") 176 | animation_filename = os.path.join(dirname, basename + "-" + str(args.n_point) + "-animation.gif") 177 | 178 | # Initialization 179 | if not os.path.exists(dat_filename) or args.force: 180 | points = initialization(args.n_point, density) 181 | print("Number of points:", args.n_point) 182 | print("Number of iterations:", args.n_iter) 183 | else: 184 | points = np.load(dat_filename) 185 | print("Number of points:", len(points)) 186 | print("Number of iterations: -") 187 | if (args.pdf): 188 | print("PDF: %s " % pdf_filename) 189 | if (args.png): 190 | print("PNG: %s " % png_filename) 191 | print("TSP: %s " % dat_filename) 192 | 193 | xmin, xmax = 0, density.shape[1] 194 | ymin, ymax = 0, density.shape[0] 195 | bbox = np.array([xmin, xmax, ymin, ymax]) 196 | ratio = (xmax-xmin)/(ymax-ymin) 197 | 198 | # Interactive display 199 | if args.interactive: 200 | 201 | # Setup figure 202 | fig = plt.figure(figsize=(args.figsize, args.figsize/ratio), 203 | facecolor="white") 204 | ax = fig.add_axes([0, 0, 1, 1], frameon=False) 205 | ax.set_xlim([xmin, xmax]) 206 | ax.set_xticks([]) 207 | ax.set_ylim([ymin, ymax]) 208 | ax.set_yticks([]) 209 | scatter = ax.scatter(points[:, 0], points[:, 1], s=1, 210 | facecolor="k", edgecolor="None") 211 | 212 | def update(frame): 213 | global points 214 | # Recompute weighted centroids 215 | regions, points = voronoi.centroids(points, density, density_P, density_Q) 216 | 217 | # Update figure 218 | Pi = points.astype(int) 219 | X = np.maximum(np.minimum(Pi[:, 0], density.shape[1]-1), 0) 220 | Y = np.maximum(np.minimum(Pi[:, 1], density.shape[0]-1), 0) 221 | sizes = (args.pointsize[0] + 222 | (args.pointsize[1]-args.pointsize[0])*density[Y, X]) 223 | scatter.set_offsets(points) 224 | scatter.set_sizes(sizes) 225 | bar.update() 226 | 227 | # Save result at last frame 228 | if (frame == args.n_iter-2 and 229 | (not os.path.exists(dat_filename) or args.save)): 230 | 231 | tspfileheader = "NAME : " + filename + "\nTYPE : TSP\nCOMMENT: Stipple of " + filename + " with " + str(len(points)) + " points\nDIMENSION: " + str(len(points)) + "\nEDGE_WEIGHT_TYPE: ATT\nNODE_COORD_SECTION" 232 | nodeindexes = np.arange(1,len(points)+1)[:,np.newaxis] 233 | np.savetxt(dat_filename, np.concatenate((nodeindexes,points),axis=1), ['%d','%d','%d'], header=tspfileheader, comments='') 234 | if (args.pdf): 235 | plt.savefig(pdf_filename) 236 | if (args.png): 237 | plt.savefig(png_filename) 238 | if (args.npy): 239 | np.save(dat_filename, points) 240 | 241 | bar = tqdm.tqdm(total=args.n_iter) 242 | animation = FuncAnimation(fig, update, 243 | repeat=False, frames=args.n_iter-1) 244 | animation.save(animation_filename) 245 | plt.show() 246 | 247 | elif not os.path.exists(dat_filename) or args.force: 248 | for i in tqdm.trange(args.n_iter): 249 | regions, points = voronoi.centroids(points, density, density_P, density_Q) 250 | 251 | 252 | if (args.save or args.display) and not args.interactive: 253 | fig = plt.figure(figsize=(args.figsize, args.figsize/ratio), 254 | facecolor="white") 255 | ax = fig.add_axes([0, 0, 1, 1], frameon=False) 256 | ax.set_xlim([xmin, xmax]) 257 | ax.set_xticks([]) 258 | ax.set_ylim([ymin, ymax]) 259 | ax.set_yticks([]) 260 | scatter = ax.scatter(points[:, 0], points[:, 1], s=1, 261 | facecolor="k", edgecolor="None") 262 | Pi = points.astype(int) 263 | X = np.maximum(np.minimum(Pi[:, 0], density.shape[1]-1), 0) 264 | Y = np.maximum(np.minimum(Pi[:, 1], density.shape[0]-1), 0) 265 | sizes = (args.pointsize[0] + 266 | (args.pointsize[1]-args.pointsize[0])*density[Y, X]) 267 | scatter.set_offsets(points) 268 | scatter.set_sizes(sizes) 269 | 270 | # Save stipple points and tippled image 271 | if not os.path.exists(dat_filename) or args.save: 272 | tspfileheader = "NAME : " + filename + "\nTYPE : TSP\nCOMMENT: Stipple of " + filename + " with " + str(len(points)) + " points\nDIMENSION: " + str(len(points)) + "\nEDGE_WEIGHT_TYPE: ATT\nNODE_COORD_SECTION" 273 | nodeindexes = np.arange(1,len(points)+1)[:,np.newaxis] 274 | np.savetxt(dat_filename, np.concatenate((nodeindexes,points),axis=1), ['%d','%d','%d'], header=tspfileheader, comments='') 275 | if (args.npy): 276 | np.save(dat_filename, points) 277 | if (args.pdf): 278 | plt.savefig(pdf_filename) 279 | if (args.png): 280 | plt.savefig(png_filename) 281 | 282 | if args.display: 283 | plt.show() 284 | 285 | # Plot voronoi regions if you want 286 | # for region in vor.filtered_regions: 287 | # vertices = vor.vertices[region, :] 288 | # ax.plot(vertices[:, 0], vertices[:, 1], linewidth=.5, color='.5' ) 289 | -------------------------------------------------------------------------------- /weighted-voronoi-stippler/voronoi.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Weighted Voronoi Stippler 3 | # Copyright (2017) Nicolas P. Rougier - BSD license 4 | # ----------------------------------------------------------------------------- 5 | import numpy as np 6 | import scipy.spatial 7 | 8 | def rasterize(V): 9 | """ 10 | Polygon rasterization (scanlines). 11 | 12 | Given an ordered set of vertices V describing a polygon, 13 | return all the (integer) points inside the polygon. 14 | See http://alienryderflex.com/polygon_fill/ 15 | 16 | Parameters: 17 | ----------- 18 | 19 | V : (n,2) shaped numpy array 20 | Polygon vertices 21 | """ 22 | 23 | n = len(V) 24 | X, Y = V[:, 0], V[:, 1] 25 | ymin = int(np.ceil(Y.min())) 26 | ymax = int(np.floor(Y.max())) 27 | #ymin = int(np.round(Y.min())) 28 | #ymax = int(np.round(Y.max())) 29 | P = [] 30 | for y in range(ymin, ymax+1): 31 | segments = [] 32 | for i in range(n): 33 | index1, index2 = (i-1) % n, i 34 | y1, y2 = Y[index1], Y[index2] 35 | x1, x2 = X[index1], X[index2] 36 | if y1 > y2: 37 | y1, y2 = y2, y1 38 | x1, x2 = x2, x1 39 | elif y1 == y2: 40 | continue 41 | if (y1 <= y < y2) or (y == ymax and y1 < y <= y2): 42 | segments.append((y-y1) * (x2-x1) / (y2-y1) + x1) 43 | 44 | segments.sort() 45 | for i in range(0, (2*(len(segments)//2)), 2): 46 | x1 = int(np.ceil(segments[i])) 47 | x2 = int(np.floor(segments[i+1])) 48 | # x1 = int(np.round(segments[i])) 49 | # x2 = int(np.round(segments[i+1])) 50 | P.extend([[x, y] for x in range(x1, x2+1)]) 51 | if not len(P): 52 | return V 53 | return np.array(P) 54 | 55 | 56 | def rasterize_outline(V): 57 | """ 58 | Polygon outline rasterization (scanlines). 59 | 60 | Given an ordered set of vertices V describing a polygon, 61 | return all the (integer) points for the polygon outline. 62 | See http://alienryderflex.com/polygon_fill/ 63 | 64 | Parameters: 65 | ----------- 66 | 67 | V : (n,2) shaped numpy array 68 | Polygon vertices 69 | """ 70 | n = len(V) 71 | X, Y = V[:, 0], V[:, 1] 72 | ymin = int(np.ceil(Y.min())) 73 | ymax = int(np.floor(Y.max())) 74 | points = np.zeros((2+(ymax-ymin)*2, 3), dtype=int) 75 | index = 0 76 | for y in range(ymin, ymax+1): 77 | segments = [] 78 | for i in range(n): 79 | index1, index2 = (i-1) % n , i 80 | y1, y2 = Y[index1], Y[index2] 81 | x1, x2 = X[index1], X[index2] 82 | if y1 > y2: 83 | y1, y2 = y2, y1 84 | x1, x2 = x2, x1 85 | elif y1 == y2: 86 | continue 87 | if (y1 <= y < y2) or (y == ymax and y1 < y <= y2): 88 | segments.append((y-y1) * (x2-x1) / (y2-y1) + x1) 89 | segments.sort() 90 | for i in range(0, (2*(len(segments)//2)), 2): 91 | x1 = int(np.ceil(segments[i])) 92 | x2 = int(np.ceil(segments[i+1])) 93 | points[index] = x1, x2, y 94 | index += 1 95 | return points[:index] 96 | 97 | 98 | def weighted_centroid_outline(V, P, Q): 99 | """ 100 | Given an ordered set of vertices V describing a polygon, 101 | return the surface weighted centroid according to density P & Q. 102 | 103 | P & Q are computed relatively to density: 104 | density_P = density.cumsum(axis=1) 105 | density_Q = density_P.cumsum(axis=1) 106 | 107 | This works by first rasterizing the polygon and then 108 | finding the center of mass over all the rasterized points. 109 | """ 110 | 111 | O = rasterize_outline(V) 112 | X1, X2, Y = O[:,0], O[:,1], O[:,2] 113 | 114 | Y = np.minimum(Y, P.shape[0]-1) 115 | X1 = np.minimum(X1, P.shape[1]-1) 116 | X2 = np.minimum(X2, P.shape[1]-1) 117 | 118 | d = (P[Y,X2]-P[Y,X1]).sum() 119 | x = ((X2*P[Y,X2] - Q[Y,X2]) - (X1*P[Y,X1] - Q[Y,X1])).sum() 120 | y = (Y * (P[Y,X2] - P[Y,X1])).sum() 121 | if d: 122 | return [x/d, y/d] 123 | return [x, y] 124 | 125 | 126 | 127 | def uniform_centroid(V): 128 | """ 129 | Given an ordered set of vertices V describing a polygon, 130 | returns the uniform surface centroid. 131 | 132 | See http://paulbourke.net/geometry/polygonmesh/ 133 | """ 134 | A = 0 135 | Cx = 0 136 | Cy = 0 137 | for i in range(len(V)-1): 138 | s = (V[i, 0]*V[i+1, 1] - V[i+1, 0]*V[i, 1]) 139 | A += s 140 | Cx += (V[i, 0] + V[i+1, 0]) * s 141 | Cy += (V[i, 1] + V[i+1, 1]) * s 142 | Cx /= 3*A 143 | Cy /= 3*A 144 | return [Cx, Cy] 145 | 146 | 147 | def weighted_centroid(V, D): 148 | """ 149 | Given an ordered set of vertices V describing a polygon, 150 | return the surface weighted centroid according to density D. 151 | 152 | This works by first rasterizing the polygon and then 153 | finding the center of mass over all the rasterized points. 154 | """ 155 | 156 | P = rasterize(V) 157 | Pi = P.astype(int) 158 | Pi[:, 0] = np.minimum(Pi[:, 0], D.shape[1]-1) 159 | Pi[:, 1] = np.minimum(Pi[:, 1], D.shape[0]-1) 160 | D = D[Pi[:, 1], Pi[:, 0]].reshape(len(Pi), 1) 161 | return ((P*D)).sum(axis=0) / D.sum() 162 | 163 | 164 | 165 | 166 | # http://stackoverflow.com/questions/28665491/... 167 | # ...getting-a-bounded-polygon-coordinates-from-voronoi-cells 168 | def in_box(points, bbox): 169 | return np.logical_and( 170 | np.logical_and(bbox[0] <= points[:, 0], points[:, 0] <= bbox[1]), 171 | np.logical_and(bbox[2] <= points[:, 1], points[:, 1] <= bbox[3])) 172 | 173 | 174 | def voronoi(points, bbox): 175 | # See http://stackoverflow.com/questions/28665491/... 176 | # ...getting-a-bounded-polygon-coordinates-from-voronoi-cells 177 | # See also https://gist.github.com/pv/8036995 178 | 179 | # Select points inside the bounding box 180 | i = in_box(points, bbox) 181 | 182 | # Mirror points 183 | points_center = points[i, :] 184 | points_left = np.copy(points_center) 185 | points_left[:, 0] = bbox[0] - (points_left[:, 0] - bbox[0]) 186 | points_right = np.copy(points_center) 187 | points_right[:, 0] = bbox[1] + (bbox[1] - points_right[:, 0]) 188 | points_down = np.copy(points_center) 189 | points_down[:, 1] = bbox[2] - (points_down[:, 1] - bbox[2]) 190 | points_up = np.copy(points_center) 191 | points_up[:, 1] = bbox[3] + (bbox[3] - points_up[:, 1]) 192 | points = np.append(points_center, 193 | np.append(np.append(points_left, points_right, axis=0), 194 | np.append(points_down, points_up, axis=0), 195 | axis=0), axis=0) 196 | # Compute Voronoi 197 | vor = scipy.spatial.Voronoi(points) 198 | 199 | # Filter regions 200 | epsilon = 0.1 201 | regions = [] 202 | for region in vor.regions: 203 | flag = True 204 | for index in region: 205 | if index == -1: 206 | flag = False 207 | break 208 | else: 209 | x = vor.vertices[index, 0] 210 | y = vor.vertices[index, 1] 211 | if not(bbox[0]-epsilon <= x <= bbox[1]+epsilon and 212 | bbox[2]-epsilon <= y <= bbox[3]+epsilon): 213 | flag = False 214 | break 215 | if region != [] and flag: 216 | regions.append(region) 217 | vor.filtered_points = points_center 218 | vor.filtered_regions = regions 219 | return vor 220 | 221 | 222 | def centroids(points, density, density_P=None, density_Q=None): 223 | """ 224 | Given a set of point and a density array, return the set of weighted 225 | centroids. 226 | """ 227 | 228 | X, Y = points[:,0], points[:, 1] 229 | # You must ensure: 230 | # 0 < X.min() < X.max() < density.shape[0] 231 | # 0 < Y.min() < Y.max() < density.shape[1] 232 | 233 | xmin, xmax = 0, density.shape[1] 234 | ymin, ymax = 0, density.shape[0] 235 | bbox = np.array([xmin, xmax, ymin, ymax]) 236 | vor = voronoi(points, bbox) 237 | regions = vor.filtered_regions 238 | centroids = [] 239 | for region in regions: 240 | vertices = vor.vertices[region + [region[0]], :] 241 | # vertices = vor.filtered_points[region + [region[0]], :] 242 | 243 | # Full version from all the points 244 | # centroid = weighted_centroid(vertices, density) 245 | 246 | # Optimized version from only the outline 247 | centroid = weighted_centroid_outline(vertices, density_P, density_Q) 248 | 249 | centroids.append(centroid) 250 | return regions, np.array(centroids) 251 | --------------------------------------------------------------------------------