├── .gitignore ├── LICENSE ├── README.md ├── images ├── gamma_image_5.png └── pygenarttut.png ├── requirements.txt └── src └── generate_art.py /.gitignore: -------------------------------------------------------------------------------- 1 | output/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 pixegami 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Generative Art Tutorial 2 | 3 | ![preview](images/pygenarttut.png) 4 | 5 | This is a tutorial for creating generative abstract art-work using Python. It is based on a generative art NFT collection I created earlier, called [Machine Psychology](https://www.mach-psy.com/) ([source](https://github.com/pixegami-team/machine-psychology-python-art)). But this tutorial project is simplified and only focuses on using Python to create the image. 6 | 7 | ## Usage 8 | 9 | First make sure you have PIL installed. 10 | 11 | ```bash 12 | pip install pillow 13 | ``` 14 | 15 | You can run it like this. 16 | 17 | ```bash 18 | # Generates a collection called 'foo' with 32 pieces into ./output/foo/ 19 | python src/generate_art.py -n 32 --collection "foo" 20 | ``` 21 | 22 | It should generate an image like this: 23 | 24 | ![example_image](images/gamma_image_5.png) 25 | -------------------------------------------------------------------------------- /images/gamma_image_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixegami/python-generative-art-tutorial/79c439dd501644992183fae647eb9aea200dc405/images/gamma_image_5.png -------------------------------------------------------------------------------- /images/pygenarttut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixegami/python-generative-art-tutorial/79c439dd501644992183fae647eb9aea200dc405/images/pygenarttut.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pillow -------------------------------------------------------------------------------- /src/generate_art.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageChops 2 | import os 3 | import random 4 | import colorsys 5 | import argparse 6 | 7 | 8 | def random_point(image_size_px: int, padding: int): 9 | return random.randint(padding, image_size_px - padding) 10 | 11 | 12 | def random_color(): 13 | 14 | # I want a bright, vivid color, so max V and S and only randomize HUE. 15 | h = random.random() 16 | s = 1 17 | v = 1 18 | float_rbg = colorsys.hsv_to_rgb(h, s, v) 19 | 20 | # Return as integer RGB. 21 | return ( 22 | int(float_rbg[0] * 255), 23 | int(float_rbg[1] * 255), 24 | int(float_rbg[2] * 255), 25 | ) 26 | 27 | 28 | def interpolate(start_color, end_color, factor: float): 29 | # Find the color that is exactly factor (0.0 - 1.0) between the two colors. 30 | new_color_rgb = [] 31 | for i in range(3): 32 | new_color_value = factor * end_color[i] + (1 - factor) * start_color[i] 33 | new_color_rgb.append(int(new_color_value)) 34 | 35 | return tuple(new_color_rgb) 36 | 37 | 38 | def generate_art(collection: str, name: str): 39 | print("Generating art") 40 | 41 | # Figure out where we are going to put it. 42 | output_dir = os.path.join("output", collection) 43 | image_path = os.path.join(output_dir, f"{name}.png") 44 | 45 | # Set size parameters. 46 | rescale = 2 47 | image_size_px = 128 * rescale 48 | padding = 12 * rescale 49 | 50 | # Create the directory and base image. 51 | os.makedirs(output_dir, exist_ok=True) 52 | bg_color = (0, 0, 0) 53 | image = Image.new("RGB", (image_size_px, image_size_px), bg_color) 54 | 55 | # How many lines do we want to draw? 56 | num_lines = 10 57 | points = [] 58 | 59 | # Pick the colors. 60 | start_color = random_color() 61 | end_color = random_color() 62 | 63 | # Generate points to draw. 64 | for _ in range(num_lines): 65 | point = ( 66 | random_point(image_size_px, padding), 67 | random_point(image_size_px, padding), 68 | ) 69 | points.append(point) 70 | 71 | # Center image. 72 | # Find the bounding box. 73 | min_x = min([p[0] for p in points]) 74 | max_x = max([p[0] for p in points]) 75 | min_y = min([p[1] for p in points]) 76 | max_y = max([p[1] for p in points]) 77 | 78 | # Find offsets. 79 | x_offset = (min_x - padding) - (image_size_px - padding - max_x) 80 | y_offset = (min_y - padding) - (image_size_px - padding - max_y) 81 | 82 | # Move all points by offset. 83 | for i, point in enumerate(points): 84 | points[i] = (point[0] - x_offset // 2, point[1] - y_offset // 2) 85 | 86 | # Draw the points. 87 | current_thickness = 1 * rescale 88 | n_points = len(points) - 1 89 | for i, point in enumerate(points): 90 | 91 | # Create the overlay. 92 | overlay_image = Image.new("RGB", (image_size_px, image_size_px), (0, 0, 0)) 93 | overlay_draw = ImageDraw.Draw(overlay_image) 94 | 95 | if i == n_points: 96 | # Connect the last point back to the first. 97 | next_point = points[0] 98 | else: 99 | # Otherwise connect it to the next element. 100 | next_point = points[i + 1] 101 | 102 | # Find the right color. 103 | factor = i / n_points 104 | line_color = interpolate(start_color, end_color, factor=factor) 105 | 106 | # Draw the line. 107 | overlay_draw.line([point, next_point], fill=line_color, width=current_thickness) 108 | 109 | # Increase the thickness. 110 | current_thickness += rescale 111 | 112 | # Add the overlay channel. 113 | image = ImageChops.add(image, overlay_image) 114 | 115 | # Image is done! Now resize it to be smooth. 116 | image = image.resize( 117 | (image_size_px // rescale, image_size_px // rescale), resample=Image.ANTIALIAS 118 | ) 119 | 120 | # Save the image. 121 | image.save(image_path) 122 | 123 | 124 | if __name__ == "__main__": 125 | 126 | parser = argparse.ArgumentParser() 127 | parser.add_argument("-n", type=int, default=1, help="Number of images to generate.") 128 | parser.add_argument("--collection", type=str, help="Collection name for the art.") 129 | 130 | args = parser.parse_args() 131 | n = args.n 132 | collection_name = args.collection 133 | 134 | for i in range(n): 135 | generate_art(collection_name, f"{collection_name}_image_{i}") 136 | --------------------------------------------------------------------------------