├── .github └── workflows │ ├── build.yml │ └── packages.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bin └── neuralart ├── example.py ├── images ├── example-400.png └── example.png ├── neuralart ├── __init__.py ├── __main__.py ├── neuralart.py └── version.txt └── setup.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | # When the 'permissions' key is specified, unspecified permission scopes (e.g., 3 | # actions, checks, etc.) are set to no access (none). 4 | permissions: 5 | contents: read 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | schedule: 12 | # Run weekly (* is a special character in YAML, so quote the string) 13 | - cron: '0 0 * * 0' 14 | workflow_dispatch: 15 | inputs: 16 | # When git-ref is empty, HEAD will be checked out. 17 | git-ref: 18 | description: Optional git ref (branch, tag, or full SHA) 19 | required: false 20 | 21 | jobs: 22 | build: 23 | runs-on: ${{ matrix.os }} 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | os: [macos-latest, windows-latest, ubuntu-latest] 28 | python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 29 | exclude: 30 | # Python 3.6 is not supported on GitHub-hosted Ubuntu runners as of Ubuntu 22.04. 31 | # https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 32 | - os: ubuntu-latest 33 | python-version: '3.6' 34 | # Python 3.7 is not supported on GitHub-hosted Ubuntu runners as of Ubuntu 24.04. 35 | - os: ubuntu-latest 36 | python-version: '3.7' 37 | # Python versions prior to 3.8 are not supported on GitHub-hosted macOS runners 38 | # as of macOS 14. 39 | # https://github.com/actions/runner-images/issues/9770 40 | - os: macos-latest 41 | python-version: '3.6' 42 | - os: macos-latest 43 | python-version: '3.7' 44 | 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | with: 50 | # When the ref is empty, HEAD will be checked out. 51 | ref: ${{ github.event.inputs.git-ref }} 52 | 53 | - name: Set up Python ${{ matrix.python-version }} 54 | uses: actions/setup-python@v5 55 | with: 56 | python-version: ${{ matrix.python-version }} 57 | 58 | - name: Upgrade Pip 59 | run: python -m pip install --upgrade pip 60 | 61 | - name: Lint 62 | run: | 63 | pip install flake8 64 | # stop the build if there are Python syntax errors or undefined names 65 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 66 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 67 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 68 | 69 | - name: Install 70 | run: pip install . 71 | 72 | - name: Test 73 | run: neuralart test-example.png 74 | -------------------------------------------------------------------------------- /.github/workflows/packages.yml: -------------------------------------------------------------------------------- 1 | name: packages 2 | # When the 'permissions' key is specified, unspecified permission scopes (e.g., 3 | # actions, checks, etc.) are set to no access (none). 4 | permissions: 5 | contents: read 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | # When git-ref is empty, HEAD will be checked out. 10 | git-ref: 11 | description: Optional git ref (branch, tag, or full SHA) 12 | required: false 13 | 14 | jobs: 15 | packages: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | 20 | steps: 21 | - name: Clone 22 | uses: actions/checkout@v4 23 | with: 24 | # When the ref is empty, HEAD will be checked out. 25 | ref: ${{ github.event.inputs.git-ref }} 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: '3.x' 31 | 32 | - name: Dependencies 33 | run: python -m pip install --upgrade pip setuptools wheel 34 | 35 | - name: Build 36 | run: | 37 | python setup.py sdist 38 | python setup.py bdist_wheel 39 | 40 | - name: Upload 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: packages 44 | path: ./dist 45 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniel Steinberg 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include example.py 4 | include images/example.png 5 | include images/example-400.png 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/dstein64/neuralart/workflows/build/badge.svg 2 | :target: https://github.com/dstein64/neuralart/actions 3 | 4 | neuralart 5 | ========= 6 | 7 | A library and command line utility for rendering generative art from a randomly 8 | initialized neural network. 9 | 10 | Based on the following blog posts and pages from `studio otoro `__ 11 | 12 | - `Neural Network Generative Art in Javascript `__ 13 | - `Generating Abstract Patterns with TensorFlow `__ 14 | - `Neurogram `__ 15 | - `Interactive Neural Network Art `__ 16 | 17 | Requirements 18 | ------------ 19 | 20 | *neuralart* supports Python 3.x. 21 | 22 | Linux, Mac, and Windows are supported. 23 | 24 | Other operating systems may be compatible if the dependencies can be properly installed. 25 | 26 | Dependencies 27 | ~~~~~~~~~~~~ 28 | 29 | - PyTorch 30 | - Pillow 31 | 32 | Installation 33 | ------------ 34 | 35 | `neuralart `__ is available on PyPI, 36 | the Python Package Index. 37 | 38 | :: 39 | 40 | $ pip install neuralart 41 | 42 | Command Line Utility 43 | -------------------- 44 | 45 | There is a command line utility for generating images. Use the :code:`--help` 46 | flag for more information. 47 | 48 | :: 49 | 50 | $ neuralart --help 51 | 52 | Example 53 | ~~~~~~~ 54 | 55 | :: 56 | 57 | $ neuralart \ 58 | --seed 2 \ 59 | --xres 2048 \ 60 | --hidden-std 1.2 \ 61 | example.png 62 | 63 | .. image:: https://github.com/dstein64/neuralart/blob/master/images/example-400.png?raw=true 64 | :target: https://github.com/dstein64/neuralart/blob/master/images/example.png 65 | 66 | Library Example Usage 67 | --------------------- 68 | 69 | See `example.py `__. 70 | 71 | License 72 | ------- 73 | 74 | The code in this repository has an `MIT License `__. 75 | 76 | See `LICENSE `__. 77 | -------------------------------------------------------------------------------- /bin/neuralart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | import neuralart 6 | 7 | if __name__ == "__main__": 8 | sys.exit(neuralart.neuralart.main()) 9 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Example usage of neuralart.py. 4 | 5 | # Images can be converted to video with ffmpeg. 6 | # > ffmpeg -pattern_type glob -i "*.png" -c:v huffyuv output.avi 7 | 8 | from __future__ import print_function 9 | 10 | import os 11 | import sys 12 | 13 | from PIL import Image 14 | 15 | import neuralart 16 | 17 | RENDER_SEED = 4 18 | ITERATIONS = 200 19 | RESOLUTION = 1024 20 | Z_DIMS = 3 21 | 22 | if len(sys.argv) != 2: 23 | sys.stderr.write("Usage: {} DIRECTORY\n".format(sys.argv[0])) 24 | sys.exit(1) 25 | 26 | directory = sys.argv[1] 27 | if not os.path.exists(directory): 28 | os.makedirs(directory) 29 | 30 | zfill = len(str(ITERATIONS - 1)) 31 | 32 | z = [-1.0] * Z_DIMS 33 | step_size = 2.0 / ITERATIONS 34 | for x in range(ITERATIONS): 35 | result = neuralart.render( 36 | xres=RESOLUTION, 37 | seed=RENDER_SEED, 38 | channels=3, 39 | z=z 40 | ) 41 | file = os.path.join(directory, str(x).zfill(zfill) + ".png") 42 | im = Image.fromarray(result.squeeze()) 43 | im.save(file, 'png') 44 | z = [_z + step_size for _z in z] 45 | -------------------------------------------------------------------------------- /images/example-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstein64/neuralart/c43df41515fcef92e24a2ead6b57a999280b3da6/images/example-400.png -------------------------------------------------------------------------------- /images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstein64/neuralart/c43df41515fcef92e24a2ead6b57a999280b3da6/images/example.png -------------------------------------------------------------------------------- /neuralart/__init__.py: -------------------------------------------------------------------------------- 1 | from .neuralart import __version__, render 2 | -------------------------------------------------------------------------------- /neuralart/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .neuralart import main 4 | 5 | if __name__ == "__main__": 6 | sys.exit(main()) 7 | -------------------------------------------------------------------------------- /neuralart/neuralart.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import argparse 4 | import copy 5 | import inspect 6 | import os 7 | import random 8 | import sys 9 | import warnings 10 | 11 | from PIL import Image 12 | import torch 13 | 14 | version_txt = os.path.join(os.path.dirname(__file__), 'version.txt') 15 | with open(version_txt, 'r') as f: 16 | __version__ = f.read().strip() 17 | 18 | def get_devices(): 19 | devices = ['cpu'] 20 | # As of PyTorch 1.7.0, calling torch.cuda.is_available shows a warning ("...Found no NVIDIA 21 | # driver on your system..."). A related issue is reported in PyTorch Issue #47038. 22 | # Warnings are suppressed below to prevent a warning from showing when no GPU is available. 23 | with warnings.catch_warnings(): 24 | warnings.simplefilter('ignore') 25 | cuda_available = torch.cuda.is_available() 26 | if cuda_available and torch.cuda.device_count() > 0: 27 | devices.append('cuda') 28 | for idx in range(torch.cuda.device_count()): 29 | devices.append('cuda:{}'.format(idx)) 30 | return tuple(devices) 31 | 32 | # ************************************************************ 33 | # * Core 34 | # ************************************************************ 35 | 36 | def render(seed=None, 37 | xlim=[-1.0, 1.0], 38 | ylim=None, 39 | xres=1024, 40 | yres=None, 41 | units=16, 42 | depth=8, 43 | hidden_std=1.0, 44 | output_std=1.0, 45 | channels=3, 46 | radius=True, 47 | bias=True, 48 | z=None, 49 | device='cpu'): 50 | if device not in get_devices(): 51 | raise RuntimeError('Device {} not in available devices: {}'.format( 52 | device, ', '.join(get_devices()))) 53 | 54 | cpu_rng_state = torch.get_rng_state() 55 | cuda_rng_states = [] 56 | if torch.cuda.is_available(): 57 | cuda_rng_states = [torch.cuda.get_rng_state(idx) for idx in range(torch.cuda.device_count())] 58 | 59 | if seed is None: 60 | seed = random.Random().randint(0, 2 ** 32 - 1) 61 | 62 | torch.cuda.manual_seed_all(seed) 63 | torch.manual_seed(seed) 64 | 65 | if ylim is None: 66 | ylim = copy.copy(xlim) 67 | 68 | if yres is None: 69 | yxscale = float(ylim[1] - ylim[0]) / (xlim[1] - xlim[0]) 70 | yres = int(yxscale * xres) 71 | 72 | x = torch.linspace(xlim[0], xlim[1], xres, device=device) 73 | y = torch.linspace(ylim[0], ylim[1], yres, device=device) 74 | meshgrid_kwargs = {} 75 | if inspect.signature(torch.meshgrid).parameters.get('indexing'): 76 | meshgrid_kwargs['indexing'] = 'ij' 77 | grid = torch.meshgrid((y, x), **meshgrid_kwargs) 78 | 79 | inputs = torch.cat((grid[0].flatten().unsqueeze(1), grid[1].flatten().unsqueeze(1)), -1) 80 | 81 | if radius: 82 | inputs = torch.cat((inputs, torch.norm(inputs, 2, 1).unsqueeze(1)), -1) 83 | 84 | if z is not None: 85 | zrep = torch.tensor(z, dtype=inputs.dtype, device=device).repeat((inputs.shape[0], 1)) 86 | inputs = torch.cat((inputs, zrep), -1) 87 | 88 | n_hidden_units = [units] * depth 89 | 90 | activations = inputs 91 | for units in n_hidden_units: 92 | if bias: 93 | bias_array = torch.ones((activations.shape[0], 1), device=device) 94 | activations = torch.cat((bias_array, activations), -1) 95 | hidden_layer_weights = torch.randn((activations.shape[1], units), device=device) * hidden_std 96 | activations = torch.tanh(torch.mm(activations, hidden_layer_weights)) 97 | 98 | if bias: 99 | bias_array = torch.ones((activations.shape[0], 1), device=device) 100 | activations = torch.cat((bias_array, activations), -1) 101 | output_layer_weights = torch.randn((activations.shape[1], channels), device=device) * output_std 102 | output = torch.sigmoid(torch.mm(activations, output_layer_weights)) 103 | output = output.reshape((yres, xres, channels)) 104 | 105 | torch.set_rng_state(cpu_rng_state) 106 | for idx, cuda_rng_state in enumerate(cuda_rng_states): 107 | torch.cuda.set_rng_state(cuda_rng_state, idx) 108 | 109 | return (output.cpu() * 255).round().type(torch.uint8).numpy() 110 | 111 | # ************************************************************ 112 | # * Command Line Interface 113 | # ************************************************************ 114 | 115 | def _parse_args(argv): 116 | parser = argparse.ArgumentParser( 117 | prog='neuralart', 118 | formatter_class=argparse.ArgumentDefaultsHelpFormatter 119 | ) 120 | parser.add_argument('--version', 121 | action='version', 122 | version='neuralart {}'.format(__version__)) 123 | parser.add_argument('--seed', type=int, help='RNG seed.') 124 | parser.add_argument('--xlim', 125 | type=float, 126 | nargs=2, 127 | help='X limits.', 128 | metavar=('MIN', 'MAX'), 129 | default=[-1.0, 1.0]) 130 | parser.add_argument('--ylim', 131 | type=float, 132 | nargs=2, 133 | metavar=('MIN', 'MAX'), 134 | help='Y limits. Defaults to xlim when not specified.') 135 | parser.add_argument('--xres', type=int, help='X resolution.', default=1024) 136 | parser.add_argument('--yres', 137 | type=int, 138 | help='Y resolution. When not specified, the value is calculated' 139 | ' automatically based on xlim, ylim, and xres.') 140 | parser.add_argument('--units', type=int, help='Units per hidden layer.', default=16) 141 | parser.add_argument('--depth', type=int, help='Number of hidden layers.',default=8) 142 | parser.add_argument('--hidden-std', 143 | type=float, 144 | help='Standard deviation used to randomly initialize hidden layer weights.', 145 | default=1.0) 146 | parser.add_argument('--output-std', 147 | type=float, 148 | help='Standard deviation used to randomly initialize output layer weights.', 149 | default=1.0) 150 | parser.add_argument('--color-space', 151 | choices=('rgb', 'bw'), 152 | help='Select the color space (RGB or black-and-white).', 153 | default='rgb') 154 | parser.add_argument('--no-radius', 155 | action='store_false', 156 | help='Disables radius input term.', 157 | dest='radius') 158 | parser.add_argument('--no-bias', 159 | action='store_false', 160 | help='Disables bias terms.', 161 | dest='bias') 162 | parser.add_argument('--device', default='cpu', choices=get_devices()) 163 | parser.add_argument('--z', type=float, nargs='*') 164 | parser.add_argument('--no-verbose', action='store_false', dest='verbose') 165 | parser.add_argument('file', help='File path to save the PNG image.') 166 | args = parser.parse_args(argv[1:]) 167 | return args 168 | 169 | 170 | def main(argv=sys.argv): 171 | args = _parse_args(argv) 172 | if not args.file.lower().endswith('.png'): 173 | sys.stderr.write('Image file is missing PNG extension.\n') 174 | channels_lookup = { 175 | 'rgb': 3, 176 | 'bw': 1 177 | } 178 | seed = args.seed 179 | if seed is None: 180 | seed = random.randint(0, 2 ** 32 - 1) 181 | if args.verbose: 182 | print('Seed: {}'.format(seed)) 183 | result = render( 184 | seed=seed, 185 | xlim=args.xlim, 186 | ylim=args.ylim, 187 | xres=args.xres, 188 | yres=args.yres, 189 | units=args.units, 190 | depth=args.depth, 191 | hidden_std=args.hidden_std, 192 | output_std=args.output_std, 193 | channels=channels_lookup[args.color_space], 194 | radius=args.radius, 195 | bias=args.bias, 196 | z=args.z, 197 | device=args.device 198 | ) 199 | im = Image.fromarray(result.squeeze()) 200 | im.save(args.file, 'png') 201 | return 0 202 | 203 | 204 | if __name__ == '__main__': 205 | sys.exit(main(sys.argv)) 206 | -------------------------------------------------------------------------------- /neuralart/version.txt: -------------------------------------------------------------------------------- 1 | 1.1.1 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | from setuptools import setup 4 | 5 | version_txt = os.path.join(os.path.dirname(__file__), 'neuralart', 'version.txt') 6 | with open(version_txt, 'r') as f: 7 | version = f.read().strip() 8 | 9 | with io.open('README.rst', encoding='utf8') as f: 10 | long_description = f.read() 11 | 12 | setup( 13 | name='neuralart', 14 | packages=['neuralart'], 15 | package_data={'neuralart': ['version.txt']}, 16 | scripts=['bin/neuralart'], 17 | license='MIT', 18 | version=version, 19 | description='A library for rendering generative art from a randomly initialized neural network.', 20 | long_description=long_description, 21 | author='Daniel Steinberg', 22 | author_email='ds@dannyadam.com', 23 | url='https://github.com/dstein64/neuralart', 24 | keywords=['neural-networks', 'generative-art'], 25 | classifiers=[ 26 | 'Development Status :: 4 - Beta', 27 | 'Intended Audience :: Developers', 28 | 'Topic :: Artistic Software', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Operating System :: Unix', 31 | 'Operating System :: POSIX :: Linux', 32 | 'Operating System :: MacOS', 33 | 'Operating System :: Microsoft :: Windows', 34 | 'Programming Language :: Python :: 3' 35 | ], 36 | install_requires=['numpy', 'pillow', 'torch'] 37 | ) 38 | --------------------------------------------------------------------------------