├── .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 | 
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 |  | 
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 | 
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 |
--------------------------------------------------------------------------------