├── .github └── workflows │ └── build_deploy.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── Makefile ├── README.md ├── _static ├── academis.css ├── academis_header.png ├── academis_logo.png └── favicon.ico ├── animation └── space_vortex.py ├── bbtor.jpg ├── blossom ├── README.rst ├── blossom.py └── infinite_blossom.gif ├── bouncing_ball.py ├── circles ├── README.rst └── circle.py ├── conf.py ├── contour ├── README.rst └── contour.py ├── convolution ├── GenerativeConvolution.ipynb ├── README.rst └── convolution.py ├── dev_requirements.txt ├── dragon_curve ├── README.rst └── dragon.py ├── dtree ├── README.rst └── decision_tree.py ├── experimental ├── deconvolution │ ├── README.md │ └── deconvolution.py └── polygon │ ├── README.md │ └── polygon.py ├── flags ├── README.rst └── flags.py ├── flower_movie ├── README.rst └── make_movie.bat ├── gradient ├── README.rst └── gradient.py ├── grayscale ├── README.rst └── grayscale.py ├── hexpanda ├── README.rst ├── hexpanda.py └── panda.png ├── images ├── arial.ttf ├── bridge_meme.png ├── caleidoscope.png ├── circle.png ├── circle_challenge.png ├── circle_challenge2.png ├── circle_challenge3.png ├── colorchannels.png ├── contour_isolines.png ├── contour_steps.png ├── convolution.png ├── corner.png ├── decision_tree.png ├── dots.png ├── dragon.png ├── fft.png ├── finland.png ├── flower_movie.png ├── gradient.png ├── gradient_gray.png ├── gradient_square.png ├── gray.png ├── gray_challenge.png ├── hexpanda.png ├── kmeans.png ├── mandelbrot.png ├── mask.png ├── monte_carlo.png ├── montecarlo.png ├── naumgabo.png ├── output.png ├── pink.png ├── puzzle_pieces.jpg ├── puzzle_pieces.png ├── puzzle_pieces.svg ├── python_logo.png ├── repeat.png ├── repeat_challenge.png ├── rgb_challenge.png ├── rotate.png ├── shadow.png ├── slice.png ├── snowflake3.png ├── sobel.png ├── spiral_challenge.png ├── stars.png ├── title.png ├── triangle.png ├── triangles.png ├── vortex.gif └── wave.gif ├── index.rst ├── kmeans ├── README.rst └── decolorize.py ├── lines ├── README.rst ├── lines.png └── lines.py ├── mandelbrot ├── README.rst └── mandelbrot.py ├── mask ├── README.rst └── mask.py ├── memegen ├── README.rst ├── arial.ttf ├── bridge.png └── memegen.py ├── montecarlo ├── README.rst ├── animation.py ├── monte_carlo.py ├── mosaic_generator.py └── patterns.py ├── puzzle ├── README.rst ├── make_pieces.py ├── pieces.zip └── puzzle.py ├── random_tiles ├── README.rst ├── random_tiles.png └── random_tiles.py ├── repeat ├── README.rst └── repeat.py ├── requirements.txt ├── rgb ├── README.rst └── rgb.py ├── rotate ├── README.rst ├── caleidoscope.py └── rotate.py ├── sand ├── README.rst ├── desert_python.png ├── desert_python.svg ├── sand.py └── sand_animation_small.gif ├── shadow ├── README.rst └── shadow.py ├── sobel ├── README.rst └── sobel.py ├── solutions ├── circle_challenge.py ├── dots.py ├── four_triangles.py ├── gradient_challenge.py ├── naumgabo.py ├── repeat_random.py └── spiral_challenge.py ├── spiral ├── README.rst ├── lines.py ├── spiral.png └── spiral.py ├── stars ├── README.rst └── stars.py ├── starwars ├── README.rst ├── message.txt ├── starwars.py ├── sw_animation.gif └── text.png ├── thank_you ├── README.rst ├── bubble.png ├── panda.png ├── pingu.png ├── thank_you.py └── thank_you_animation.gif ├── transform_logo.py ├── triangles ├── README.rst └── corner.py ├── vortex ├── README.rst └── vortex.py └── warhol ├── README.rst └── colorchannels.py /.github/workflows/build_deploy.yml: -------------------------------------------------------------------------------- 1 | 2 | name: deploy numpy graphics 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | 8 | jobs: 9 | 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: checkout repo 16 | uses: actions/checkout@v1 17 | 18 | - name: build static html 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install -r dev_requirements.txt 22 | make html 23 | 24 | - name: copy to academis server 25 | uses: appleboy/scp-action@master 26 | with: 27 | host: ${{ secrets.ACADEMIS_HOST }} 28 | username: ${{ secrets.ACADEMIS_USERNAME }} 29 | port: 22 30 | key: ${{ secrets.SSH_PRIVATE_KEY }} 31 | source: build/html/* 32 | target: "/www/academis/numpy_graphics" 33 | rm: true 34 | strip_components: 2 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [pylint] 2 | disable=C0103,C0111 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kristian Rother 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Graphics with NumPy 3 | 4 | In this tutorial you find NumPy exercises that create images. 5 | 6 | ![title image](images/title.png) 7 | 8 | It is meant as a gentle introduction to NumPy. 9 | It assumes that you have made your first steps with Python already. 10 | To get the best out of it, put it into practice immediately: 11 | 12 | **paint things – create art – have fun!** 13 | 14 | ---- 15 | 16 | ## Installation 17 | 18 | If you are using the [Anaconda distribution](https://www.anaconda.com/), you should have all necessary libraries already. 19 | 20 | Otherwise, download or clone the git repository [github.com/krother/generative_art](https://github.com/krother/generative_art) and install the libraries using `pip`: 21 | 22 | :::bash 23 | pip install -r requirements.txt 24 | 25 | Next, try the examples and challenges. 26 | 27 | ---- 28 | 29 | ## First Steps 30 | 31 | | Example | Topic | 32 | |-----------|--------| 33 | | [Creating Images](grayscale/README.rst) | Create a grayscale image | 34 | | [Color](rgb/README.rst) | Create a RGB image | 35 | | [Random Blur](random_blur/README.rst) | Create random pixels | 36 | | [Flags](flags/README.rst) | Slicing | 37 | | [Repeat](repeat/README.rst) | Repeating tiles | 38 | 39 | 40 | ## Elementary Geometry 41 | 42 | | Example | Topic | 43 | |-----------|--------| 44 | | [Stars](stars/README.rst) | Indexing | 45 | | [Lines](lines/README.rst) | Arithmetics | 46 | | [Gradient](gradient/README.rst) | Linear space | 47 | | [Triangles](triangles/README.rst) | Matrix operations | 48 | | [Circles](circles/README.rst) | Euclidean Distances | 49 | | [Mask](mask/README.rst) | Indexing | 50 | | [Meme Generator](memegen/README.rst) | Adding text with Pillow | 51 | 52 | ## Machine Learning 53 | 54 | | Example | Topic | 55 | |-----------|--------| 56 | | [K-Means](kmeans/README.rst) | Clustering | 57 | | [Decision Tree](dtree/README.rst) | Color prediction | 58 | | [Convolution](convolution/README.rst) | CNN kernel | 59 | | [Monte Carlo](montecarlo/README.rst) | Sampling | 60 | 61 | ## Effects 62 | 63 | | Example | Topic | 64 | |-----------|--------| 65 | | [Rotation](rotate/README.rst) | Rotation figure | 66 | | [Shadow](shadow/README.rst) | shadow using a mask | 67 | | [Warhol](warhol/README.rst) | Color channels | 68 | | [Puzzle](puzzle/README.rst) | vstack and hstack | 69 | | [Contour Lines](contour/README.rst) | Gaussian Mixture | 70 | | [Edge Detection](sobel/README.rst) | Sobel Operator | 71 | | [Dragon Curve](dragon_curve/README.rst) | Recursive graphics | 72 | | [Mandelbrot](mandelbrot/README.rst) | Recursive graphics | 73 | | [Hexpanda](hexpanda/README.rst) | Hexbin plot | 74 | 75 | ## Animations 76 | 77 | | Example | Topic | 78 | |-----------|--------| 79 | | [Flower Assembly](flower_movie/README.rst) | puzzle | 80 | | [Vortex](vortex/README.rst) | rotating parts | 81 | | [Sand blows away](sand/README.rst) | moving particles | 82 | | [Star Wars Titles](starwars/README.rst) | text moving in 3D | 83 | | [Infinity Blossom](blossom/README.rst) | depth illusion | 84 | | [Thank You](thank_you/README.rst) | scripted assembly | 85 | 86 | ---- 87 | 88 | ## Contact 89 | 90 | (c) 2024 Dr. Kristian Rother (`kristian.rother@posteo.de`) 91 | 92 | Distributed under the conditions of the MIT License. See `LICENSE` for details. 93 | 94 | ---- 95 | 96 | ## References 97 | 98 | [The Brandenburg Gate image](https://commons.wikimedia.org/wiki/File:Brandenburger_Tor_abends.jpg) is by Thomas Wolf, www.foto-tw.de / Wikimedia Commons / CC BY-SA 3.0 99 | -------------------------------------------------------------------------------- /_static/academis.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | margin-top: 250px; 4 | margin-bottom: 250px; 5 | background-image: url("academis_header.png"); 6 | background-repeat: no-repeat; 7 | } 8 | -------------------------------------------------------------------------------- /_static/academis_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/_static/academis_header.png -------------------------------------------------------------------------------- /_static/academis_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/_static/academis_logo.png -------------------------------------------------------------------------------- /_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/_static/favicon.ico -------------------------------------------------------------------------------- /animation/space_vortex.py: -------------------------------------------------------------------------------- 1 | 2 | import imageio 3 | import numpy as np 4 | import cv2 5 | 6 | class SpaceVortex: 7 | """ 8 | Generates an animated vortex of random stars 9 | """ 10 | def __init__(self, xsize, ysize, n_stars=100, speed=0.2): 11 | self.amp = np.random.randint(0, xsize//2 - 10, size=(n_stars)).astype(np.int32) 12 | self.phase = np.random.randint(0, 360, size=(n_stars)).astype(np.int32) 13 | 14 | self.ycoord = np.random.randint(0, ysize, size=(n_stars)) 15 | self.angle = 0 16 | self.speed = speed 17 | self.xsize = xsize 18 | 19 | def step(self): 20 | """move the stars ahead""" 21 | self.angle += self.speed 22 | if self.angle >= 360: 23 | self.angle = 0 24 | 25 | def draw(self, frame): 26 | """draw stars into a np.array""" 27 | xx = self.xsize // 2 + np.round(np.cos(np.pi * (self.angle + self.phase) / 180) * self.amp).astype(np.int32) 28 | frame[self.ycoord, xx] = 255 29 | 30 | 31 | stars = SpaceVortex(800, 600, n_stars=100, speed=0.1) 32 | 33 | while True: 34 | frame = np.zeros((600, 800, 4), dtype=np.uint8) 35 | 36 | stars.step() 37 | stars.draw(frame) 38 | 39 | cv2.imshow('frame', frame) 40 | if cv2.waitKey(1) & 0xFF == ord('q'): 41 | break 42 | 43 | 44 | cv2.destroyAllWindows() 45 | -------------------------------------------------------------------------------- /bbtor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/bbtor.jpg -------------------------------------------------------------------------------- /blossom/README.rst: -------------------------------------------------------------------------------- 1 | Infinite Blossom 2 | ================ 3 | 4 | |image0| 5 | 6 | How does it work 7 | ---------------- 8 | 9 | The petals rotate around the center while getting closer. 10 | Therefore, they are represented by **polar coordinates**: an angle and a distance. 11 | The polar coordinates make the updating of petal positions very easy. 12 | Of course, some trigonometric functions need to be applied to convert polar coordinates to cartesian (x/y) coordinates. 13 | 14 | The animation happens on a canvas that is a lot bigger than the actual image. 15 | This is because new petals appear regularly at the borders of the image, but that part is cropped off for better aesthetics. 16 | The rest is optimizing the intervals in which the petals are places. 17 | 18 | For drawing filled polygons I originally wanted to write a numpy function from scratch. 19 | ChatGPT-3 told me there is a function `np.fill_poly()` and gave me a quite sophisticated code example. 20 | It turned out it was hallucinating and no such function exists. 21 | However, OpenCV has a fast enough function for drawing polygons. 22 | 23 | Prerequisites 24 | ------------- 25 | 26 | This script requires two libraries: **OpenCV** for displaying the live 27 | animation and **imageio** for exporting animated GIFs. 28 | 29 | :: 30 | 31 | pip install opencv-python 32 | pip install imagio 33 | 34 | The Script 35 | ---------- 36 | 37 | .. literalinclude:: blossom.py 38 | 39 | .. |image0| image:: infinite_blossom.gif 40 | -------------------------------------------------------------------------------- /blossom/blossom.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | import numpy as np 4 | import cv2 5 | import time 6 | import imageio 7 | 8 | MAXX, MAXY = 3000, 3000 9 | SIZE = 5 10 | 11 | 12 | class Petal: 13 | """ 14 | Position of a petal in polar coordinates 15 | """ 16 | def __init__(self, angle, dist): 17 | self.angle = angle 18 | self.dist = dist 19 | 20 | @property 21 | def moving(self): 22 | return self.dist > 0 23 | 24 | def update(self): 25 | self.angle += 1 26 | self.dist -= 1 27 | 28 | def polar_to_cartesian(self, angle, dist): 29 | rad = math.pi * angle / 180 30 | x = math.cos(rad) * dist 31 | y = math.sin(rad) * dist 32 | return int(y), int(x) 33 | 34 | def draw(self, frame): 35 | multiplier = 1.2 + self.dist / 200 36 | y1, x1 = self.polar_to_cartesian(self.angle, self.dist) 37 | y2, x2 = self.polar_to_cartesian(self.angle - 30, self.dist * multiplier) 38 | y3, x3 = self.polar_to_cartesian(self.angle + 30, self.dist * multiplier) 39 | 40 | col = (0, 64 - (64 * self.dist) // 360, 255 - (255 * self.dist)//360) 41 | 42 | offset = np.array([MAXY // 2, MAXX // 2]) 43 | vertices = np.array([[y1, x1], [y2, x2], [y3, x3]]) + offset 44 | cv2.fillPoly(frame, [vertices], col) 45 | 46 | 47 | def create_petals(dist, angle_offset=0): 48 | return [ 49 | Petal(angle_offset + 0, dist), 50 | Petal(angle_offset + 120, dist), 51 | Petal(angle_offset + 240, dist), 52 | ] 53 | 54 | 55 | background = np.zeros((MAXY, MAXX, 3), np.uint8) 56 | petals = [] 57 | angle = 60 58 | for dist in range(40, 400, 40): 59 | petals += create_petals(dist, angle) 60 | angle = 60 - angle 61 | 62 | 63 | frames = [] 64 | angle_ofs = 0 65 | 66 | for i in range(240): 67 | frame = background.copy() 68 | # more all petals 69 | for p in petals: 70 | p.update() 71 | p.draw(frame) 72 | petals = [p for p in petals if p.moving] # remove finished petals 73 | 74 | cropped = frame[1200:-1200,1200:-1200] # cut off border where new petals appear 75 | cv2.imshow('frame', cropped) 76 | rgb = cv2.cvtColor(cropped, cv2.COLOR_BGR2RGB) 77 | frames.append(rgb) 78 | 79 | if i % 40 == 0: 80 | petals += create_petals(400, angle_ofs + i) 81 | angle_ofs = 60 - angle_ofs 82 | 83 | key = chr(cv2.waitKey(1) & 0xFF) 84 | if key == 'q': 85 | break 86 | 87 | time.sleep(0.03) 88 | 89 | cv2.destroyAllWindows() 90 | 91 | imageio.mimsave('infinite_blossom.gif', frames[::2], fps=20) -------------------------------------------------------------------------------- /blossom/infinite_blossom.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/blossom/infinite_blossom.gif -------------------------------------------------------------------------------- /bouncing_ball.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | import cv2 4 | import numpy as np 5 | import random 6 | 7 | WHITE = 255, 255, 255 8 | SIZE = 700 9 | 10 | 11 | class BouncingBall: 12 | 13 | radius = 20 14 | color = WHITE 15 | 16 | def __init__(self): 17 | self.x = 100 18 | self.y = 100 19 | self.dx = 1 20 | self.dy = 1 21 | 22 | def move(self): 23 | self.x += self.dx 24 | self.y += self.dy 25 | if self.x < 0: 26 | self.x = 0 27 | self.dx = random.randint(1, 3) 28 | if self.y < 0: 29 | self.y = 0 30 | self.dy = random.randint(1, 3) 31 | if self.x >= SIZE: 32 | self.dx = -random.randint(1, 3) 33 | if self.y >= SIZE: 34 | self.dy = -random.randint(1, 3) 35 | 36 | def draw(self, frame): 37 | Y, X = np.ogrid[0:SIZE, 0:SIZE] 38 | square_dist = (X - self.x) ** 2 + (Y - self.y) ** 2 39 | mask = square_dist < self.radius ** 2 40 | frame[mask] = self.color 41 | 42 | 43 | bg = np.zeros((SIZE, SIZE, 3), dtype=np.uint8) 44 | ball = BouncingBall() 45 | 46 | while True: 47 | frame = bg.copy() // 2 48 | ball.move() 49 | ball.draw(frame) 50 | cv2.imshow('bouncing ball', frame) 51 | 52 | key = chr(cv2.waitKey(1) & 0xFF) 53 | if key == 'q': 54 | break 55 | 56 | cv2.destroyAllWindows() 57 | -------------------------------------------------------------------------------- /circles/README.rst: -------------------------------------------------------------------------------- 1 | Circles 2 | ======= 3 | 4 | **Circles are drawn by calculating a 2D array of Euclidean distances and 5 | clipping:** 6 | 7 | |image0| 8 | 9 | Here is the code that creates the circle: 10 | 11 | .. literalinclude:: circle.py 12 | 13 | Hints 14 | ----- 15 | 16 | - the ``np.ogrid`` function creates two 2D-array of the X/Y coordinates 17 | - the ``square_dist`` contains the squared distance to the center for 18 | each pixel 19 | - for the mask, the rule of Pythagoras is applied to select points 20 | inside the circle 21 | 22 | ---- 23 | 24 | Challenge: 25 | ---------- 26 | 27 | Adopt the code to create patterns from circles, e.g.: 28 | 29 | |image1| 30 | 31 | |image2| 32 | 33 | |image3| 34 | 35 | .. |image0| image:: ../images/circle.png 36 | .. |image1| image:: ../images/circle_challenge2.png 37 | .. |image2| image:: ../images/circle_challenge3.png 38 | .. |image3| image:: ../images/circle_challenge.png 39 | 40 | -------------------------------------------------------------------------------- /circles/circle.py: -------------------------------------------------------------------------------- 1 | 2 | from PIL import Image 3 | import numpy as np 4 | 5 | def circle(a, xcenter, ycenter, radius, color): 6 | Y, X = np.ogrid[0:400, 0:400] 7 | square_dist = (X - xcenter) ** 2 + (Y - ycenter) ** 2 8 | mask = square_dist < radius ** 2 9 | a[mask] = color 10 | 11 | radius = 100 12 | xcenter, ycenter = 200, 200 13 | color = np.array([255, 128, 0], np.uint8) 14 | 15 | a = np.zeros((400, 400, 3), np.uint8) 16 | circle(a, xcenter, ycenter, radius, color) 17 | 18 | im = Image.fromarray(a) 19 | im.save('circle.png') 20 | -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'NumPy Graphics' 10 | copyright = '2024, Kristian Rother' 11 | author = 'Kristian Rother' 12 | release = '1.0' 13 | html_title = f"{project}" 14 | 15 | 16 | # -- General configuration --------------------------------------------------- 17 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 18 | 19 | extensions = [ 20 | 'sphinx_design', 21 | 'sphinx_copybutton', 22 | 'sphinx.ext.todo', 23 | 'myst_parser', 24 | ] 25 | 26 | exclude_patterns = ['experimental', '_build', 'Thumbs.db', '.DS_Store'] 27 | 28 | language = 'en' 29 | 30 | # -- Options for HTML output ------------------------------------------------- 31 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 32 | 33 | html_theme = 'furo' 34 | html_static_path = ['_static'] 35 | html_logo = "_static/academis_logo.png" 36 | html_favicon = "_static/favicon.ico" 37 | 38 | html_css_files = [ 39 | "academis.css", 40 | ] 41 | html_theme_options = { 42 | "source_repository": "https://github.com/krother/generative_art", 43 | "source_branch": "main", 44 | "source_directory": "", 45 | } -------------------------------------------------------------------------------- /contour/README.rst: -------------------------------------------------------------------------------- 1 | Contour Lines 2 | ============= 3 | 4 | |image0| 5 | 6 | How the image is created 7 | ------------------------ 8 | 9 | 1. generate a regular grid of x and y positions 10 | 2. define x/y positions and widths of several hills 11 | 3. calculate the height of the hills as a Gaussian Mixture using the 12 | positions as means and the widths as standard deviations of a normal 13 | distribution, both for x and y separately 14 | 4. sum up the hills 15 | 5. scale and round the height to regular steps 16 | 17 | You can put in any number of hills. 18 | 19 | .. literalinclude:: contour.py 20 | 21 | Lines only 22 | ---------- 23 | 24 | With a modulo instead of the rounded steps you get to see the contour lines or isolines: 25 | 26 | |image1| 27 | 28 | Challenge 29 | --------- 30 | 31 | Generate random hill centers + sizes. 32 | 33 | .. |image0| image:: ../images/contour_steps.png 34 | .. |image1| image:: ../images/contour_isolines.png 35 | 36 | -------------------------------------------------------------------------------- /contour/contour.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | 5 | 6 | # create a 400 x 400 grid 7 | dim = np.linspace(-10, 10, 400) 8 | x, y, _ = np.meshgrid(dim, dim, [1]) # the [1] adds an extra dimension 9 | 10 | # positions and widths of the 'hills' 11 | position_x = np.array([-3.0, 7.0, 9.0]) 12 | position_y = np.array([0.0, 8.0, -9.0]) 13 | width_x = np.array([5.3, 8.3, 4.0]) 14 | width_y = np.array([6.3, 5.7, 4.0]) 15 | 16 | # calculate height as a combination of Gaussians 17 | d = np.sqrt(((x - position_x) / width_x) ** 2 + ((y - position_y) / width_y) ** 2) 18 | z = np.exp(-d ** 2) # shape is (400, 400, 3) because we have 3 hills 19 | 20 | 21 | z = z.sum(axis=2) # add the hills to get a single landscape 22 | znorm = (z - z.min()) / (z.max() - z.min()) # normalize to range (0.0 .. 1.0) 23 | 24 | # contour lines 25 | contour = (znorm * 8).astype(np.uint8) * 32 26 | im = Image.fromarray(contour, mode='L') 27 | im.save('contour_steps.png') 28 | 29 | # isolines 30 | isolines = ((znorm * 100).round() % 16) == 0 31 | isolines = (isolines * 255).astype(np.uint8) 32 | im = Image.fromarray(isolines, mode='L') 33 | im.save('contour_isolines.png') 34 | -------------------------------------------------------------------------------- /convolution/README.rst: -------------------------------------------------------------------------------- 1 | Convolution 2 | =========== 3 | 4 | |image0| 5 | 6 | The script runs a 5x5 Convolutional Kernel over the image to recognize 7 | horizontal and vertical lines. 8 | 9 | Then it draws the horizontal lines red and the vertical ones green. 10 | 11 | .. literalinclude:: convolution.py 12 | 13 | .. |image0| image:: ../images/convolution.png 14 | 15 | -------------------------------------------------------------------------------- /convolution/convolution.py: -------------------------------------------------------------------------------- 1 | 2 | from PIL import Image 3 | import numpy as np 4 | 5 | im = Image.open('../bbtor.jpg') 6 | bw = im.convert('L') # grayscale 7 | 8 | new_size = bw.size[0] // 4, bw.size[1] // 4 # smaller image 9 | small = bw.resize(new_size) 10 | 11 | a = np.array(small).astype(np.float64) 12 | 13 | # define a 5x5 kernel with preset weights 14 | kernel = np.zeros((5, 5), dtype=np.float64) 15 | kernel[1,:] = -1.0 16 | kernel[2,:] = 2.0 17 | kernel[3,:] = -1.0 18 | 19 | 20 | def convolve(image, kernel): 21 | """apply a 5x5 convolutional kernel on the image""" 22 | feature_map = [] 23 | for y in range(0, image.shape[0] - 4): 24 | for x in range(0, image.shape[1] - 4): 25 | dot = (image[y:y+5, x:x+5] * kernel).sum() 26 | feature_map.append(dot) 27 | 28 | # reshape, cutting of borders 29 | feature_map = np.array(feature_map, dtype=np.float64) 30 | new_shape = image.shape[0] - 4, image.shape[1] - 4 31 | return feature_map.reshape(new_shape) 32 | 33 | 34 | def scale(fm, threshold=120): 35 | """scale with a threshold value found by trial and error""" 36 | fm += threshold 37 | fm[fm < 0] = 0.0 38 | fm = 255 * fm / fm.max() 39 | fm = fm.astype(np.uint8) # prepare for image 40 | return fm 41 | 42 | 43 | # apply the convolution 44 | fm_horizontal = scale(convolve(a, kernel)) 45 | fm_vertical = scale(convolve(a, kernel.T)) 46 | 47 | # set color channels to result 48 | b = np.zeros((fm_horizontal.shape[0], fm_horizontal.shape[1], 3), dtype=np.uint8) 49 | b[:,:,0] = fm_horizontal 50 | b[:,:,1] = fm_vertical 51 | 52 | final = Image.fromarray(b, mode='RGB') 53 | final = final.resize((final.size[0]*2, final.size[1]*2)) # increase image size 54 | 55 | final.save('convolution.png') 56 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-design 3 | sphinx-copybutton 4 | myst-parser 5 | furo 6 | #git+https://github.com/krother/academis_sphinx_theme.git 7 | -------------------------------------------------------------------------------- /dragon_curve/README.rst: -------------------------------------------------------------------------------- 1 | Dragon Curve 2 | ============ 3 | 4 | In this example, we use turtle graphics to generate fractals. 5 | 6 | |image0| 7 | 8 | The curve is generated from a self-similar sequence of left and right 9 | turns. The basic pattern is : 10 | 11 | :: 12 | 13 | dragon(0) -> R 14 | 15 | dragon(1) -> dragon(0) + R + inverse(dragon(0) -> RRL 16 | 17 | dragon(2) -> dragon(0) + R + inverse(dragon(0) -> RRL 18 | 19 | and so on. You find a detailed description on `Dragon Curve (Wikipedia) `__ 20 | 21 | .. literalinclude:: dragon.py 22 | 23 | Hints 24 | ----- 25 | 26 | - generate the curve for a small depth first 27 | - if the ``size`` parameter is too big, the program will crash because 28 | it reaches the array boundaries. 29 | 30 | ---- 31 | 32 | Challenge 33 | --------- 34 | 35 | Try the following self-similar sequence: 36 | 37 | :: 38 | 39 | :::text 40 | seq(0) -> LRRL 41 | seq(1) -> seq(0) + L + seq(0) + R + seq(0) + R + seq(0) + L + seq(0) 42 | ... 43 | 44 | .. |image0| image:: ../images/dragon.png 45 | 46 | -------------------------------------------------------------------------------- /dragon_curve/dragon.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from PIL import Image 3 | 4 | LEFT, UP, RIGHT, DOWN = range(4) 5 | 6 | 7 | def left(direction): 8 | """rotates the direction counter-clockwise""" 9 | return (direction + 3) % 4 10 | 11 | 12 | def right(direction): 13 | """rotates the direction clockwise""" 14 | return (direction + 1) % 4 15 | 16 | 17 | def forward(a, pos, dist, direction, color=0): 18 | """draws a line on the canvas""" 19 | x, y = pos 20 | if direction == RIGHT: 21 | a[y, x:x+dist] = color 22 | pos = x + dist, y 23 | elif direction == LEFT: 24 | a[y, x-dist:x] = color 25 | pos = x - dist, y 26 | elif direction == UP: 27 | a[y-dist:y, x] = color 28 | pos = x, y - dist 29 | elif direction == DOWN: 30 | a[y:y+dist, x] = color 31 | pos = x, y + dist 32 | return pos 33 | 34 | 35 | def draw_sequence(a, sequence, pos, size, direction, color): 36 | """draws a sequence of L and R characters""" 37 | for char in sequence: 38 | pos = forward(a, pos, size, direction, color) 39 | if char == 'R': 40 | direction = right(direction) 41 | else: 42 | direction = left(direction) 43 | pos = forward(a, pos, size, direction, color) 44 | 45 | 46 | def invert(seq): 47 | return ''.join(['L' if char == 'R' else 'R' for char in seq]) 48 | 49 | 50 | def dragon(fwd, depth): 51 | if depth == 0: 52 | return fwd 53 | else: 54 | bwd = invert(reversed(fwd)) 55 | return dragon(fwd + 'R' + bwd, depth-1) 56 | 57 | 58 | a = np.zeros((600, 850, 3), dtype=np.uint8) 59 | lightblue = (0, 128, 255) 60 | 61 | seq = dragon('R', 12) 62 | print(seq) 63 | 64 | draw_sequence(a, seq, 65 | pos=(500, 150), 66 | size=4, 67 | direction=UP, 68 | color=lightblue) 69 | 70 | im = Image.fromarray(a) 71 | im.save('dragon.png') 72 | -------------------------------------------------------------------------------- /dtree/README.rst: -------------------------------------------------------------------------------- 1 | Decision Tree Classifier 2 | ======================== 3 | 4 | |image0| 5 | 6 | .. literalinclude:: decision_tree.py 7 | 8 | .. |image0| image:: ../images/decision_tree.png 9 | 10 | -------------------------------------------------------------------------------- /dtree/decision_tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of using a Decision Tree 3 | to remodel a photograph 4 | """ 5 | from sklearn.tree import DecisionTreeRegressor 6 | from PIL import Image 7 | import numpy as np 8 | import pandas as pd 9 | 10 | # Put your own file name here 11 | INPUT_FILE = 'bbtor.jpg' 12 | OUTPUT_FILE = 'output.png' 13 | 14 | # Experiment with these parameters 15 | SAMPLE_SIZE = 50000 16 | DEPTH = 200 17 | 18 | # read an input RGB image 19 | im = Image.open(INPUT_FILE) 20 | target = np.array(im) 21 | 22 | 23 | # loop through color channels 24 | result = target.copy() 25 | for i in range(3): 26 | df = pd.DataFrame(target[:,:,i]) 27 | df = df.stack().reset_index() 28 | df.columns = ['x', 'y', 'color'] 29 | training = df.sample(SAMPLE_SIZE) 30 | 31 | # train a model that predicts the color from the coordinates 32 | X = training[['x','y']] 33 | m = DecisionTreeRegressor(max_depth=DEPTH) 34 | m.fit(X, training['color']) 35 | ypred = m.predict(df[['x', 'y']]) # predict on all data 36 | 37 | # merge the prediction into the result image 38 | result[:,:,i] = ypred.reshape((im.size[1], im.size[0])) 39 | 40 | output = Image.fromarray(result) 41 | output.save(OUTPUT_FILE) 42 | -------------------------------------------------------------------------------- /experimental/deconvolution/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Deconvolution 3 | 4 | I also attempted a 2D deconvolution. It works, but so far I could not set the weights in any way that it produces anything reasonable: 5 | 6 | :::include deconvolution.py 7 | -------------------------------------------------------------------------------- /experimental/deconvolution/deconvolution.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | 5 | KERNEL_Y, KERNEL_X = 9, 9 6 | IMAGE_SIZE = 100, 100 7 | FMAP_Y = IMAGE_SIZE[0] - KERNEL_Y + 1 8 | FMAP_X = IMAGE_SIZE[1] - KERNEL_X + 1 9 | 10 | FILTERS = 4 11 | 12 | weights = np.random.random(size=(KERNEL_Y, KERNEL_X, FILTERS)) 13 | features = np.random.random(size=(FMAP_Y, FMAP_X, FILTERS)) 14 | 15 | line = np.zeros((KERNEL_Y, KERNEL_X)) 16 | line[:, 4] = 2.0 17 | 18 | block = np.zeros((KERNEL_Y, KERNEL_X)) 19 | block[1:-1, 1:-1] = 1.0 20 | 21 | weights[:,:,0] = line 22 | weights[:,:,1] = line.T 23 | weights[:,:,2] = block 24 | 25 | a = np.zeros(IMAGE_SIZE) 26 | for y in range(FMAP_Y): 27 | for x in range(FMAP_X): 28 | dec = features[y][x] * weights 29 | dec = dec.sum(axis=2) 30 | a[y:y+KERNEL_Y, x:x+KERNEL_X] += dec 31 | 32 | # scale 33 | a = a - a.min() 34 | a = 255 * a / a.max() 35 | a = a.astype(np.uint8) 36 | 37 | im = Image.fromarray(a) 38 | im.save('deconv.png') 39 | -------------------------------------------------------------------------------- /experimental/polygon/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Polygons 3 | 4 | 5 | ![](../images/polygon.png) 6 | 7 | Polygons are good for drawing many shapes. 8 | Because the corners (vertices) are vectors, it is easy to scale, rotate or otherwise transform a polygon. 9 | In Numpy, polygons can be drawn with a smart combination of indexing operations. 10 | 11 | **CODE IS STILL BUGGY** 12 | 13 | :::include polygon.py 14 | 15 | ### Hints 16 | 17 | * the vertices have to be in clockwise order 18 | * check out what the function `np.indices()` 19 | * the **Pillow** library also has a polygon tool 20 | 21 | ---- 22 | 23 | ## Challenge: 24 | 25 | Draw a repeating pattern. 26 | 27 | ![](../images/polygon_repeat.png) 28 | -------------------------------------------------------------------------------- /experimental/polygon/polygon.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import imageio 3 | 4 | 5 | def polygon(a, vertices): 6 | fill = np.ones(a.shape) * True 7 | idx = np.indices(a.shape) 8 | 9 | # loop over pairs of corner points 10 | for k in range(vertices.shape[0]): 11 | p1, p2 = vertices[k-1], vertices[k] 12 | dist = p2 - p1 13 | max_idx = (idx[0] - p1[0]) / dist[0] * dist[1] + p1[1] 14 | sign = np.sign(dist[0]) 15 | check = idx[1] * sign <= max_idx * sign 16 | fill = np.all([fill, check], axis=0) 17 | 18 | a[fill] = 127 19 | 20 | 21 | # clockwise! 22 | vertices = np.array([[50, 50], [100, 150], [80, 200], [50, 350], 23 | [200, 300], [300, 200], [200, 100] 24 | ]) 25 | 26 | a = np.zeros((400, 400), dtype=np.uint8) 27 | polygon(a, vertices) 28 | 29 | imageio.imsave('polygon.png', a) 30 | 31 | # source: https://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array 32 | -------------------------------------------------------------------------------- /flags/README.rst: -------------------------------------------------------------------------------- 1 | Flags 2 | ===== 3 | 4 | **Slicing NumPy arrays allows you to edit rectangular blocks of data.** 5 | 6 | |image0| 7 | 8 | Here is the code that carves out a red square from the image: 9 | 10 | .. literalinclude:: flags.py 11 | 12 | Hints 13 | ----- 14 | 15 | - the slicing contains an interval ``from:to`` for each dimension 16 | - the smallest ``from`` value is zero 17 | - the highest ``to`` value is the size of that dimension plus one 18 | - negative numbers count from the back 19 | - both ``from`` and ``to`` can be omitted 20 | 21 | ---- 22 | 23 | Challenge 24 | --------- 25 | 26 | Draw the flag of a country of your choice, e.g.: 27 | 28 | |image1| 29 | 30 | .. |image0| image:: ../images/slice.png 31 | .. |image1| image:: ../images/finland.png 32 | -------------------------------------------------------------------------------- /flags/flags.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import numpy as np 3 | 4 | a = np.zeros((300, 500, 3), dtype=np.uint8) 5 | 6 | a[100:200, 200:299, 0] = 255 7 | 8 | im = Image.fromarray(a) 9 | im.save('area.png') 10 | -------------------------------------------------------------------------------- /flower_movie/README.rst: -------------------------------------------------------------------------------- 1 | Flower Assembly 2 | =============== 3 | 4 | .. figure:: ../images/flower_movie.png 5 | :alt: flower assembly movie frame 6 | 7 | flower assembly movie frames 8 | 9 | Challenge 10 | --------- 11 | 12 | Write an animation where an image assembles itself from pieces, like the 13 | `Flower Assembly Movie on YouTube `__. 14 | Split the image into many square-shaped tiles and create a series of 15 | frames as ``.png`` images. 16 | 17 | You need 18 | -------- 19 | 20 | - an image 21 | - Python + NumPy + Pillow 22 | - ``MEncoder`` or a similar program that can assemble several frames to an animation 23 | 24 | With MEncoder, you need to run a line similar to the following: 25 | 26 | :: 27 | 28 | mencoder "mf://*.png" -mf fps=25 -o output.avi -ovc lavc -lavcopts vcodec=mpeg4 29 | 30 | Hints 31 | ----- 32 | 33 | - start by creating a very simple animation (e.g. a square moving across the screen) 34 | - you need about 25 frames per second 35 | - a trick to create the assembly effect is to **move apart** the squares in random directions, and play that animation backwards 36 | -------------------------------------------------------------------------------- /flower_movie/make_movie.bat: -------------------------------------------------------------------------------- 1 | mencoder "mf://*.png" -mf fps=25 -o output.avi -ovc lavc -lavcopts vcodec=mpeg4 2 | -------------------------------------------------------------------------------- /gradient/README.rst: -------------------------------------------------------------------------------- 1 | Gradient 2 | ======== 3 | 4 | Let’s create color gradients: 5 | 6 | |image0| 7 | 8 | Here is the code that creates the gradients: 9 | 10 | .. literalinclude:: gradient.py 11 | 12 | The code first creates a grayscale gradient that is later copied into 13 | the RGB array. 14 | 15 | |image1| 16 | 17 | Hints 18 | ----- 19 | 20 | - the ``np.linspace()`` function creates numbers in evenly spaced 21 | intervals 22 | - the parameters indicate the *first and last number and the number of 23 | points* 24 | - by default it creates floating point numbers 25 | - setting ``dtype=np.uint8`` rounds the numbers 26 | - the ``[::-1]`` notation slices an array in the opposite direction 27 | 28 | To create a vertical gradient, you need to transpose the image (swap x and y). 29 | 30 | Note that you may need to create the gradient with swapped dimensions: 31 | 32 | .. code:: python3 33 | 34 | vertical = np.zeros((1200, 300), dtype=np.uint8) 35 | vertical[:1200, :] = np.linspace(0, 255, 300, dtype=np.uint8) 36 | a[:, :1200, :, 0] = vertical.T 37 | 38 | ---- 39 | 40 | Challenge 41 | --------- 42 | 43 | Mix horizontal and vertical gradients: 44 | 45 | |image2| 46 | 47 | .. |image0| image:: ../images/gradient.png 48 | .. |image1| image:: ../images/gradient_gray.png 49 | .. |image2| image:: ../images/gradient_square.png 50 | -------------------------------------------------------------------------------- /gradient/gradient.py: -------------------------------------------------------------------------------- 1 | 2 | from PIL import Image 3 | import numpy as np 4 | 5 | a = np.zeros((300, 1200, 3), dtype=np.uint8) 6 | 7 | full = np.zeros((100, 1200), dtype=np.uint8) 8 | full[:100, :] = np.linspace(0, 255, 1200, dtype=np.uint8) 9 | 10 | half = np.zeros((100, 1200), dtype=np.uint8) 11 | half[:100, :] = np.linspace(0, 127, 1200, dtype=np.uint8) 12 | 13 | a[:100, :, 0] = full 14 | a[100:200, ::-1, 1] = full 15 | a[200:, :, 1] = half 16 | a[200:, :, 2] = full 17 | 18 | im = Image.fromarray(a) 19 | im.save('gradient.png') 20 | -------------------------------------------------------------------------------- /grayscale/README.rst: -------------------------------------------------------------------------------- 1 | Creating Images 2 | =============== 3 | 4 | The Pillow library can draw two-dimensional NumPy arrays as grayscale 5 | images: 6 | 7 | |image0| 8 | 9 | Here is the code that generates the image: 10 | 11 | .. literalinclude:: grayscale.py 12 | 13 | Note that: 14 | 15 | - the two dimensions are written as a tuple ``(y-size, x-size)`` 16 | - the data type is ``np.uint8`` (numbers from 0..255) 17 | - the example adds 128 to each value to obtain a gray image: 18 | 19 | If you are using **Jupyter Notebook**, you can conveniently display any 20 | Pillow image variable by putting the image variable into a Jupyter cell, 21 | e.g.: 22 | 23 | .. code:: python3 24 | 25 | im 26 | 27 | ---- 28 | 29 | Challenges: 30 | ----------- 31 | 32 | - create a rectangular image 33 | - create a black square-shaped image 34 | - create an image that is entirely white 35 | 36 | |image1| 37 | 38 | .. |image0| image:: ../images/gray.png 39 | .. |image1| image:: ../images/gray_challenge.png 40 | 41 | -------------------------------------------------------------------------------- /grayscale/grayscale.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | 5 | a = np.zeros((200, 200), dtype=np.uint8) 6 | a += 128 7 | 8 | im = Image.fromarray(a) 9 | im.save('gray.png') 10 | -------------------------------------------------------------------------------- /hexpanda/README.rst: -------------------------------------------------------------------------------- 1 | Hexpanda 2 | ======== 3 | 4 | |image0| 5 | 6 | The script 7 | 8 | 1. puts the pixels of the original image into a pandas DataFrame 9 | 2. draws a random sample 10 | 3. creates a hexbin plot 11 | 12 | There is a bit of data wrangling done, mostly messing with a pandas 13 | MultiIndex. 14 | 15 | .. literalinclude:: hexpanda.py 16 | 17 | .. |image0| image:: ../images/hexpanda.png 18 | 19 | -------------------------------------------------------------------------------- /hexpanda/hexpanda.py: -------------------------------------------------------------------------------- 1 | 2 | import pandas as pd 3 | import pylab as plt 4 | import numpy as np 5 | from PIL import Image 6 | 7 | GRIDSIZE = 30 8 | 9 | panda = Image.open('panda.png') 10 | panda = panda.convert('L') 11 | panda = np.array(panda) 12 | 13 | panda = 255 - panda # invert 14 | df = pd.DataFrame(panda) 15 | 16 | df = df.unstack() # single column with x/y as index 17 | df = df[df > 0] # only non-white pixels 18 | 19 | df = df.reset_index() 20 | df.columns = ['x', 'y', 'col'] # for convenience 21 | df['y'] *= -1 # otherwise matplotlib draws the panda upside-down 22 | 23 | df = df.sample(df.shape[0] // 4) 24 | 25 | df.plot.hexbin(x='x', y='y', gridsize=GRIDSIZE, cmap=plt.get_cmap('Greys')) 26 | plt.savefig('hexpanda.png') 27 | -------------------------------------------------------------------------------- /hexpanda/panda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/hexpanda/panda.png -------------------------------------------------------------------------------- /images/arial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/arial.ttf -------------------------------------------------------------------------------- /images/bridge_meme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/bridge_meme.png -------------------------------------------------------------------------------- /images/caleidoscope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/caleidoscope.png -------------------------------------------------------------------------------- /images/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/circle.png -------------------------------------------------------------------------------- /images/circle_challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/circle_challenge.png -------------------------------------------------------------------------------- /images/circle_challenge2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/circle_challenge2.png -------------------------------------------------------------------------------- /images/circle_challenge3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/circle_challenge3.png -------------------------------------------------------------------------------- /images/colorchannels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/colorchannels.png -------------------------------------------------------------------------------- /images/contour_isolines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/contour_isolines.png -------------------------------------------------------------------------------- /images/contour_steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/contour_steps.png -------------------------------------------------------------------------------- /images/convolution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/convolution.png -------------------------------------------------------------------------------- /images/corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/corner.png -------------------------------------------------------------------------------- /images/decision_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/decision_tree.png -------------------------------------------------------------------------------- /images/dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/dots.png -------------------------------------------------------------------------------- /images/dragon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/dragon.png -------------------------------------------------------------------------------- /images/fft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/fft.png -------------------------------------------------------------------------------- /images/finland.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/finland.png -------------------------------------------------------------------------------- /images/flower_movie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/flower_movie.png -------------------------------------------------------------------------------- /images/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/gradient.png -------------------------------------------------------------------------------- /images/gradient_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/gradient_gray.png -------------------------------------------------------------------------------- /images/gradient_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/gradient_square.png -------------------------------------------------------------------------------- /images/gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/gray.png -------------------------------------------------------------------------------- /images/gray_challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/gray_challenge.png -------------------------------------------------------------------------------- /images/hexpanda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/hexpanda.png -------------------------------------------------------------------------------- /images/kmeans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/kmeans.png -------------------------------------------------------------------------------- /images/mandelbrot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/mandelbrot.png -------------------------------------------------------------------------------- /images/mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/mask.png -------------------------------------------------------------------------------- /images/monte_carlo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/monte_carlo.png -------------------------------------------------------------------------------- /images/montecarlo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/montecarlo.png -------------------------------------------------------------------------------- /images/naumgabo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/naumgabo.png -------------------------------------------------------------------------------- /images/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/output.png -------------------------------------------------------------------------------- /images/pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/pink.png -------------------------------------------------------------------------------- /images/puzzle_pieces.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/puzzle_pieces.jpg -------------------------------------------------------------------------------- /images/puzzle_pieces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/puzzle_pieces.png -------------------------------------------------------------------------------- /images/python_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/python_logo.png -------------------------------------------------------------------------------- /images/repeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/repeat.png -------------------------------------------------------------------------------- /images/repeat_challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/repeat_challenge.png -------------------------------------------------------------------------------- /images/rgb_challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/rgb_challenge.png -------------------------------------------------------------------------------- /images/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/rotate.png -------------------------------------------------------------------------------- /images/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/shadow.png -------------------------------------------------------------------------------- /images/slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/slice.png -------------------------------------------------------------------------------- /images/snowflake3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/snowflake3.png -------------------------------------------------------------------------------- /images/sobel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/sobel.png -------------------------------------------------------------------------------- /images/spiral_challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/spiral_challenge.png -------------------------------------------------------------------------------- /images/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/stars.png -------------------------------------------------------------------------------- /images/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/title.png -------------------------------------------------------------------------------- /images/triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/triangle.png -------------------------------------------------------------------------------- /images/triangles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/triangles.png -------------------------------------------------------------------------------- /images/vortex.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/vortex.gif -------------------------------------------------------------------------------- /images/wave.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/images/wave.gif -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | :hide-toc: 2 | 3 | Graphics with NumPy 4 | =================== 5 | 6 | .. figure:: images/title.png 7 | :alt: title image 8 | 9 | In this tutorial you find NumPy exercises that create images. 10 | It is meant as a gentle introduction to NumPy. It assumes that you have 11 | made your first steps with Python already. To get the best out of it, 12 | put it into practice immediately: 13 | 14 | **paint things – create art – have fun!** 15 | 16 | 17 | Installation 18 | ------------ 19 | 20 | To execute the examples, you need to install the libraries in :download:`requirements.txt`. 21 | Install them with: 22 | 23 | :: 24 | 25 | pip install -r requirements.txt 26 | 27 | If you are using the `Anaconda distribution `__, 28 | you should have all necessary libraries already. 29 | 30 | ---- 31 | 32 | First Steps 33 | ----------- 34 | 35 | .. toctree:: 36 | :maxdepth: 1 37 | 38 | grayscale/README.rst 39 | rgb/README.rst 40 | random_tiles/README.rst 41 | flags/README.rst 42 | repeat/README.rst 43 | 44 | ---- 45 | 46 | Elementary Geometry 47 | ------------------- 48 | 49 | .. toctree:: 50 | :maxdepth: 1 51 | 52 | stars/README.rst 53 | lines/README.rst 54 | gradient/README.rst 55 | triangles/README.rst 56 | circles/README.rst 57 | spiral/README.rst 58 | mask/README.rst 59 | memegen/README.rst 60 | 61 | ---- 62 | 63 | Machine Learning 64 | ---------------- 65 | 66 | .. toctree:: 67 | :maxdepth: 1 68 | 69 | kmeans/README.rst 70 | dtree/README.rst 71 | convolution/README.rst 72 | montecarlo/README.rst 73 | 74 | ---- 75 | 76 | Effects 77 | ------- 78 | 79 | .. toctree:: 80 | :maxdepth: 1 81 | 82 | rotate/README.rst 83 | shadow/README.rst 84 | warhol/README.rst 85 | puzzle/README.rst 86 | contour/README.rst 87 | sobel/README.rst 88 | dragon_curve/README.rst 89 | mandelbrot/README.rst 90 | hexpanda/README.rst 91 | 92 | ---- 93 | 94 | Animations 95 | ---------- 96 | 97 | .. toctree:: 98 | :maxdepth: 1 99 | 100 | flower_movie/README.rst 101 | vortex/README.rst 102 | sand/README.rst 103 | starwars/README.rst 104 | blossom/README.rst 105 | thank_you/README.rst 106 | 107 | ---- 108 | 109 | .. topic:: License 110 | 111 | © 2024 Dr. Kristian Rother (`kristian.rother@posteo.de`) 112 | 113 | Usable under the conditions of the MIT License. 114 | See :download:`LICENSE` for details. 115 | 116 | .. topic:: References 117 | 118 | `The Brandenburg Gate 119 | image `__ 120 | is by Thomas Wolf, www.foto-tw.de / Wikimedia Commons / CC BY-SA 3.0 121 | -------------------------------------------------------------------------------- /kmeans/README.rst: -------------------------------------------------------------------------------- 1 | K-Means Clustering 2 | ================== 3 | 4 | |image0| 5 | 6 | .. literalinclude:: decolorize.py 7 | 8 | .. |image0| image:: ../images/kmeans.png 9 | 10 | -------------------------------------------------------------------------------- /kmeans/decolorize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cluster the colors of an image 3 | """ 4 | from PIL import Image 5 | import numpy as np 6 | from sklearn.cluster import KMeans 7 | 8 | # put your own image here 9 | INPUT_FILE = "bbtor.jpg" 10 | OUTPUT_FILE = "output.png" 11 | N_CLUSTERS = 6 12 | N_SAMPLES = 100 # number of random pixels used for clustering 13 | 14 | 15 | def cluster_transform(X): 16 | """cluster the colors of an image""" 17 | m = KMeans(N_CLUSTERS) 18 | indices = np.random.randint(0, X.shape[0]-1, size=N_SAMPLES) 19 | Xtrain = X[indices] 20 | m.fit(Xtrain) 21 | clusters = m.predict(X) 22 | c = [m.cluster_centers_[i] for i in clusters] 23 | return np.array(c) 24 | 25 | 26 | if __name__ == '__main__': 27 | im = Image.open(INPUT_FILE) 28 | im = im.resize((im.size[0] // 2, im.size[1] // 2)) 29 | 30 | a = np.array(im) 31 | a = a.reshape((a.shape[0] * a.shape[1], 3)) 32 | c = cluster_transform(a) 33 | 34 | c = c.reshape(im.size[1], im.size[0], 3) 35 | im = Image.fromarray(c.astype(np.uint8), 'RGB') 36 | im.save(OUTPUT_FILE) 37 | -------------------------------------------------------------------------------- /lines/README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _lines: 3 | 4 | Lines 5 | ===== 6 | 7 | Let’s draw lines: 8 | 9 | |image0| 10 | 11 | Here is the code that calculates lines between two points: 12 | 13 | .. literalinclude:: lines.py 14 | 15 | 16 | The algorithm calculates the number of points necessary to have at least one point on each x and y coordinate. 17 | This is necessary because because it is unknown which of the dimensions is wider. 18 | 19 | For calculating the coordinates, the function ``np.linspace`` does a great job at interpolating. 20 | One tricky detail is that the numbers will be used for indexing the bigger array, so they have to be integers. 21 | You need to remember rounding them before the conversion to int, otherwise you would see some strange artifacts. 22 | 23 | .. note:: 24 | 25 | The ``PIL.ImageDraw`` module contains a line drawing tool that 26 | allows you to draw thicker lines. 27 | 28 | ---- 29 | 30 | Challenge 31 | --------- 32 | 33 | Create line art (remotely inspired from the artist **Naum Gabo**) 34 | 35 | |image1| 36 | 37 | .. |image0| image:: lines.png 38 | .. |image1| image:: ../images/naumgabo.png 39 | -------------------------------------------------------------------------------- /lines/lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/lines/lines.png -------------------------------------------------------------------------------- /lines/lines.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | 5 | 6 | def draw_line(a, xstart, ystart, xend, yend): 7 | npoints = max(abs(xend - xstart) + 1, abs(yend - ystart) + 1) 8 | x = np.linspace(xstart, xend, npoints).round().astype(int) 9 | y = np.linspace(ystart, yend, npoints).round().astype(int) 10 | a[y, x] = 255 11 | 12 | 13 | a = np.zeros((400, 400), dtype=np.uint8) 14 | 15 | for yend in range(0, 400, 50): 16 | draw_line(a, 50, 200, 350, yend) 17 | 18 | im = Image.fromarray(a) 19 | im.save('lines.png') 20 | -------------------------------------------------------------------------------- /mandelbrot/README.rst: -------------------------------------------------------------------------------- 1 | Mandelbrot 2 | ========== 3 | 4 | Drawing the Mandelbrot set: 5 | 6 | |image0| 7 | 8 | .. literalinclude:: mandelbrot.py 9 | 10 | .. |image0| image:: ../images/mandelbrot.png 11 | 12 | -------------------------------------------------------------------------------- /mandelbrot/mandelbrot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Drawing the Mandelbrot set 3 | based on R code by Myles Harrison 4 | http://www.everydayanalytics.ca 5 | """ 6 | import numpy as np 7 | from scipy.misc import imshow 8 | 9 | 10 | SIZE = 500 11 | 12 | x = np.linspace(-2.0, 1.0, SIZE) 13 | y = np.linspace(-1.5, 1.5, SIZE) 14 | 15 | ones = np.ones(SIZE) 16 | 17 | c = np.outer(x, ones) + 1j * np.outer(ones, y) 18 | 19 | z = np.zeros((SIZE, SIZE)) * 1j 20 | k = np.zeros((SIZE, SIZE)) 21 | 22 | for i in range(100): 23 | index = z < 2 24 | z[index] = z[index] ** 2 + c[index] 25 | k[index] = k[index] + 1 26 | 27 | imshow(k) 28 | -------------------------------------------------------------------------------- /mask/README.rst: -------------------------------------------------------------------------------- 1 | Mask 2 | ==== 3 | 4 | Define a binary mask to access a part of the image. 5 | 6 | |image0| 7 | 8 | The mask is basically a 2D array of booleans that select pixels. 9 | 10 | .. literalinclude:: mask.py 11 | 12 | ---- 13 | 14 | Challenge 15 | --------- 16 | 17 | Modify the code to create your own mask shape. 18 | 19 | .. |image0| image:: ../images/mask.png 20 | 21 | -------------------------------------------------------------------------------- /mask/mask.py: -------------------------------------------------------------------------------- 1 | 2 | import imageio 3 | import numpy as np 4 | 5 | a = imageio.imread("bbtor.jpg") 6 | 7 | h, w = a.shape[0], a.shape[1] 8 | 9 | Y, X = np.ogrid[0:h, 0:w] 10 | mask = (X - w / 2) ** 2 + (Y - h / 2) ** 2 > w * h / 6 11 | 12 | a[mask] = 0 13 | 14 | imageio.imsave('mask.png', a) 15 | -------------------------------------------------------------------------------- /memegen/README.rst: -------------------------------------------------------------------------------- 1 | Meme Generator 2 | ============== 3 | 4 | Let’s add some text to an image: 5 | 6 | .. figure:: ../images/bridge_meme.png 7 | :alt: Bridge over Troubled Water 8 | 9 | Bridge over Troubled Water 10 | 11 | NumPy cannot add text of its own. You need to use the ``Pillow`` library 12 | instead: 13 | 14 | .. literalinclude:: memegen.py 15 | 16 | For the code to run, you need a **True-Type-Font (TTF)**. You can use 17 | your own or `download arial.ttf `__. 18 | 19 | To convert a Pillow Image to a Numpy array, use: 20 | 21 | .. code:: python3 22 | 23 | a = np.array(im) 24 | 25 | and back: 26 | 27 | .. code:: python3 28 | 29 | im = Image.fromarray(a) 30 | 31 | Challenge 32 | --------- 33 | 34 | Add text to your own image to create a meme or inspirational image. 35 | -------------------------------------------------------------------------------- /memegen/arial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/memegen/arial.ttf -------------------------------------------------------------------------------- /memegen/bridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/memegen/bridge.png -------------------------------------------------------------------------------- /memegen/memegen.py: -------------------------------------------------------------------------------- 1 | 2 | from PIL import Image 3 | from PIL import ImageDraw, ImageFont 4 | 5 | im = Image.open('bridge.png') 6 | draw = ImageDraw.Draw(im) 7 | arial = ImageFont.truetype('arial.ttf', 30) 8 | 9 | draw.text( 10 | (20, 390), 11 | 'All your dreams are on their way', 12 | fill=('white'), 13 | font=arial 14 | ) 15 | draw.text( 16 | (20, 430), 17 | '(Simon & Garfunkel)', 18 | fill=('white'), 19 | font=arial 20 | ) 21 | 22 | im.save('meme.png') 23 | 24 | -------------------------------------------------------------------------------- /montecarlo/README.rst: -------------------------------------------------------------------------------- 1 | Monte Carlo Sampling 2 | ==================== 3 | 4 | |image0| 5 | 6 | The Monte Carlo image generator approximate a target image by randomly placing tiles. 7 | It accepts the change if the distance to the target image becomes smaller. 8 | 9 | To try the algorithm, you need the files: 10 | 11 | - :download:`monte_carlo.py` 12 | - :download:`mosaic_generator.py` 13 | - :download:`patterns.py` 14 | 15 | .. |image0| image:: ../images/montecarlo.png 16 | 17 | -------------------------------------------------------------------------------- /montecarlo/animation.py: -------------------------------------------------------------------------------- 1 | """ 2 | creates an animated GIF from the MC-simulation 3 | """ 4 | import os 5 | import imageio 6 | from mosaic_generator import MosaicGenerator, TILE_SIZE, PATTERNS, COLORS 7 | 8 | os.mkdir('frames') 9 | 10 | patterns = get_patterns(size=TILE_SIZE, functions=PATTERNS, colors=COLORS) 11 | 12 | m = MosaicGenerator(INPUT_FILE, OUTPUT_SIZE, patterns) 13 | m.create_random_image() 14 | 15 | # fit the model 16 | for i in range(1, 100): 17 | m.optimize(i * 100) # increasingly longer periods 18 | m.write_image(f'frames/{i}.png') 19 | 20 | # create the GIF 21 | images = [] 22 | for i in range(1, 100): 23 | filename = f'frames/{i}.png' 24 | images.append(imageio.imread(filename)) 25 | 26 | imageio.mimsave('output.gif', images, fps=20) 27 | -------------------------------------------------------------------------------- /montecarlo/monte_carlo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runs a Monte Carlo Simulation to generate an image 3 | close to a given target. 4 | """ 5 | import random 6 | import numpy as np 7 | from PIL import Image 8 | from matplotlib import pyplot as plt 9 | 10 | 11 | # put your own image here 12 | INPUT_FILE = "bbtor.jpg" 13 | OUTPUT_FILE = "monte_carlo.png" 14 | OUTPUT_SIZE = 960, 640 15 | 16 | # play with these 17 | ITERATIONS = 4000 18 | MINW = 50 19 | MAXW = 200 20 | 21 | 22 | def load_target_image(fn, size): 23 | """returns the image as a NumPy array""" 24 | target = Image.open(fn) 25 | target = target.convert('RGB') 26 | print(f"read target image with size {target.size}") 27 | target = target.resize(size) 28 | return np.array(target, dtype=np.int16) 29 | 30 | def get_random_rect(T): 31 | """Returns a random rectangle""" 32 | xcenter = random.randint(0, T.shape[1]) 33 | ycenter = random.randint(0, T.shape[0]) 34 | w = random.randint(MINW, MAXW) 35 | h = random.randint(MINW, MAXW) 36 | color = [random.randint(0, 255) for i in range(3)] 37 | x = max(0, xcenter - w // 2) 38 | y = max(0, ycenter - h // 2) 39 | return x, y, w, h, color 40 | 41 | def get_distance(a, b): 42 | """the 'energy function' of the simulation""" 43 | return np.abs(a - b).sum() 44 | 45 | def insert_rect_if_better(T, X, rect): 46 | """ 47 | Core of the Monte Carlo procedure: 48 | Calculates the distance between the current and the target image 49 | for the given position. 50 | Then calculates the distance that the given pattern would result in. 51 | If the pattern lowers the distance, it is applied. 52 | """ 53 | x, y, w, h, color = rect 54 | Xrect = X[y:y + h, x:x + h] 55 | Trect = T[y:y + h, x:x + h] 56 | 57 | patch = np.zeros(Xrect.shape) 58 | patch[:,:] = color 59 | 60 | old_dist = get_distance(Xrect, Trect) 61 | new_dist = get_distance(patch, Trect) 62 | if new_dist < old_dist: 63 | X[y:y + h, x:x + h] = patch 64 | 65 | 66 | def optimize(T, X, iterations, history_interval=100): 67 | """ 68 | Run a Monte Carlo optimization algorithm 69 | """ 70 | assert X.shape == T.shape 71 | history = [] 72 | for i in range(iterations): 73 | rect = get_random_rect(T) 74 | insert_rect_if_better(T, X, rect) 75 | if i % history_interval == 0: 76 | history.append(get_distance(X, T)) 77 | return history 78 | 79 | 80 | def write_image(X, out_fn): 81 | """Writes the output image to disk""" 82 | im = Image.fromarray(X.astype(np.uint8), 'RGB') 83 | im.save(out_fn) 84 | 85 | 86 | if __name__ == '__main__': 87 | 88 | T = load_target_image(INPUT_FILE, OUTPUT_SIZE) 89 | X = np.zeros(T.shape, dtype=np.int16) 90 | 91 | history = optimize(T, X, ITERATIONS) 92 | write_image(X, OUTPUT_FILE) 93 | 94 | # plot the history of the loss function 95 | plt.plot(range(len(history)), history) 96 | plt.show() 97 | -------------------------------------------------------------------------------- /montecarlo/mosaic_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates mosaic images from an input image 3 | and a set of tiles using a random sampling algorithm. 4 | """ 5 | import random 6 | import numpy as np 7 | from PIL import Image 8 | from patterns import FULL, MOSAIC, RAINBOW, SPICED, GRAYSCALE, get_patterns 9 | from matplotlib import pyplot as plt 10 | 11 | 12 | # put your own image here 13 | INPUT_FILE = "bbtor.jpg" 14 | OUTPUT_FILE = "output.png" 15 | OUTPUT_SIZE = 960, 640 16 | 17 | # play with these 18 | PATTERNS = MOSAIC # also try FULL 19 | COLORS = SPICED # also try GRAYSCALE or RAINBOW (or your own list) 20 | GRID = True 21 | ITERATIONS = 40000 22 | TILE_SIZE = 20 23 | 24 | 25 | class MosaicGenerator: 26 | """ 27 | generates mosaic images 28 | """ 29 | def __init__(self, target_fn, size, patterns): 30 | self.size = size 31 | self.step = patterns[0].size[0] 32 | self.patterns = [np.array(p, dtype=np.int16) for p in patterns] 33 | self.history = [] 34 | self.T = self.load_target_image(target_fn) 35 | self.X = None 36 | assert self.xsize % self.step == 0 37 | assert self.ysize % self.step == 0 38 | 39 | @property 40 | def xsize(self): 41 | return self.size[0] 42 | 43 | @property 44 | def ysize(self): 45 | return self.size[1] 46 | 47 | def load_target_image(self, fn): 48 | """returns the image as a NumPy array""" 49 | target = Image.open(fn) 50 | target = target.convert('RGB') 51 | print(f"read target image with size {target.size}") 52 | target = target.resize(self.size) 53 | return np.array(target, dtype=np.int16) 54 | 55 | def create_random_image(self): 56 | """Creates an image from random patterns""" 57 | self.X = np.zeros(self.T.shape, dtype=np.int16) 58 | for x in range(0, self.xsize, self.step): 59 | for y in range(0, self.ysize, self.step): 60 | pat = random.choice(self.patterns) 61 | self.X[y:y + self.step, x:x + self.step] = pat 62 | self.history = [] 63 | 64 | def get_random_position(self): 65 | """Returns a random position on the grid""" 66 | x = random.randint(0, self.xsize - self.step) 67 | y = random.randint(0, self.ysize - self.step) 68 | return x, y 69 | 70 | def get_random_position_grid(self): 71 | """Returns a random position on the grid""" 72 | x = random.randint(0, self.xsize // self.step -1) * self.step 73 | y = random.randint(0, self.ysize // self.step -1) * self.step 74 | return x, y 75 | 76 | @staticmethod 77 | def get_dist(a, b): 78 | return np.abs(a - b).sum() 79 | 80 | def insert_pattern_if_better(self, pos, pattern): 81 | """ 82 | Core of the Monte Carlo procedure: 83 | 84 | Calculates the distance between the current and the target image 85 | for the given position. 86 | Then calculates the distance that the given pattern would result in. 87 | 88 | If the pattern lowers the distance, it is applied. 89 | """ 90 | x, y = pos 91 | current_tile = self.X[y:y + self.step, x:x + self.step] 92 | target_tile = self.T[y:y + self.step, x:x + self.step] 93 | old_dist = self.get_dist(current_tile, target_tile) 94 | new_dist = self.get_dist(pattern, target_tile) 95 | if new_dist < old_dist: 96 | self.X[y:y + self.step, x:x + self.step] = pattern 97 | 98 | 99 | def optimize(self, iterations, history_interval=100, grid=True): 100 | """ 101 | Run a Monte Carlo optimization algorithm 102 | """ 103 | assert self.X.shape == self.T.shape 104 | if grid: 105 | random_pos = self.get_random_position_grid 106 | else: 107 | random_pos = self.get_random_position 108 | 109 | for i in range(iterations): 110 | pos = random_pos() 111 | pat = random.choice(self.patterns) 112 | self.insert_pattern_if_better(pos, pat) 113 | if i % history_interval == 0: 114 | self.history.append(self.get_dist(self.X, self.T)) 115 | 116 | def write_image(self, out_fn): 117 | """Writes the output image to disk""" 118 | im = Image.fromarray(self.X.astype(np.uint8), 'RGB') 119 | im.save(out_fn) 120 | 121 | 122 | 123 | if __name__ == '__main__': 124 | patterns = get_patterns(size=TILE_SIZE, functions=PATTERNS, colors=COLORS) 125 | 126 | m = MosaicGenerator(INPUT_FILE, OUTPUT_SIZE, patterns) 127 | m.create_random_image() 128 | m.optimize(ITERATIONS) 129 | m.write_image(OUTPUT_FILE) 130 | 131 | # get the history of the loss function 132 | plt.plot(range(len(m.history)), m.history) 133 | plt.show() 134 | -------------------------------------------------------------------------------- /montecarlo/patterns.py: -------------------------------------------------------------------------------- 1 | 2 | from itertools import combinations 3 | from PIL import Image, ImageDraw 4 | 5 | RAINBOW = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'white', 'black'] 6 | SPICED = ['#6e0096', '#aa0078', '#ff0000', '#ff6900', '#ffa028', '#ffffff'] 7 | GRAYSCALE = ['#000000', '#222222', '#444444', '#666666', '#888888', '#aaaaaa', '#cccccc', '#ffffff'] 8 | 9 | def full(im, d, col, size): 10 | return im 11 | 12 | def horizontal(im, d, col, size): 13 | d.rectangle((0, 0, size, size//2), col) 14 | return im 15 | 16 | def vertical(im, d, col, size): 17 | d.rectangle((0, 0, size//2, size), col) 18 | return im 19 | 20 | def diagonal1(im, d, col, size): 21 | d.polygon((0, 0, size, 0, 0, size), col) 22 | return im 23 | 24 | def diagonal2(im, d, col, size): 25 | d.polygon((0, 0, size, size, 0, size), col) 26 | return im 27 | 28 | def diamond(im, d, col, size): 29 | d.polygon((0, size//2, size//2, 0, size, size//2, size//2, size), col) 30 | return im 31 | 32 | def inset(im, d, col, size): 33 | d.rectangle((size//4, size//4, size-size//4, size-size//4), col) 34 | return im 35 | 36 | def generate_patterns(colors, func, size): 37 | for col1, col2 in combinations(colors, 2): 38 | im = Image.new('RGB', (size, size), color=col1) 39 | d = ImageDraw.Draw(im) 40 | func(im, d, col2, size) 41 | yield im 42 | 43 | FULL = [full] 44 | MOSAIC = [full, horizontal, vertical, diagonal1, diagonal2, diamond, inset] 45 | 46 | 47 | 48 | def get_patterns(size, functions=MOSAIC, colors=RAINBOW): 49 | """ 50 | Returns a list of pattern tiles of the given sizes. 51 | 52 | size : an integer 53 | functions : a list of functions 54 | colors : a list of color strings or hex codes 55 | """ 56 | patterns = [] 57 | for f in functions: 58 | patterns += list(generate_patterns(colors, f, size)) 59 | return patterns 60 | -------------------------------------------------------------------------------- /puzzle/README.rst: -------------------------------------------------------------------------------- 1 | Puzzle 2 | ====== 3 | 4 | **The Numpy functions vstack and hstack concatenate arrays.** 5 | 6 | |image0| 7 | 8 | Assemble the complete picture from the images in :download:`pieces.zip`. 9 | 10 | Here is the code that assembles two of the pieces: 11 | 12 | .. literalinclude:: puzzle.py 13 | 14 | Hints 15 | ----- 16 | 17 | - the functions ``np.vstack`` and ``np.hstack`` accept a list of NumPy 18 | arrays 19 | - one dimension of the arrays needs to be identical 20 | - use ``print(x.shape)`` a lot 21 | - the order in which you assemble matters 22 | 23 | ---- 24 | 25 | Challenge 26 | --------- 27 | 28 | Create your own puzzle. 29 | In :download:`make_pieces.py` you find code that was used to create pieces. 30 | 31 | 32 | .. |image0| image:: ../images/puzzle_pieces.png 33 | 34 | -------------------------------------------------------------------------------- /puzzle/make_pieces.py: -------------------------------------------------------------------------------- 1 | 2 | from PIL import Image 3 | import numpy as np 4 | 5 | a = np.array(Image.open('../bbtor.jpg')) 6 | a = a[::2, ::2] 7 | print(a.shape) 8 | 9 | left = a[:, :220] 10 | right = a[:, 220:] 11 | 12 | a = left[:-320] 13 | b = left[-320:] 14 | 15 | t2 = right[:500] 16 | c = right[500:] 17 | 18 | t3 = t2[:, :400] 19 | d = t2[:, 400:] 20 | 21 | e = t3[:200] 22 | f = t3[200:] 23 | 24 | Image.fromarray(a).save('a.png') 25 | Image.fromarray(b).save('b.png') 26 | Image.fromarray(c).save('c.png') 27 | Image.fromarray(d).save('d.png') 28 | Image.fromarray(e).save('e.png') 29 | Image.fromarray(f).save('f.png') 30 | -------------------------------------------------------------------------------- /puzzle/pieces.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/puzzle/pieces.zip -------------------------------------------------------------------------------- /puzzle/puzzle.py: -------------------------------------------------------------------------------- 1 | 2 | from PIL import Image 3 | import numpy as np 4 | 5 | a = np.array(Image.open('a.png')) 6 | b = np.array(Image.open('b.png')) 7 | print(a.shape, b.shape) 8 | 9 | im = np.vstack([a,b]) # vertical merge (correct) 10 | print(im.shape) 11 | Image.fromarray(im).save('vertical.png') 12 | 13 | im = np.hstack([a,b]) # horizontal merge (WRONG) 14 | print(im.shape) 15 | Image.fromarray(im).save('horizontal.png') -------------------------------------------------------------------------------- /random_tiles/README.rst: -------------------------------------------------------------------------------- 1 | Random Tiles 2 | ============ 3 | 4 | The ``np.random`` module contains many random functions. We can use them 5 | to generate random pixels: 6 | 7 | |image0| 8 | 9 | The ``np.random.randint()`` function uses the uniform distribution. 10 | The ``np.kron()`` calculates the `Kronecker Product `__ . 11 | We use it here to scale up the image. 12 | 13 | .. literalinclude:: random_tiles.py 14 | 15 | Print the array to take a look at the values: 16 | 17 | .. code:: python3 18 | 19 | print(a) 20 | 21 | ---- 22 | 23 | Challenges: 24 | ----------- 25 | 26 | - change the boundaries in the ``randint()`` function to create low 27 | amplitude noise 28 | - create grayscale colors 29 | - discretize the color values to use only values divisible by 16 30 | - reproduce images in the style of `1024 Colors `__ by **Gerhard Richter** 31 | 32 | 33 | .. |image0| image:: random_tiles.png 34 | 35 | -------------------------------------------------------------------------------- /random_tiles/random_tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/random_tiles/random_tiles.png -------------------------------------------------------------------------------- /random_tiles/random_tiles.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | 5 | a = np.random.randint(0, 255, size=(10, 10, 3), dtype=np.uint8) 6 | a = np.kron(a, np.ones((20, 20, 1), dtype=a.dtype)) 7 | 8 | im = Image.fromarray(a) 9 | im.save('random_colors.png') 10 | -------------------------------------------------------------------------------- /repeat/README.rst: -------------------------------------------------------------------------------- 1 | Repeat 2 | ====== 3 | 4 | |image0| 5 | 6 | There are probably some Numpy tricks to do this without the for loop. 7 | 8 | Whether the code gets any easier remains to be seen. 9 | 10 | .. literalinclude:: repeat.py 11 | 12 | ---- 13 | 14 | Challenge 15 | --------- 16 | 17 | Put together your own or random colors: 18 | 19 | |image1| 20 | 21 | .. |image0| image:: ../images/repeat.png 22 | .. |image1| image:: ../images/repeat_challenge.png 23 | -------------------------------------------------------------------------------- /repeat/repeat.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import numpy as np 3 | 4 | N_TILES = 4 5 | TILE_WIDTH = 125 6 | SPACING = 15 7 | SIZE = TILE_WIDTH * N_TILES + SPACING 8 | 9 | a = np.zeros((SIZE, SIZE, 3), dtype=np.uint8) 10 | 11 | for x in range(N_TILES): 12 | for y in range(N_TILES): 13 | color = (50 * y + 50, 50 * x + 50, 0) 14 | a[ 15 | SPACING + y * TILE_WIDTH : (y + 1) * TILE_WIDTH, 16 | SPACING + x * TILE_WIDTH : (x + 1) * TILE_WIDTH, 17 | ] = color 18 | 19 | im = Image.fromarray(a, "RGB") 20 | im.save("repeat.png") 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | imageio 2 | pillow 3 | pandas 4 | numpy 5 | scikit-learn 6 | opencv-python>=4.4 7 | -------------------------------------------------------------------------------- /rgb/README.rst: -------------------------------------------------------------------------------- 1 | Color 2 | ===== 3 | 4 | With a three-dimensional array you get a RGB image. 5 | 6 | |image0| 7 | 8 | Here is the code that creates the image: 9 | 10 | .. literalinclude:: rgb.py 11 | 12 | .. note:: 13 | 14 | - the three dimensions are ``(y-size, x-size, colorchannel)`` 15 | - the third dimension has to have the size 3 16 | - the three color channels are *red* ``[0]``, *green* ``[1]``, *blue* 17 | ``[2]`` 18 | - a 0 in a color channel means that color is inactive 19 | - a 255 in a color channel means that the color is at maximum 20 | saturation 21 | 22 | Using sliced assignments, you can modify each color channel separately. 23 | 24 | ---- 25 | 26 | Challenges: 27 | ----------- 28 | 29 | - create a pure green square 30 | - create a purple rectangle 31 | - create an orange rectangle 32 | - create a white image 33 | 34 | |image1| 35 | 36 | .. |image0| image:: ../images/pink.png 37 | .. |image1| image:: ../images/rgb_challenge.png 38 | 39 | -------------------------------------------------------------------------------- /rgb/rgb.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | 5 | a = np.zeros((200, 200, 3), dtype=np.uint8) 6 | a[:, :, 0] += 255 7 | a[:, :, 2] += 128 8 | 9 | im = Image.fromarray(a) 10 | im.save('pink.png') 11 | -------------------------------------------------------------------------------- /rotate/README.rst: -------------------------------------------------------------------------------- 1 | Rotation 2 | ======== 3 | 4 | |image0| 5 | 6 | The image consists of many rotated rectangles copied on top of each 7 | other. 8 | 9 | .. literalinclude:: rotate.py 10 | 11 | ---- 12 | 13 | Challenge 14 | --------- 15 | 16 | Create a caleidoscope image: 17 | 18 | |image1| 19 | 20 | .. |image0| image:: ../images/rotate.png 21 | .. |image1| image:: ../images/caleidoscope.png 22 | 23 | -------------------------------------------------------------------------------- /rotate/caleidoscope.py: -------------------------------------------------------------------------------- 1 | import imageio 2 | import numpy as np 3 | from scipy import ndimage 4 | 5 | b = imageio.imread("bbtor.jpg")[::2,::2] 6 | b.shape 7 | a = np.zeros((1200, 1200, 3), np.uint8) 8 | s = b[200:440, 280:680] 9 | 10 | a[200:440, 400:800] = s 11 | 12 | r1 = ndimage.rotate(s, 120) 13 | r2 = ndimage.rotate(s, 240) 14 | 15 | x, y = 600, 440 16 | a[y:y+r.shape[0], x-r.shape[1]:x] = r1 17 | a[y:y+r.shape[0], x:x+r.shape[1]] = r2 18 | 19 | c = a[::-2,::2] 20 | a[240:840,300:900] |= c 21 | 22 | imageio.imsave('caleidoscope.png', a) 23 | -------------------------------------------------------------------------------- /rotate/rotate.py: -------------------------------------------------------------------------------- 1 | import imageio 2 | import numpy as np 3 | from scipy import ndimage 4 | 5 | a = np.zeros((600, 600, 3), dtype=np.uint8) 6 | rect = np.zeros((400, 400, 3), dtype=np.uint8) 7 | blue = np.array([0, 127, 255]) 8 | rect[100:300,:] = blue // 9 9 | 10 | for i in range(0, 180, 10): 11 | r = ndimage.rotate(rect, i) 12 | x = 300 - r.shape[1] // 2 13 | y = 300 - r.shape[0] // 2 14 | a[y:y+r.shape[0], x:x+r.shape[1]] += r 15 | 16 | imageio.imsave('rotate.png', a) 17 | -------------------------------------------------------------------------------- /sand/README.rst: -------------------------------------------------------------------------------- 1 | Sand blows away 2 | =============== 3 | 4 | |image0| 5 | 6 | How does it work 7 | ---------------- 8 | 9 | The basic mechanics of the animation are: 10 | 11 | - every location of the image is covered by a grain of sand 12 | - all grains are black while not moving 13 | - every grain waits for a random time before it starts to move 14 | - the movement starts slowly in a random direction 15 | - a grain of sand has a color as soon as it starts to move 16 | - every frame, the movement gets accelerated to the right 17 | 18 | To make the animated GIF smaller, the image was shrunk and cropped. 19 | 20 | The `Obi-Wan Kenobi Series by Disney `__ 21 | has a much better animation that inspired me to write my own. 22 | 23 | Prerequisites 24 | ------------- 25 | 26 | This script requires two libraries: **OpenCV** for displaying the live 27 | animation and **imageio** for exporting animated GIFs. 28 | 29 | :: 30 | 31 | pip install opencv-python 32 | pip install imagio 33 | 34 | The Script 35 | ---------- 36 | 37 | .. literalinclude:: sand.py 38 | 39 | .. |image0| image:: sand_animation_small.gif 40 | 41 | -------------------------------------------------------------------------------- /sand/desert_python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/sand/desert_python.png -------------------------------------------------------------------------------- /sand/desert_python.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | PYTHON 74 | 75 | -------------------------------------------------------------------------------- /sand/sand.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | import numpy as np 4 | import cv2 5 | import time 6 | import imageio 7 | 8 | BLACK = (0, 0, 0) 9 | BEIGE = (128, 192, 255) 10 | 11 | GRAIN_SIZE = 3 # smaller looks better but also slower 12 | 13 | MAXX = 1200 14 | MAXY = 550 15 | 16 | 17 | class GrainOfSand: 18 | """ 19 | generates sand coordinates 20 | """ 21 | def __init__(self, x, y, xtarget): 22 | self._x = x 23 | self._y = y 24 | self.ymod = random.randint(-30, 30) / 10 25 | self.xtarget = xtarget 26 | self.xspeed = random.randint(-20, 20) / 10 27 | self.xgrowth = 1.1 # multiplier for xspeed 28 | self.delay = max(0, int(random.gauss(65, 20))) 29 | 30 | # randomize color a little 31 | self.color = list(BEIGE) 32 | self.color[0] += random.randint(-50, 50) 33 | self.color[1] += random.randint(-50, 50) 34 | self.color[2] += random.randint(-50, 0) 35 | 36 | @property 37 | def x(self): 38 | return int(self._x) 39 | 40 | @property 41 | def y(self): 42 | return int(self._y) 43 | 44 | @property 45 | def finished(self): 46 | return self._x >= self.xtarget or self._y < 0 or self._y >= MAXY 47 | 48 | @property 49 | def moving(self): 50 | return self.delay == 0 51 | 52 | def update(self): 53 | if self.delay > 0: 54 | self.delay -= 1 # waiting time before grain starts to move 55 | else: 56 | self._x += self.xspeed 57 | self._y += self.ymod 58 | if self.xspeed < 1.0: # redirect moves from left to right 59 | self.xspeed += 0.3 60 | else: 61 | self.xspeed *= self.xgrowth 62 | 63 | 64 | def create_grains(): 65 | grains = [] 66 | for x in range(300, 900, GRAIN_SIZE): 67 | for y in range(200, 350, GRAIN_SIZE): 68 | grains.append(GrainOfSand(x, y, MAXX-2)) 69 | return grains 70 | 71 | 72 | def draw_grain(frame, x, y, color): 73 | if x >= MAXX - 1: return 74 | frame[y:y+GRAIN_SIZE, x:x+GRAIN_SIZE] = color 75 | 76 | 77 | def move_grains(frame, grains): 78 | for g in grains: 79 | g.update() 80 | 81 | # draw static grains 82 | for g in grains: 83 | if not g.moving: 84 | draw_grain(frame, g.x, g.y, BLACK) 85 | 86 | # draw moving grains 87 | for g in grains: 88 | if g.moving: 89 | draw_grain(frame, g.x, g.y, g.color) 90 | 91 | return [g for g in grains if not g.finished] 92 | 93 | 94 | background = np.zeros((MAXY, MAXX, 3), np.uint8) 95 | text = cv2.imread('desert_python.png') 96 | background[200:350, 300:853] = text 97 | grains = create_grains() 98 | 99 | 100 | frames = [] 101 | while True: 102 | # display frames 103 | frame = background.copy() 104 | grains = move_grains(frame, grains) 105 | cv2.imshow('frame', frame) 106 | 107 | # shrink frame for using it on the web 108 | rgb = cv2.cvtColor(frame[100:450:2, 200:-200:2], cv2.COLOR_BGR2RGB) 109 | frames.append(rgb) 110 | 111 | key = chr(cv2.waitKey(1) & 0xFF) 112 | if key == 'q': 113 | break 114 | 115 | #time.sleep(0.03) 116 | 117 | cv2.destroyAllWindows() 118 | 119 | 120 | imageio.mimsave('sand_animation.gif', frames[::2], fps=20) -------------------------------------------------------------------------------- /sand/sand_animation_small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/sand/sand_animation_small.gif -------------------------------------------------------------------------------- /shadow/README.rst: -------------------------------------------------------------------------------- 1 | Shadow 2 | ====== 3 | 4 | |image0| 5 | 6 | How the image is created 7 | ------------------------ 8 | 9 | 1. select all blue pixels above a threshold as a mask 10 | 2. create a shadow image that is gray outside and white inside the mask 11 | 3. move the shadow by 20 pixels 12 | 4. copy the shadow onto the white part of the main picture 13 | 14 | Note that some blue pixels do not turn white, because changing the 15 | threshold would distort the gate too much. 16 | 17 | .. literalinclude:: shadow.py 18 | 19 | .. |image0| image:: ../images/shadow.png 20 | 21 | -------------------------------------------------------------------------------- /shadow/shadow.py: -------------------------------------------------------------------------------- 1 | 2 | from PIL import Image 3 | import numpy as np 4 | 5 | a = np.array(Image.open("bbtor.jpg")) 6 | 7 | mask = a[:,:,2] > 180 8 | 9 | shadow = np.ones(a.shape, dtype=np.uint8) * 127 10 | shadow[mask] = 255 11 | shadow[20:, 20:] = shadow[:-20, :-20] 12 | 13 | a[mask] = shadow[mask] 14 | Image.fromarray(a, 'RGB').save("shadow.png") 15 | -------------------------------------------------------------------------------- /sobel/README.rst: -------------------------------------------------------------------------------- 1 | Edge Detection 2 | ============== 3 | 4 | |image0| 5 | 6 | The **Sobel Operator** is a simple procedure for detecting edges in an 7 | image. It uses two *convolutional kernels* (3x3 matrices) that are run 8 | over the image. The result of the sobel operator is the Euclidean length 9 | of the dot products from both kernels. 10 | 11 | To my understanding, running the double for loop over the image is very 12 | hard to avoid. But I am happy to get myself disproved. 13 | 14 | .. literalinclude:: sobel.py 15 | 16 | ---- 17 | 18 | Challenges 19 | ---------- 20 | 21 | - Try your own image 22 | - Use only one of the kernels, so that you detect horizontal *or* 23 | vertical lines 24 | - Apply the Sobel operator on one color channel at a time 25 | 26 | .. |image0| image:: ../images/sobel.png 27 | 28 | -------------------------------------------------------------------------------- /sobel/sobel.py: -------------------------------------------------------------------------------- 1 | 2 | from PIL import Image 3 | import numpy as np 4 | 5 | im = Image.open('../bbtor.jpg').convert('L') 6 | 7 | a = np.array(im)[::2, ::2] 8 | 9 | gx = np.array([[-1, 0, 1], 10 | [-2, 0, 2], 11 | [-1, 0, 1]]) 12 | 13 | gy = np.array([[-1, -2, -1], 14 | [ 0, 0, 0], 15 | [ 1, 2, 1]]) 16 | 17 | sobel = np.zeros(a.shape) 18 | 19 | for y in range(a.shape[0]-2): 20 | for x in range(a.shape[1]-2): 21 | sx = np.sum(gx * a[y:y+3, x:x+3]) 22 | sy = np.sum(gx * a[y:y+3, x:x+3]) 23 | 24 | sobel[y, x] = np.sqrt(sx**2 + sy**2) 25 | 26 | snorm = 255 * sobel / sobel.max() 27 | 28 | b = snorm.astype(np.uint8) 29 | im = Image.fromarray(b) 30 | im.save('sobel.png') 31 | -------------------------------------------------------------------------------- /solutions/circle_challenge.py: -------------------------------------------------------------------------------- 1 | 2 | import imageio 3 | import numpy as np 4 | 5 | def circle(a, xcenter, ycenter, radius, color): 6 | Y, X = np.ogrid[0:a.shape[0], 0:a.shape[1]] 7 | square_dist = (X - xcenter) ** 2 + (Y - ycenter) ** 2 8 | mask = square_dist < radius ** 2 9 | a[mask] = color 10 | 11 | circles = [ 12 | (250, np.array([127, 0, 0], np.uint8)), 13 | (200, np.array([255, 0, 0], np.uint8)), 14 | (175, np.array([255, 64, 0], np.uint8)), 15 | (150, np.array([255, 128, 0], np.uint8)), 16 | (125, np.array([255, 192, 0], np.uint8)), 17 | (100, np.array([255, 255, 0], np.uint8)), 18 | (75, np.array([255, 255, 127], np.uint8)), 19 | (50, np.array([255, 255, 255], np.uint8)), 20 | ] 21 | 22 | a = np.zeros((1200, 1200, 3), np.uint8) 23 | b = np.zeros((600, 600, 3), np.uint8) 24 | c = np.zeros((600, 600, 3), np.uint8) 25 | for r, col in circles: 26 | circle(b, 300, 300, r, col) 27 | circle(c, 300, 550-r, r, col) 28 | circle(a, 600+r, 600, r, col) 29 | circle(a, 600-r, 600, r, col) 30 | circle(a, 600, 600+r, r, col) 31 | circle(a, 600, 600-r, r, col) 32 | 33 | imageio.imsave('circle_challenge.png', a) 34 | imageio.imsave('circle_challenge2.png', b) 35 | imageio.imsave('circle_challenge3.png', c) 36 | -------------------------------------------------------------------------------- /solutions/dots.py: -------------------------------------------------------------------------------- 1 | 2 | import imageio 3 | import numpy as np 4 | 5 | a = np.zeros((400, 400), dtype=np.uint8) 6 | xcoord = np.random.randint(0, 399, size=(10000)) 7 | ycoord = np.random.normal(200, 30, size=(10000)).astype(np.int32) 8 | 9 | a[xcoord, ycoord] = 255 10 | 11 | imageio.imsave('dots.png', a) 12 | -------------------------------------------------------------------------------- /solutions/four_triangles.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import imageio 4 | 5 | a = np.zeros((300, 300, 3), dtype=np.uint8) 6 | 7 | x = np.arange(200).reshape(200, 1) 8 | y = x + x.T 9 | y[y <= 200] = 1 10 | y[y > 200] = 0 11 | 12 | a[:200, :200, 0] = 200 * y 13 | a[100:300, :200, 1] = 200 * y[::-1] 14 | a[:200, 100:300, 2] = 200 * y[:,::-1] 15 | a[100:300, 100:300, 0] = 200 * y[::-1,::-1] 16 | 17 | imageio.imsave('triangles.png', a) 18 | -------------------------------------------------------------------------------- /solutions/gradient_challenge.py: -------------------------------------------------------------------------------- 1 | 2 | from PIL import Image 3 | import numpy as np 4 | 5 | a = np.zeros((600, 600, 3), dtype=np.uint8) 6 | 7 | full = np.zeros((200, 600), dtype=np.uint8) 8 | full[:200, :] = np.linspace(0, 255, 600, dtype=np.uint8) 9 | 10 | a[:200, :, 0] = full 11 | a[400:, ::-1, 1] = full 12 | a[400:, :, 2] = full 13 | 14 | a[::-1, :200, 2] = full.T 15 | a[:, 400:, 1] = full.T 16 | 17 | im = Image.fromarray(a) 18 | im.save('gradient_square.png') 19 | -------------------------------------------------------------------------------- /solutions/naumgabo.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | import math 5 | 6 | 7 | def interpolate(astart, aend, bstart, bend): 8 | """ 9 | Returns arrays of x/y positions given start/end 10 | coordinates of a longer and shorter side 11 | """ 12 | a = np.arange(astart, aend) 13 | slope = (bend - bstart) / (aend - astart) 14 | b = bstart + ((a - astart) * slope).round() 15 | b = b.astype(np.int32) 16 | return a, b 17 | 18 | 19 | def draw_line(a, xstart, ystart, xend, yend, color): 20 | if xstart == xend: 21 | # vertical line 22 | x = xstart 23 | y = np.arange(*sorted((ystart, yend))) 24 | elif ystart == yend: 25 | # horizontal line 26 | x = np.arange(*sorted((xstart, xend))) 27 | y = ystart 28 | elif abs(xend - xstart) > abs(yend - ystart): 29 | # x wider than y 30 | (xstart, ystart), (xend, yend) = sorted([(xstart, ystart), (xend, yend)]) 31 | x, y = interpolate(xstart, xend, ystart, yend) 32 | else: 33 | # y wider than x or same 34 | (ystart, xstart), (yend, xend) = sorted([(ystart, xstart), (yend, xend)]) 35 | y, x = interpolate(ystart, yend, xstart, xend) 36 | a[y, x, color] = 255 37 | 38 | 39 | a = np.zeros((500, 1200, 3), dtype=np.uint8) 40 | 41 | for i in range(1, 1201, 10): 42 | xstart = 0 43 | ystart = 250 44 | xend = i 45 | color = i % 3 46 | yend = int(250 + math.sin(math.pi*xend/180) * 190) 47 | draw_line(a, xstart, ystart, xend, yend, color) 48 | 49 | im = Image.fromarray(a) 50 | im.save('naumgabo.png') 51 | -------------------------------------------------------------------------------- /solutions/repeat_random.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import random 3 | import numpy as np 4 | 5 | X_TILES = 32 6 | Y_TILES = 12 7 | TILE_WIDTH = 25 8 | SPACING = 5 9 | XSIZE = TILE_WIDTH * X_TILES + SPACING 10 | YSIZE = TILE_WIDTH * Y_TILES + SPACING 11 | 12 | a = np.zeros((YSIZE, XSIZE, 3), dtype=np.uint8) 13 | a[:,:] = (128, 128, 128) 14 | 15 | def random_color(): 16 | return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) 17 | 18 | color_list = [random_color() for i in range(12)] 19 | for x in range(X_TILES): 20 | for y in range(Y_TILES): 21 | random.shuffle(color_list) 22 | color = color_list[0] 23 | a[SPACING + y*TILE_WIDTH:(y+1)*TILE_WIDTH, 24 | SPACING + x*TILE_WIDTH:(x+1)*TILE_WIDTH] = color 25 | 26 | im = Image.fromarray(a, 'RGB') 27 | im.save("repeat.png") 28 | im -------------------------------------------------------------------------------- /solutions/spiral_challenge.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | import math 5 | 6 | 7 | def circle(a, xcenter, ycenter, radius, color): 8 | Y, X = np.ogrid[0:a.shape[0], 0:a.shape[1]] 9 | square_dist = (X - xcenter) ** 2 + (Y - ycenter) ** 2 10 | mask = square_dist < radius ** 2 11 | a[mask] = color 12 | 13 | 14 | def spiral( 15 | xcenter, 16 | ycenter, 17 | angle=0, 18 | radius=1, 19 | angle_step=10, 20 | radius_step=0.7 21 | ): 22 | """generates spiral coordinates""" 23 | while True: 24 | angle += angle_step 25 | radius += radius_step 26 | yield ( 27 | round(math.cos(angle * math.pi / 180) * radius + xcenter), 28 | round(math.sin(angle * math.pi / 180) * radius + ycenter) 29 | ) 30 | 31 | 32 | circles = [ 33 | (250, np.array([127, 0, 0], np.uint8)), 34 | (200, np.array([255, 0, 0], np.uint8)), 35 | (175, np.array([255, 64, 0], np.uint8)), 36 | (150, np.array([255, 128, 0], np.uint8)), 37 | (125, np.array([255, 192, 0], np.uint8)), 38 | (100, np.array([255, 255, 0], np.uint8)), 39 | (75, np.array([255, 255, 127], np.uint8)), 40 | (50, np.array([255, 255, 255], np.uint8)), 41 | ] 42 | 43 | 44 | a = np.zeros((800, 800, 3), dtype=np.uint8) 45 | spiral_gen = spiral( 46 | xcenter=400, 47 | ycenter=400, 48 | angle=90, 49 | radius=1, 50 | angle_step=20, 51 | radius_step=6 52 | ) 53 | 54 | for i in range(50): 55 | x, y = next(spiral_gen) 56 | radius = i // 2 57 | circle(a, x, y, radius, (0, abs(i*10 - 250), 0)) 58 | circle(a, 800-x, 800-y, radius, (abs(i*10 - 250), 0, 0)) 59 | 60 | 61 | im = Image.fromarray(a) 62 | im.save('spiral_challenge.png') 63 | -------------------------------------------------------------------------------- /spiral/README.rst: -------------------------------------------------------------------------------- 1 | Spiral 2 | ====== 3 | 4 | |image0| 5 | 6 | To draw a spiral you can reuse the :ref:`line drawing function `: 7 | 8 | .. literalinclude:: spiral.py 9 | 10 | When drawing a spiral it is key to **start in the middle**. 11 | This allows to catch the essential properties of a spiral in code: 12 | 13 | * it rotates in circles 14 | * the radius of the circle becomes bigger and bigger 15 | * the spiral is potentially endless 16 | 17 | The angle and radius are **polar coordinates**. 18 | You can convert them to **cartesian coordinates** through trigonometric functions. 19 | A **generator function** is a good solution to model a *potentially endless* spiral. 20 | The line drawing function takes care of the rest. 21 | 22 | ---- 23 | 24 | Challenge 25 | --------- 26 | 27 | Become creative with the spiral function: 28 | 29 | |image1| 30 | 31 | .. |image0| image:: spiral.png 32 | .. |image1| image:: ../images/spiral_challenge.png 33 | -------------------------------------------------------------------------------- /spiral/lines.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | 5 | 6 | def draw_line(a, xstart, ystart, xend, yend): 7 | npoints = max(abs(xend - xstart) + 1, abs(yend - ystart) + 1) 8 | x = np.linspace(xstart, xend, npoints).round().astype(int) 9 | y = np.linspace(ystart, yend, npoints).round().astype(int) 10 | a[y, x] = 255 11 | -------------------------------------------------------------------------------- /spiral/spiral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/spiral/spiral.png -------------------------------------------------------------------------------- /spiral/spiral.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | import math 5 | from lines import draw_line 6 | 7 | 8 | def spiral( 9 | xcenter, 10 | ycenter, 11 | angle=0, 12 | radius=1, 13 | angle_step=10, 14 | radius_step=0.7 15 | ): 16 | """generates spiral coordinates""" 17 | while True: 18 | angle += angle_step 19 | radius += radius_step 20 | yield ( 21 | round(math.cos(angle * math.pi / 180) * radius + xcenter), 22 | round(math.sin(angle * math.pi / 180) * radius + ycenter) 23 | ) 24 | 25 | 26 | a = np.zeros((400, 400, 3), dtype=np.uint8) 27 | x, y = 200, 200 28 | spiral_gen = spiral(x, y) 29 | 30 | for i in range(250): 31 | xnew, ynew = next(spiral_gen) 32 | draw_line(a, x, y, xnew, ynew) 33 | x, y = xnew, ynew 34 | 35 | im = Image.fromarray(a) 36 | im.save('spiral.png') 37 | -------------------------------------------------------------------------------- /stars/README.rst: -------------------------------------------------------------------------------- 1 | Stars 2 | ===== 3 | 4 | **Index arrays with integer arrays to create dots.** 5 | 6 | |image0| 7 | 8 | .. literalinclude:: stars.py 9 | 10 | ---- 11 | 12 | Challenge: 13 | ---------- 14 | 15 | Create point clouds using other distribution functions. 16 | 17 | |image1| 18 | 19 | .. |image0| image:: ../images/stars.png 20 | .. |image1| image:: ../images/dots.png 21 | -------------------------------------------------------------------------------- /stars/stars.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | 5 | a = np.zeros((400, 400), dtype=np.uint8) 6 | x = np.random.randint(0, 399, size=(100)) 7 | y = np.random.randint(0, 399, size=(100)) 8 | 9 | a[y, x] = 255 10 | 11 | im = Image.fromarray(a) 12 | im.save('stars.png') 13 | -------------------------------------------------------------------------------- /starwars/README.rst: -------------------------------------------------------------------------------- 1 | Star Wars Titles 2 | ================ 3 | 4 | Have you ever wondered how you could produce an animation like in the 5 | Star Wars main titles? 6 | 7 | Of course this can be done in Python: 8 | 9 | |image0| 10 | 11 | How it works: 12 | ------------- 13 | 14 | The text uses the *“Zen of Python”* (obtained via ``import this`` and 15 | adding a few line breaks). First, a plain image of the text is created. 16 | In the script, OpenCV is used, but you can do the same with any image of 17 | your choice, as long as the image is sufficiently long. 18 | 19 | |image1| 20 | 21 | The main challenge is to calculate the perspective. For every pixel *(x, 22 | y)* on the screen, you want to know what is the closest pixel *(x0, y0)* 23 | on the text image. The equations to calculate those are: 24 | 25 | .. math:: 26 | 27 | x0 = x - \frac{x \cdot y}{y-1} 28 | 29 | .. math:: 30 | 31 | y0 = - \frac{y \cdot c}{y-1} 32 | 33 | where 34 | 35 | - **y** is a float in the range of 0..1. 36 | - **x**, **x0** and **y0** are absolute pixel positions 37 | - **c** is a constant for the distance of the observer. It defines how *“steep”* the trapezoid will be 38 | 39 | In practice you need to do some scaling of these numbers to center the 40 | animation on the screen. 41 | 42 | There is probably a more efficient way to calculate the perspective 43 | (using more linear algebra). But I found this solution from scratch on a 44 | piece of paper and made it work fast enough. 45 | 46 | Installation 47 | ------------ 48 | 49 | The script requires the **OpenCV** library for displaying the live 50 | animation. A second script in the repository uses **imageio** for 51 | exporting animated GIFs. 52 | 53 | :: 54 | 55 | pip install opencv-python 56 | pip install imageio 57 | 58 | The code 59 | -------- 60 | 61 | .. literalinclude:: starwars.py 62 | 63 | .. |image0| image:: sw_animation.gif 64 | .. |image1| image:: text.png 65 | 66 | -------------------------------------------------------------------------------- /starwars/message.txt: -------------------------------------------------------------------------------- 1 | The Zen of Python, by Tim Peters 2 | 3 | Beautiful is better than ugly. 4 | Explicit is better than implicit. 5 | Simple is better than complex. 6 | Complex is better than complicated. 7 | Flat is better than nested. 8 | Sparse is better than dense. 9 | Readability counts. 10 | Special cases aren't special enough 11 | to break the rules. 12 | Although practicality beats purity. 13 | Errors should never pass silently. 14 | Unless explicitly silenced. 15 | In the face of ambiguity, refuse the 16 | temptation to guess. 17 | There should be one-- and preferably 18 | only one --obvious way to do it. 19 | Although that way may not be obvious 20 | at first unless you're Dutch. 21 | Now is better than never. 22 | Although never is often better than 23 | *right* now. 24 | If the implementation is hard to 25 | explain, it's a bad idea. 26 | If the implementation is easy to 27 | explain, it may be a good idea. 28 | Namespaces are one honking great 29 | idea -- let's do more of those! 30 | -------------------------------------------------------------------------------- /starwars/starwars.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import numpy as np 3 | import cv2 4 | import time 5 | import imageio 6 | 7 | 8 | XSIZE = 1400 # width of the screen 9 | YSIZE = 800 10 | TEXT_YSIZE = 3000 # length of the bitmap that is scrolled through 11 | 12 | YELLOW = (0, 255, 255) 13 | WHITE = (255, 255, 255) 14 | 15 | 16 | def create_text_bitmap(fn, color=YELLOW, line_spacing=50): 17 | """ 18 | returns a numpy array that contains the entire text from the file 19 | """ 20 | text = open('message.txt') 21 | msg = np.zeros((TEXT_YSIZE, XSIZE, 3), np.uint8) 22 | for y, line in enumerate(text): 23 | cv2.putText( 24 | msg, 25 | line.strip(), 26 | (50, y * line_spacing + line_spacing), 27 | cv2.FONT_HERSHEY_TRIPLEX, 28 | fontScale=1.5, 29 | color=color, 30 | thickness=3 31 | ) 32 | return msg 33 | 34 | 35 | def prepare_index_arrays(c=300): 36 | """ 37 | pre-calculate index arrays mapping each row 38 | from the text bitmap to the perspective. 39 | This greatly speeds up display. 40 | 41 | c : distance of the observer 42 | 43 | returns a dictionary of {y-position: numpy array} 44 | """ 45 | indices = {} 46 | xx = np.arange(0, XSIZE) 47 | for yy in range(1, YSIZE): 48 | y = 1 - yy / YSIZE 49 | y0 = int(-y * c / (y - 1)) 50 | 51 | x = xx - XSIZE // 2 52 | x0 = (x - (x * y) / (y - 1)).astype(int) 53 | x0 += 600 54 | 55 | idx = np.where((x0 >= 0) * (x0 < XSIZE)) 56 | src = x0[idx] 57 | dest = xx[idx] 58 | indices[yy] = (y0, src, dest) 59 | return indices 60 | 61 | 62 | msg = create_text_bitmap('message.txt', YELLOW) 63 | indices = prepare_index_arrays() 64 | 65 | ofs = 0 # vertical shift of the text 66 | background = np.zeros((YSIZE, XSIZE, 3), np.uint8) 67 | frames = [] 68 | 69 | while True: 70 | # display frames 71 | frame = background.copy() 72 | for yy in range(1, YSIZE): 73 | y0, src, dest = indices[yy] 74 | if 0 <= y0 < YSIZE: 75 | frame[yy][dest] = msg[-y0 + ofs][src] 76 | 77 | cv2.imshow('frame', frame) 78 | ofs += 1 79 | 80 | key = chr(cv2.waitKey(1) & 0xFF) 81 | if key == 'q': 82 | break 83 | 84 | # shrink frame for using it on the web 85 | #rgb = cv2.cvtColor(frame[200::2, 100:-100:2], cv2.COLOR_BGR2RGB) 86 | #frames.append(rgb) 87 | 88 | #time.sleep(0.03) 89 | 90 | cv2.destroyAllWindows() 91 | # imageio.mimsave('sw_animation.gif', frames, fps=20) -------------------------------------------------------------------------------- /starwars/sw_animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/starwars/sw_animation.gif -------------------------------------------------------------------------------- /starwars/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/starwars/text.png -------------------------------------------------------------------------------- /thank_you/README.rst: -------------------------------------------------------------------------------- 1 | Thank You 2 | ========= 3 | 4 | Here is a simple assembly of a **Thank You** image. 5 | 6 | |image0| 7 | 8 | How it works: 9 | ------------- 10 | 11 | The script uses two images with characters, and one with the bubble: 12 | 13 | |image1| 14 | 15 | |image2| 16 | 17 | |image3| 18 | 19 | The main logic is to use a sine function to calculate smooth movements. 20 | For frame *i* the coordinate *x* would be: 21 | 22 | .. math:: 23 | 24 | x = sin(\frac{i}{i_{max}} \cdot (end - start)) + start 25 | 26 | At the end, the image is cropped, so that the logo parts seem to move in from the outside. 27 | 28 | Installation 29 | ------------ 30 | 31 | The script requires the **OpenCV** library for displaying the live 32 | animation and **imageio** for 33 | exporting animated GIFs. 34 | 35 | :: 36 | 37 | pip install opencv-python 38 | pip install imageio 39 | 40 | The code 41 | -------- 42 | 43 | .. literalinclude:: thank_you.py 44 | 45 | .. |image0| image:: thank_you_animation.gif 46 | 47 | .. |image1| image:: panda.png 48 | 49 | .. |image2| image:: pingu.png 50 | 51 | .. |image3| image:: bubble.png 52 | -------------------------------------------------------------------------------- /thank_you/bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/thank_you/bubble.png -------------------------------------------------------------------------------- /thank_you/panda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/thank_you/panda.png -------------------------------------------------------------------------------- /thank_you/pingu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/thank_you/pingu.png -------------------------------------------------------------------------------- /thank_you/thank_you.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | import math 4 | import imageio 5 | import time 6 | 7 | BLACK = (0, 0, 0) 8 | 9 | FRAME1 = 80 10 | FRAME2 = 20 11 | FRAME3 = 10 12 | 13 | MAXX = 2000 14 | MAXY = 800 15 | YCHARS = 300 16 | XCENTER = 1000 - 341 // 2 17 | 18 | def sine_move(start, end, frames, wait_frames=0): 19 | """ 20 | smooth movement from a to b using a sine function. 21 | remain in end position forever 22 | """ 23 | for _ in range(wait_frames): 24 | yield start 25 | for i in range(frames): 26 | angle_rad = i / frames * math.pi / 2 27 | if end > start: 28 | value = int(math.sin(angle_rad) * (end - start) + start) 29 | else: 30 | value = start - int(math.sin(angle_rad) * (start - end)) 31 | yield value 32 | while True: 33 | yield value 34 | 35 | 36 | def create_text(): 37 | text = np.zeros((100, 300, 3), np.uint8) + 255 38 | return cv2.putText( 39 | text, 40 | "Thank You", 41 | (10, 50), 42 | cv2.FONT_HERSHEY_TRIPLEX, 43 | fontScale=1.0, 44 | color=BLACK, 45 | thickness=2, 46 | ) 47 | 48 | 49 | background = np.zeros((MAXY, MAXX, 3), np.uint8) + 255 50 | panda = cv2.imread("panda.png")[::2, ::2] 51 | pingu = cv2.imread("pingu.png")[::2, ::2] 52 | bubble = cv2.imread("bubble.png")[::2, ::2] 53 | ysize, xsize, _ = panda.shape 54 | text = create_text() 55 | 56 | frames = [] 57 | xgen = sine_move(0, XCENTER, FRAME1) 58 | ygen = sine_move(0, 170, FRAME2, wait_frames=FRAME1) 59 | fade = sine_move(start=255, end=0, frames=50, wait_frames=FRAME1 + FRAME2 + 5) 60 | 61 | while True: 62 | # display frames 63 | frame = background.copy() 64 | xpanda = next(xgen) 65 | xpingu = MAXX - pingu.shape[1] - xpanda 66 | frame[YCHARS: YCHARS + ysize, xpanda : xpanda + xsize] = panda 67 | frame[YCHARS: YCHARS + ysize, xpingu : xpingu + xsize] &= pingu 68 | frame[YCHARS-150: YCHARS-150 + bubble.shape[0], XCENTER: XCENTER + bubble.shape[1]] =\ 69 | np.maximum(bubble, next(fade)) 70 | ytext = next(ygen) 71 | frame[ytext: ytext + 100, XCENTER + 40:XCENTER + 340] &= text 72 | frame = frame[100:550, 500:-500] # crop 73 | cv2.imshow("frame", frame) 74 | 75 | key = chr(cv2.waitKey(1) & 0xFF) 76 | if key == "q": 77 | break 78 | 79 | time.sleep(0.03) 80 | 81 | # shrink frame for using it on the web 82 | rgb = cv2.cvtColor(frame[::2, ::2], cv2.COLOR_BGR2RGB) 83 | frames.append(rgb) 84 | 85 | cv2.destroyAllWindows() 86 | # imageio.mimsave('thank_you_animation.gif', frames, fps=25) 87 | -------------------------------------------------------------------------------- /thank_you/thank_you_animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/generative_art/0de2258aeed8f415e8fb579d5237a6ccd0c380cf/thank_you/thank_you_animation.gif -------------------------------------------------------------------------------- /transform_logo.py: -------------------------------------------------------------------------------- 1 | # 2 | # more examples that transform the Python logo 3 | # 4 | 5 | from scipy.ndimage import imread, rotate 6 | from scipy.misc import imsave 7 | import numpy as np 8 | 9 | 10 | a = imread('python_logo.png', mode='RGB') 11 | 12 | # dim 13 | d = a // 2 14 | imsave('dim.png', d) 15 | 16 | 17 | b = a[:,::-1,:] 18 | imsave('flip.png', b) 19 | 20 | g = np.array(a) 21 | g[:,:,1] = 0 22 | imsave('purple.png', g) 23 | 24 | # rotation 25 | c = rotate(a, 30, reshape=False) 26 | c[c == 0] = 255 27 | imsave('rotate.png', c) 28 | 29 | # displacement blur 30 | blur = np.array(a, np.int32) 31 | factor = np.array([8], np.int32) 32 | 33 | blur[5:,:] += a[:-5,:] * factor 34 | blur[:,5:] += a[:,:-5] * factor 35 | blur[:-5,:] += a[5:,:] * factor 36 | blur[:,:-5] += a[:,5:] * factor 37 | blur //= 33 38 | imsave('blur.png', blur) 39 | 40 | # rolling displacement 41 | roll = np.array(a) 42 | roll[:,:,2] = np.roll(roll[:,:,2], 25) 43 | roll[:,:,1] = np.roll(roll[:,:,1], 50) 44 | imsave('roll.png', roll) 45 | 46 | 47 | from numpy.random import randint 48 | 49 | rr = randint(0, 10, (200, 200, 3)) 50 | rr = a * rr // 10 51 | imsave('rand.png', rr) 52 | 53 | spaced = np.array(a, np.int64) 54 | spaced[::3,::3,:] *= rr[::3,::3,:] 55 | spaced[::3,::3,:] //= 10 56 | imsave('spaced.png', spaced) 57 | 58 | 59 | s = np.array(a) 60 | s[75:,75:,0] = 0 61 | s[:125,:125,1] = 0 62 | s[:125,75:,2] = 0 63 | imsave('square.png', s) 64 | 65 | # circle 66 | xx, yy = np.mgrid[:200, :200] 67 | imsave('meshx.png', xx) 68 | imsave('meshy.png', yy) 69 | 70 | # circles contains the squared distance to the (100, 100) point 71 | circle = (xx - 100) ** 2 + (yy - 100) ** 2 72 | imsave('circle.png', circle // 100) 73 | 74 | # apply circle to logo 75 | g = np.array(a, np.int64) 76 | g[:,:,0] *= circle 77 | g[:,:,1] *= circle 78 | g[:,:,2] *= circle 79 | imsave('logocircle.png', g // 20000) 80 | 81 | # donuts contains 1's and 0's organized in a donut shape 82 | # you apply 2 thresholds on circle to define the shape 83 | donut = np.logical_and(circle < (4000 + 500), circle > (4000 - 500)) 84 | g = np.array(a, np.int64) 85 | mask = 1 - donut.astype(np.int64) 86 | imsave('mask.png', mask) 87 | 88 | g[:,:,0] *= mask 89 | g[:,:,1] *= mask 90 | g[:,:,2] *= mask 91 | imsave('masked.png', g) 92 | -------------------------------------------------------------------------------- /triangles/README.rst: -------------------------------------------------------------------------------- 1 | Triangles 2 | ========= 3 | 4 | Arithmetics on a vector with its own transpose results in a matrix. 5 | 6 | Cropping selects a triangular section. 7 | 8 | |image0| 9 | 10 | .. literalinclude:: corner.py 11 | 12 | Hints 13 | ----- 14 | 15 | - the code only works for triangles with two edges parallel to the axes 16 | of the coordinate system 17 | - the instruction ``a.T`` *transposes* the array 18 | - the conditional assignment allows you to set only a part of the 19 | values in an array 20 | 21 | ---- 22 | 23 | Challenge: 24 | ---------- 25 | 26 | Create multiple straight-edge triangles. 27 | 28 | |image1| 29 | 30 | .. |image0| image:: ../images/corner.png 31 | .. |image1| image:: ../images/triangles.png 32 | 33 | -------------------------------------------------------------------------------- /triangles/corner.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from PIL import Image 4 | 5 | x = np.arange(200).reshape(200, 1) 6 | 7 | a = x + x.T 8 | a[a <= 200] = 1 9 | a[a > 200] = 0 10 | a *= 200 11 | 12 | im = Image.fromarray(a.astype(np.uint8)) 13 | im.save('corner.png') 14 | -------------------------------------------------------------------------------- /vortex/README.rst: -------------------------------------------------------------------------------- 1 | Vortex 2 | ====== 3 | 4 | |image0| 5 | 6 | How is the vortex created? 7 | -------------------------- 8 | 9 | 1. Define multiple concentric circles 10 | 2. Rotate every circle by a small amount 11 | 3. Save a frame, repeat 12 | 13 | *watch the two innermost circles to see what is happening* 14 | 15 | Running the Script 16 | ------------------ 17 | 18 | You need to install the ``imageio`` package: 19 | 20 | :: 21 | 22 | pip install imageio 23 | 24 | The code 25 | -------- 26 | 27 | .. literalinclude:: vortex.py 28 | 29 | .. |image0| image:: ../images/vortex.gif 30 | 31 | -------------------------------------------------------------------------------- /vortex/vortex.py: -------------------------------------------------------------------------------- 1 | 2 | from scipy.ndimage import imread, rotate 3 | from scipy.misc import imsave 4 | import numpy as np 5 | 6 | a = imread('python_logo.png', mode='RGB') 7 | 8 | # circles containing the squared distance to the (100, 100) point 9 | xx, yy = np.mgrid[:200, :200] 10 | circle = (xx - 100) ** 2 + (yy - 100) ** 2 11 | 12 | 13 | # create masks 14 | bounds = [0, 800, 2000, 3200, 5000, 6500, 9000, 10500, 13000, 16000, 40000] 15 | masks = [None] 16 | for i in range(1, 10): 17 | donut = np.logical_and(circle < bounds[i], circle >= bounds[i - 1]) 18 | masks.append(donut.astype(np.int64)) 19 | 20 | # calculate 360 frames 21 | for iframe in range(361): 22 | vortex = np.zeros_like(a, np.int64) 23 | for i in range(1, 10): 24 | rot = rotate(a, i * iframe, reshape=False) 25 | g = np.array(rot, np.int64) 26 | g[:, :, 0] *= masks[i] 27 | g[:, :, 1] *= masks[i] 28 | g[:, :, 2] *= masks[i] 29 | vortex += g 30 | 31 | vortex[vortex == 0] = 255 # cover black dots 32 | imsave('vortex/vortex_{}.png'.format(iframe), vortex) 33 | 34 | # 35 | # create an animated GIF 36 | # 37 | import imageio 38 | 39 | PATH = 'vortex/' 40 | images = [] 41 | 42 | for i in range(15): 43 | images.append(imageio.imread(PATH + 'vortex_0.png')) 44 | 45 | for i in range(0, 361, 3): 46 | filename = 'vortex_{}.png'.format(i) 47 | images.append(imageio.imread(PATH + filename)) 48 | 49 | imageio.mimsave('vortex.gif', images, fps=20) 50 | -------------------------------------------------------------------------------- /warhol/README.rst: -------------------------------------------------------------------------------- 1 | Warhol 2 | ====== 3 | 4 | |image0| 5 | 6 | By rotating the color channels you can create effects similar to Andy 7 | Warhols famous picture. 8 | 9 | .. literalinclude:: colorchannels.py 10 | 11 | .. |image0| image:: ../images/colorchannels.png 12 | 13 | -------------------------------------------------------------------------------- /warhol/colorchannels.py: -------------------------------------------------------------------------------- 1 | 2 | import imageio 3 | import numpy as np 4 | 5 | a = imageio.imread("bbtor.jpg")[::4,::4] 6 | z = np.zeros((660, 980, 3), np.uint8) 7 | 8 | z[:320, :480] = a[:,:,[2,0,1]] 9 | z[:320, 500:] = a[:,:,[1,2,0]] 10 | z[340:, :480] = a[:,:,[1,0,2]] 11 | z[340:, 500:] = a[:,:,[2,1,0]] 12 | 13 | imageio.imsave('colorchannels.png', z) 14 | --------------------------------------------------------------------------------