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