zebranoise 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zebranoise-0.1.0/LICENSE.txt +23 -0
- zebranoise-0.1.0/PKG-INFO +121 -0
- zebranoise-0.1.0/README.md +100 -0
- zebranoise-0.1.0/pyproject.toml +2 -0
- zebranoise-0.1.0/setup.cfg +4 -0
- zebranoise-0.1.0/setup.py +46 -0
- zebranoise-0.1.0/zebranoise/__init__.py +5 -0
- zebranoise-0.1.0/zebranoise/_perlin.c +239 -0
- zebranoise-0.1.0/zebranoise/_version.py +1 -0
- zebranoise-0.1.0/zebranoise/easy.py +51 -0
- zebranoise-0.1.0/zebranoise/perlin_stimulus.py +267 -0
- zebranoise-0.1.0/zebranoise/util.py +152 -0
- zebranoise-0.1.0/zebranoise.egg-info/PKG-INFO +121 -0
- zebranoise-0.1.0/zebranoise.egg-info/SOURCES.txt +15 -0
- zebranoise-0.1.0/zebranoise.egg-info/dependency_links.txt +1 -0
- zebranoise-0.1.0/zebranoise.egg-info/requires.txt +4 -0
- zebranoise-0.1.0/zebranoise.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Maxwell Shinn
|
|
4
|
+
Copyright (c) 2024 Sophie Skriabine
|
|
5
|
+
Copyright (c) 2008 Casey Duncan
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: zebranoise
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zebra noise stimuli for Python
|
|
5
|
+
Home-page: https://github.com/mwshinn/zebra_noise
|
|
6
|
+
Author: Max Shinn
|
|
7
|
+
Author-email: m.shinn@ucl.ac.uk
|
|
8
|
+
License: UNKNOWN
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: C
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE.txt
|
|
19
|
+
|
|
20
|
+
This library will efficiently generate visual stimulus videos using Perlin noise, including zebra noise. The core algorithm is in optimized C.
|
|
21
|
+
|
|
22
|
+
[Example zebra noise](http://www.youtube.com/watch?v=-SyjgbNCP4Q)
|
|
23
|
+
|
|
24
|
+
[](http://www.youtube.com/watch?v=-SyjgbNCP4Q "Example zebra noise")
|
|
25
|
+
|
|
26
|
+
# Installation
|
|
27
|
+
|
|
28
|
+
## From pypi
|
|
29
|
+
|
|
30
|
+
Run:
|
|
31
|
+
|
|
32
|
+
pip install zebranoise
|
|
33
|
+
|
|
34
|
+
## From source
|
|
35
|
+
|
|
36
|
+
Make sure you have a C compiler installed. The easiest way to do this is by installing Cython.
|
|
37
|
+
|
|
38
|
+
Then, install using the standard
|
|
39
|
+
|
|
40
|
+
python setup.py install
|
|
41
|
+
|
|
42
|
+
This will compile the sources and install.
|
|
43
|
+
|
|
44
|
+
I have only tested this on Linux.
|
|
45
|
+
|
|
46
|
+
# Usage
|
|
47
|
+
|
|
48
|
+
## Basic usage
|
|
49
|
+
|
|
50
|
+
If you would like to generate a zebra noise of size 640x480 for 2 minutes, you can run
|
|
51
|
+
|
|
52
|
+
import zebranoise
|
|
53
|
+
zebranoise.zebra_noise("output.mp4", xsize=640, ysize=480, tdur=60*2, fps=30, seed=0)
|
|
54
|
+
|
|
55
|
+
Other parameters control the spatial and temporal frequencies. For instance,
|
|
56
|
+
|
|
57
|
+
zebranoise.zebra_noise("output2.mp4", xsize=640, ysize=480, tdur=60*2, levels=10, xyscale=.2, tscale=50, fps=30, xscale=1.0, yscale=1.0, seed=0)
|
|
58
|
+
|
|
59
|
+
The meaning of these parameters is:
|
|
60
|
+
|
|
61
|
+
- **xsize** and **ysize**: the x and y dimensions of the output video. (Sometimes
|
|
62
|
+
these will be rounded up to multiples of 16.)
|
|
63
|
+
- **tdur**: the duration in seconds
|
|
64
|
+
- **levels**: The number of octaves to use when approximating the 1/f spectrum.
|
|
65
|
+
The default of 10 should be more than enough.
|
|
66
|
+
- **xyscale**: The spatial scale of the Perlin noise, from 0 to 1. Low values
|
|
67
|
+
will make the video smoother and high values choppier.
|
|
68
|
+
- **tscale**: The speed of the video
|
|
69
|
+
- **xscale** and **yscale**: Resize the x and y dimensions of the output.
|
|
70
|
+
- **fps**: Frames per second
|
|
71
|
+
- **seed**: Random seed
|
|
72
|
+
|
|
73
|
+
## Advanced usage
|
|
74
|
+
|
|
75
|
+
This package can be used to generate new stimuli based on Perlin noise.
|
|
76
|
+
|
|
77
|
+
To generate a Perlin noise-based video, follow two steps with the package. First, generate the Perlin noise. This is the most time consuming step, and will also use a lot of temporary disk space. This is accomplished with:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
noise = zebranoise.PerlinStimulus(xsize=400, ysize=100, tdur=60*2, xyscale=.2, tscale=50, seed=0)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
See the function documentation for perlstim.Perl for more information about modifying the properties of the noise.
|
|
84
|
+
|
|
85
|
+
Second, apply "filters" to the Perlin noise and save the resulting video. Filters include thresholds, sync squares in the corner, etc. If using a filter with no arguments, just pass a string naming the filter. If using a filter with arguments, pass a tuple where the first element is the name of the filter as a string, and the remaining elements are the arguments. For example,
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
noise.save_video("noise.mp4", filters=["reverse", ("comb", .05)])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The above example applies the "reverse" filter (with no arguments) and the "comb" filter with the argument 0.1.
|
|
92
|
+
|
|
93
|
+
## Filters
|
|
94
|
+
|
|
95
|
+
The following filters are currently defined:
|
|
96
|
+
|
|
97
|
+
- "threshold" (1 argument): set all values above the threshold argument to white, and all below to black.
|
|
98
|
+
- "softthresh" (1 argument): set values much greater than 0 to white and much less than zero to blac. Use shades of grey for intermediate values, according to a sigmoid with a temperature given as the argument.
|
|
99
|
+
- "comb" (1 argument): Alternate black and white thresholds at intervals given by the argument.
|
|
100
|
+
- "reverse" (0 arguments): play the video in reverse
|
|
101
|
+
- "invert" (0 arguments): switch white and black
|
|
102
|
+
- "wood" (1 argument): similar to "comb", but use a sawtooth wave
|
|
103
|
+
- "blur" (1 argument): Gaussian blur with the given spatial standard deviation
|
|
104
|
+
- "photodiode" (1 argument): Draw a sync square in the corner, with a size given by the argument.
|
|
105
|
+
- "photodiode_anywhere" (3 arguments): Draw a sync square at the coordinates given by the first two arguments (x and y), and of size given by argument 3.
|
|
106
|
+
- [any function] (0 arguments): If you pass a function, the function will be applied to each chunk of the video. Chunks consist of the entire x and y but a subset of z, guaranteed to be an even number.
|
|
107
|
+
|
|
108
|
+
## Example
|
|
109
|
+
|
|
110
|
+
To get started, try the following:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import zebranoise
|
|
114
|
+
stim = zebranoise.PerlinStimulus(xsize=480, ysize=128, tdur=60*5, xyscale=.2, tscale=50)
|
|
115
|
+
stim.save_video("perlin_stimulus.mp4", loop=1, filters=[("comb", .08), ("photodiode", 30)])
|
|
116
|
+
```
|
|
117
|
+
# Other information
|
|
118
|
+
|
|
119
|
+
Much of the C code is based on [Casey Duncan's "noise" package for Python](https://github.com/caseman/noise), under the MIT license.
|
|
120
|
+
|
|
121
|
+
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
This library will efficiently generate visual stimulus videos using Perlin noise, including zebra noise. The core algorithm is in optimized C.
|
|
2
|
+
|
|
3
|
+
[Example zebra noise](http://www.youtube.com/watch?v=-SyjgbNCP4Q)
|
|
4
|
+
|
|
5
|
+
[](http://www.youtube.com/watch?v=-SyjgbNCP4Q "Example zebra noise")
|
|
6
|
+
|
|
7
|
+
# Installation
|
|
8
|
+
|
|
9
|
+
## From pypi
|
|
10
|
+
|
|
11
|
+
Run:
|
|
12
|
+
|
|
13
|
+
pip install zebranoise
|
|
14
|
+
|
|
15
|
+
## From source
|
|
16
|
+
|
|
17
|
+
Make sure you have a C compiler installed. The easiest way to do this is by installing Cython.
|
|
18
|
+
|
|
19
|
+
Then, install using the standard
|
|
20
|
+
|
|
21
|
+
python setup.py install
|
|
22
|
+
|
|
23
|
+
This will compile the sources and install.
|
|
24
|
+
|
|
25
|
+
I have only tested this on Linux.
|
|
26
|
+
|
|
27
|
+
# Usage
|
|
28
|
+
|
|
29
|
+
## Basic usage
|
|
30
|
+
|
|
31
|
+
If you would like to generate a zebra noise of size 640x480 for 2 minutes, you can run
|
|
32
|
+
|
|
33
|
+
import zebranoise
|
|
34
|
+
zebranoise.zebra_noise("output.mp4", xsize=640, ysize=480, tdur=60*2, fps=30, seed=0)
|
|
35
|
+
|
|
36
|
+
Other parameters control the spatial and temporal frequencies. For instance,
|
|
37
|
+
|
|
38
|
+
zebranoise.zebra_noise("output2.mp4", xsize=640, ysize=480, tdur=60*2, levels=10, xyscale=.2, tscale=50, fps=30, xscale=1.0, yscale=1.0, seed=0)
|
|
39
|
+
|
|
40
|
+
The meaning of these parameters is:
|
|
41
|
+
|
|
42
|
+
- **xsize** and **ysize**: the x and y dimensions of the output video. (Sometimes
|
|
43
|
+
these will be rounded up to multiples of 16.)
|
|
44
|
+
- **tdur**: the duration in seconds
|
|
45
|
+
- **levels**: The number of octaves to use when approximating the 1/f spectrum.
|
|
46
|
+
The default of 10 should be more than enough.
|
|
47
|
+
- **xyscale**: The spatial scale of the Perlin noise, from 0 to 1. Low values
|
|
48
|
+
will make the video smoother and high values choppier.
|
|
49
|
+
- **tscale**: The speed of the video
|
|
50
|
+
- **xscale** and **yscale**: Resize the x and y dimensions of the output.
|
|
51
|
+
- **fps**: Frames per second
|
|
52
|
+
- **seed**: Random seed
|
|
53
|
+
|
|
54
|
+
## Advanced usage
|
|
55
|
+
|
|
56
|
+
This package can be used to generate new stimuli based on Perlin noise.
|
|
57
|
+
|
|
58
|
+
To generate a Perlin noise-based video, follow two steps with the package. First, generate the Perlin noise. This is the most time consuming step, and will also use a lot of temporary disk space. This is accomplished with:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
noise = zebranoise.PerlinStimulus(xsize=400, ysize=100, tdur=60*2, xyscale=.2, tscale=50, seed=0)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
See the function documentation for perlstim.Perl for more information about modifying the properties of the noise.
|
|
65
|
+
|
|
66
|
+
Second, apply "filters" to the Perlin noise and save the resulting video. Filters include thresholds, sync squares in the corner, etc. If using a filter with no arguments, just pass a string naming the filter. If using a filter with arguments, pass a tuple where the first element is the name of the filter as a string, and the remaining elements are the arguments. For example,
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
noise.save_video("noise.mp4", filters=["reverse", ("comb", .05)])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The above example applies the "reverse" filter (with no arguments) and the "comb" filter with the argument 0.1.
|
|
73
|
+
|
|
74
|
+
## Filters
|
|
75
|
+
|
|
76
|
+
The following filters are currently defined:
|
|
77
|
+
|
|
78
|
+
- "threshold" (1 argument): set all values above the threshold argument to white, and all below to black.
|
|
79
|
+
- "softthresh" (1 argument): set values much greater than 0 to white and much less than zero to blac. Use shades of grey for intermediate values, according to a sigmoid with a temperature given as the argument.
|
|
80
|
+
- "comb" (1 argument): Alternate black and white thresholds at intervals given by the argument.
|
|
81
|
+
- "reverse" (0 arguments): play the video in reverse
|
|
82
|
+
- "invert" (0 arguments): switch white and black
|
|
83
|
+
- "wood" (1 argument): similar to "comb", but use a sawtooth wave
|
|
84
|
+
- "blur" (1 argument): Gaussian blur with the given spatial standard deviation
|
|
85
|
+
- "photodiode" (1 argument): Draw a sync square in the corner, with a size given by the argument.
|
|
86
|
+
- "photodiode_anywhere" (3 arguments): Draw a sync square at the coordinates given by the first two arguments (x and y), and of size given by argument 3.
|
|
87
|
+
- [any function] (0 arguments): If you pass a function, the function will be applied to each chunk of the video. Chunks consist of the entire x and y but a subset of z, guaranteed to be an even number.
|
|
88
|
+
|
|
89
|
+
## Example
|
|
90
|
+
|
|
91
|
+
To get started, try the following:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import zebranoise
|
|
95
|
+
stim = zebranoise.PerlinStimulus(xsize=480, ysize=128, tdur=60*5, xyscale=.2, tscale=50)
|
|
96
|
+
stim.save_video("perlin_stimulus.mp4", loop=1, filters=[("comb", .08), ("photodiode", 30)])
|
|
97
|
+
```
|
|
98
|
+
# Other information
|
|
99
|
+
|
|
100
|
+
Much of the C code is based on [Casey Duncan's "noise" package for Python](https://github.com/caseman/noise), under the MIT license.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
try:
|
|
3
|
+
from setuptools import setup, Extension
|
|
4
|
+
except ImportError:
|
|
5
|
+
from distutils.core import setup, Extension
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
if sys.platform != 'win32':
|
|
10
|
+
compile_args = ['-funroll-loops']
|
|
11
|
+
else:
|
|
12
|
+
compile_args = []
|
|
13
|
+
|
|
14
|
+
with open("README.md", "r") as f:
|
|
15
|
+
long_desc = f.read()
|
|
16
|
+
|
|
17
|
+
with open("zebranoise/_version.py", "r") as f:
|
|
18
|
+
exec(f.read())
|
|
19
|
+
|
|
20
|
+
setup(
|
|
21
|
+
name='zebranoise',
|
|
22
|
+
version=__version__,
|
|
23
|
+
description='Zebra noise stimuli for Python',
|
|
24
|
+
long_description = long_desc,
|
|
25
|
+
long_description_content_type='text/markdown',
|
|
26
|
+
author='Max Shinn',
|
|
27
|
+
author_email='m.shinn@ucl.ac.uk',
|
|
28
|
+
url='https://github.com/mwshinn/zebra_noise',
|
|
29
|
+
classifiers = [
|
|
30
|
+
'Development Status :: 4 - Beta',
|
|
31
|
+
'Programming Language :: C',
|
|
32
|
+
'Programming Language :: Python :: 3',
|
|
33
|
+
'Intended Audience :: Science/Research',
|
|
34
|
+
'License :: OSI Approved :: MIT License',
|
|
35
|
+
'Topic :: Multimedia :: Graphics',
|
|
36
|
+
'Topic :: Scientific/Engineering',
|
|
37
|
+
],
|
|
38
|
+
install_requires = ['numpy', 'imageio', 'imageio-ffmpeg', 'tqdm'],
|
|
39
|
+
packages=['zebranoise'],
|
|
40
|
+
ext_modules=[
|
|
41
|
+
Extension('zebranoise._perlin', ['zebranoise/_perlin.c'],
|
|
42
|
+
extra_compile_args=compile_args,
|
|
43
|
+
include_dirs=[np.get_include()],
|
|
44
|
+
)
|
|
45
|
+
],
|
|
46
|
+
)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// Copyright (c) 2008, Casey Duncan (casey dot duncan at gmail dot com)
|
|
2
|
+
// Copyright (c) 2022, Max Shinn
|
|
3
|
+
// see LICENSE.txt for details
|
|
4
|
+
|
|
5
|
+
#include "Python.h"
|
|
6
|
+
#include <math.h>
|
|
7
|
+
#include <stdio.h>
|
|
8
|
+
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
|
|
9
|
+
#include <numpy/arrayobject.h>
|
|
10
|
+
|
|
11
|
+
#ifdef _MSC_VER
|
|
12
|
+
#define inline __inline
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
#define lerp(t, a, b) ((a) + (t) * ((b) - (a)))
|
|
16
|
+
|
|
17
|
+
const float GRAD3[][3] = {
|
|
18
|
+
{1,1,0},{-1,1,0},{1,-1,0},{-1,-1,0},
|
|
19
|
+
{1,0,1},{-1,0,1},{1,0,-1},{-1,0,-1},
|
|
20
|
+
{0,1,1},{0,-1,1},{0,1,-1},{0,-1,-1},
|
|
21
|
+
{1,0,-1},{-1,0,-1},{0,-1,1},{0,1,1}};
|
|
22
|
+
|
|
23
|
+
// At the possible cost of unaligned access, we use char instead of
|
|
24
|
+
// int here to try to ensure that this table fits in L1 cache
|
|
25
|
+
const unsigned char PERM[] = {
|
|
26
|
+
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140,
|
|
27
|
+
36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120,
|
|
28
|
+
234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
|
|
29
|
+
88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71,
|
|
30
|
+
134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133,
|
|
31
|
+
230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161,
|
|
32
|
+
1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130,
|
|
33
|
+
116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250,
|
|
34
|
+
124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227,
|
|
35
|
+
47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44,
|
|
36
|
+
154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98,
|
|
37
|
+
108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34,
|
|
38
|
+
242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14,
|
|
39
|
+
239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121,
|
|
40
|
+
50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243,
|
|
41
|
+
141, 128, 195, 78, 66, 215, 61, 156, 180, 151, 160, 137, 91, 90, 15, 131,
|
|
42
|
+
13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37,
|
|
43
|
+
240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252,
|
|
44
|
+
219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125,
|
|
45
|
+
136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158,
|
|
46
|
+
231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245,
|
|
47
|
+
40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187,
|
|
48
|
+
208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198,
|
|
49
|
+
173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126,
|
|
50
|
+
255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223,
|
|
51
|
+
183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167,
|
|
52
|
+
43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185,
|
|
53
|
+
112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179,
|
|
54
|
+
162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199,
|
|
55
|
+
106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236,
|
|
56
|
+
205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156,
|
|
57
|
+
180};
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
static inline float
|
|
61
|
+
grad3(const int hash, const float x, const float y, const float z)
|
|
62
|
+
{
|
|
63
|
+
const int h = hash & 15;
|
|
64
|
+
return x * GRAD3[h][0] + y * GRAD3[h][1] + z * GRAD3[h][2];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
float
|
|
68
|
+
noise3(float x, float y, float z, const int repeatx, const int repeaty, const int repeatz,
|
|
69
|
+
const int base)
|
|
70
|
+
{
|
|
71
|
+
float fx, fy, fz;
|
|
72
|
+
int A, AA, AB, B, BA, BB;
|
|
73
|
+
int i = (int)x;
|
|
74
|
+
int j = (int)y;
|
|
75
|
+
int k = (int)z;
|
|
76
|
+
int ii = (i + 1) % repeatx;
|
|
77
|
+
int jj = (j + 1) % repeaty;
|
|
78
|
+
int kk = (k + 1) % repeatz;
|
|
79
|
+
i = ((i + base) & 255);
|
|
80
|
+
j = ((j + base) & 255);
|
|
81
|
+
k = ((k + base) & 255);
|
|
82
|
+
ii = ((ii + base) & 255);
|
|
83
|
+
jj = ((jj + base) & 255);
|
|
84
|
+
kk = ((kk + base) & 255);
|
|
85
|
+
|
|
86
|
+
x -= (float)(int)x; y -= (float)(int)y; z -= (float)(int)z;
|
|
87
|
+
fx = x*x*x * (x * (x * 6 - 15) + 10);
|
|
88
|
+
fy = y*y*y * (y * (y * 6 - 15) + 10);
|
|
89
|
+
fz = z*z*z * (z * (z * 6 - 15) + 10);
|
|
90
|
+
|
|
91
|
+
A = PERM[i];
|
|
92
|
+
AA = PERM[A + j];
|
|
93
|
+
AB = PERM[A + jj];
|
|
94
|
+
B = PERM[ii];
|
|
95
|
+
BA = PERM[B + j];
|
|
96
|
+
BB = PERM[B + jj];
|
|
97
|
+
|
|
98
|
+
return lerp(fz, lerp(fy, lerp(fx, grad3(PERM[AA + k], x, y, z),
|
|
99
|
+
grad3(PERM[BA + k], x - 1, y, z)),
|
|
100
|
+
lerp(fx, grad3(PERM[AB + k], x, y - 1, z),
|
|
101
|
+
grad3(PERM[BB + k], x - 1, y - 1, z))),
|
|
102
|
+
lerp(fy, lerp(fx, grad3(PERM[AA + kk], x, y, z - 1),
|
|
103
|
+
grad3(PERM[BA + kk], x - 1, y, z - 1)),
|
|
104
|
+
lerp(fx, grad3(PERM[AB + kk], x, y - 1, z - 1),
|
|
105
|
+
grad3(PERM[BB + kk], x - 1, y - 1, z - 1))));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static PyObject *
|
|
109
|
+
make_perlin(PyObject *self, PyObject *args, PyObject *kwargs)
|
|
110
|
+
{
|
|
111
|
+
int octaves = 1;
|
|
112
|
+
float persistence = 0.5f;
|
|
113
|
+
float lacunarity = 2.0f;
|
|
114
|
+
int repeatx = 1024; // arbitrary
|
|
115
|
+
int repeaty = 1024; // arbitrary
|
|
116
|
+
int repeatz = 1024; // arbitrary
|
|
117
|
+
int base = 0;
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
float *x, *y, *z;
|
|
121
|
+
PyArrayObject *_x, *_y, *_z;
|
|
122
|
+
PyObject *__x, *__y, *__z;
|
|
123
|
+
|
|
124
|
+
static char *kwlist[] = {"x", "y", "z", "octaves", "persistence", "lacunarity",
|
|
125
|
+
"repeatx", "repeaty", "repeatz", "base", NULL};
|
|
126
|
+
|
|
127
|
+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OOO|iffiiii:noise3", kwlist,
|
|
128
|
+
&__x, &__y, &__z, &octaves, &persistence, &lacunarity, &repeatx, &repeaty, &repeatz, &base))
|
|
129
|
+
return NULL;
|
|
130
|
+
if (base < 0 || base > 255) {
|
|
131
|
+
PyErr_SetString(PyExc_ValueError, "Base must be between 0 and 255");
|
|
132
|
+
return NULL;
|
|
133
|
+
}
|
|
134
|
+
_x = (PyArrayObject*)PyArray_FROMANY(__x, NPY_FLOAT, 1, 1, NPY_ARRAY_C_CONTIGUOUS);
|
|
135
|
+
_y = (PyArrayObject*)PyArray_FROMANY(__y, NPY_FLOAT, 1, 1, NPY_ARRAY_C_CONTIGUOUS);
|
|
136
|
+
_z = (PyArrayObject*)PyArray_FROMANY(__z, NPY_FLOAT, 1, 1, NPY_ARRAY_C_CONTIGUOUS);
|
|
137
|
+
if (!_x || !_y || !_z)
|
|
138
|
+
return NULL;
|
|
139
|
+
x = (float*)PyArray_DATA(_x);
|
|
140
|
+
y = (float*)PyArray_DATA(_y);
|
|
141
|
+
z = (float*)PyArray_DATA(_z);
|
|
142
|
+
int len_x = PyArray_SIZE(_x);
|
|
143
|
+
int len_y = PyArray_SIZE(_y);
|
|
144
|
+
int len_z = PyArray_SIZE(_z);
|
|
145
|
+
int i,j,k,l;
|
|
146
|
+
float *ret = (float*)malloc(sizeof(float)*len_x*len_y*len_z);
|
|
147
|
+
if (x[len_x-1] >= repeatx || y[len_y-1] >= repeaty || z[len_z-1] >= repeatz) {
|
|
148
|
+
PyErr_SetString(PyExc_ValueError, "Cannot pass values greater than repeatx/y/z");
|
|
149
|
+
return NULL;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (octaves == 1) {
|
|
153
|
+
// Single octave, return simple noise
|
|
154
|
+
for (i=0; i<len_x; i++) {
|
|
155
|
+
for (j=0; j<len_y; j++) {
|
|
156
|
+
for (k=0; k<len_z; k++) {
|
|
157
|
+
ret[i*len_y*len_z+j*len_z+k] = noise3(x[i], y[j], z[k],
|
|
158
|
+
repeatx, repeaty, repeatz, base);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} else if (octaves > 1) {
|
|
163
|
+
for (i=0; i<len_x; i++) {
|
|
164
|
+
for (j=0; j<len_y; j++) {
|
|
165
|
+
for (k=0; k<len_z; k++) {
|
|
166
|
+
float freq = 1.0f;
|
|
167
|
+
float amp = 1.0f;
|
|
168
|
+
float max = 0.0f;
|
|
169
|
+
float total = 0.0f;
|
|
170
|
+
for (l = 0; l < octaves; l++) {
|
|
171
|
+
total += noise3(x[i] * freq, y[j] * freq, z[k] * freq,
|
|
172
|
+
(const int)(repeatx*freq), (const int)(repeaty*freq), (const int)(repeatz*freq), base) * amp;
|
|
173
|
+
max += amp;
|
|
174
|
+
freq *= lacunarity;
|
|
175
|
+
amp *= persistence;
|
|
176
|
+
if (amp < .004) break; // No chance of influence beyond ~1/256
|
|
177
|
+
}
|
|
178
|
+
ret[i*len_y*len_z+j*len_z+k] = (float) (total / max);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
PyErr_SetString(PyExc_ValueError, "Expected octaves value > 0");
|
|
184
|
+
return NULL;
|
|
185
|
+
}
|
|
186
|
+
npy_intp dims[3] = { len_x, len_y, len_z };
|
|
187
|
+
PyObject *retarray = PyArray_SimpleNewFromData(3, dims, NPY_FLOAT, ret);
|
|
188
|
+
PyArray_ENABLEFLAGS((PyArrayObject*)retarray, NPY_ARRAY_OWNDATA);
|
|
189
|
+
Py_DECREF(_x);
|
|
190
|
+
Py_DECREF(_y);
|
|
191
|
+
Py_DECREF(_z);
|
|
192
|
+
return retarray;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static PyMethodDef perlin_functions[] = {
|
|
196
|
+
{"make_perlin", (PyCFunction) make_perlin, METH_VARARGS | METH_KEYWORDS,
|
|
197
|
+
"Generate a perlin noise stimulus\n\
|
|
198
|
+
\n\
|
|
199
|
+
Parameters\n\
|
|
200
|
+
----------\n\
|
|
201
|
+
x,y,z : 1D ndarray of float32, length > 0\n\
|
|
202
|
+
Grid of positions on which to compute the noise\n\
|
|
203
|
+
octaves : int > 0, default: 1\n\
|
|
204
|
+
Number of spatial scales to include\n\
|
|
205
|
+
persistence : float > 0, default: 0.5\n\
|
|
206
|
+
Relative strength of neighbouring octaves\n\
|
|
207
|
+
repeatx,repeaty,repeatz : int > 0, default: 1024\n\
|
|
208
|
+
Maximum x, y, or z value before the stimulus repeats\n\
|
|
209
|
+
base : int ∈ [0,255], default: 0\n\
|
|
210
|
+
Start position of the permutation, essentially the random seed\n\
|
|
211
|
+
\n\
|
|
212
|
+
Returns\n\
|
|
213
|
+
-------\n\
|
|
214
|
+
3D int ndarray, values ∈ [0,255]\n\
|
|
215
|
+
Pink noise movie\n\
|
|
216
|
+
"},
|
|
217
|
+
{NULL}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
PyDoc_STRVAR(module_doc, "Efficiently generate Perlin noise, based on the 'noise' package by Casey Duncan");
|
|
221
|
+
|
|
222
|
+
static struct PyModuleDef moduledef = {
|
|
223
|
+
PyModuleDef_HEAD_INIT,
|
|
224
|
+
"_perlin",
|
|
225
|
+
module_doc,
|
|
226
|
+
-1, /* m_size */
|
|
227
|
+
perlin_functions, /* m_methods */
|
|
228
|
+
NULL, /* m_reload (unused) */
|
|
229
|
+
NULL, /* m_traverse */
|
|
230
|
+
NULL, /* m_clear */
|
|
231
|
+
NULL /* m_free */
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
PyObject *
|
|
235
|
+
PyInit__perlin(void)
|
|
236
|
+
{
|
|
237
|
+
import_array();
|
|
238
|
+
return PyModule_Create(&moduledef);
|
|
239
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.1.0'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import imageio
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
import warnings
|
|
4
|
+
from .util import generate_frames, filter_frames_index_function, apply_filters, discretize
|
|
5
|
+
|
|
6
|
+
def zebra_noise(output_file, xsize, ysize, tdur, levels=10, xyscale=.2, tscale=50, fps=30, xscale=1.0, yscale=1.0, seed=0, filters=[("comb", 0.08)]):
|
|
7
|
+
"""Generate a .mp4 of zebra noise.
|
|
8
|
+
|
|
9
|
+
This method is a simplified interface for the PerlinStimulus class, designed to only generate zebra noise
|
|
10
|
+
as defined in the paper.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
output_file : string
|
|
15
|
+
Filename to save the generated .mp4 file
|
|
16
|
+
xsize, ysize : int
|
|
17
|
+
The x and y dimensions of the output video. (Sometimes these will be rounded up to multiples of 16.)
|
|
18
|
+
tdur : float
|
|
19
|
+
The duration of the video in seconds
|
|
20
|
+
levels : int
|
|
21
|
+
The number of octaves to use when approximating the 1/f spectrum. The default of 10 should be more than enough.
|
|
22
|
+
xyscale : float from (0,1)
|
|
23
|
+
The spatial scale of the Perlin noise. Values near 0 will make the video smoother and near 1 choppier.
|
|
24
|
+
tscale : int
|
|
25
|
+
A scaling factor to set the speed of the video
|
|
26
|
+
xscale, yscale : float
|
|
27
|
+
Resize the x and y dimensions of the output
|
|
28
|
+
fps : int
|
|
29
|
+
Frames per second
|
|
30
|
+
seed : int
|
|
31
|
+
Random seed
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
None, but saves the video file to the desired filename
|
|
36
|
+
"""
|
|
37
|
+
tsize = int(tdur*fps)
|
|
38
|
+
tscale = tscale * (fps/30)
|
|
39
|
+
textra = (tscale - (tsize % tscale)) % tscale
|
|
40
|
+
if textra > 0:
|
|
41
|
+
warnings.warn(f"Adding {textra} extra timepoints to make tscale a multiple of tdur")
|
|
42
|
+
tsize += round(textra)
|
|
43
|
+
get_index = filter_frames_index_function(filters, tsize)
|
|
44
|
+
writer = imageio.get_writer(output_file, fps=fps)
|
|
45
|
+
for _i in tqdm(range(0, tsize)):
|
|
46
|
+
i = get_index(_i)
|
|
47
|
+
frame = generate_frames(xsize, ysize, tsize, [i], levels=levels, xyscale=xyscale, tscale=tscale, xscale=xscale, yscale=yscale, seed=seed)
|
|
48
|
+
filtered = apply_filters(frame[None], filters)[0] # TODO I don't think this will work with the photodiode filter
|
|
49
|
+
disc = discretize(filtered[:,:,0])
|
|
50
|
+
writer.append_data(disc)
|
|
51
|
+
writer.close()
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
import warnings
|
|
4
|
+
import tempfile
|
|
5
|
+
from subprocess import call
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import numpy as np
|
|
8
|
+
import imageio
|
|
9
|
+
from imageio_ffmpeg import get_ffmpeg_exe
|
|
10
|
+
|
|
11
|
+
from . import _perlin
|
|
12
|
+
from .util import generate_frames, filter_frames, filter_frames_index_function, XYSCALEBASE, discretize
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PerlinStimulus:
|
|
16
|
+
"""Class to generate a Perlin noise stimulus.
|
|
17
|
+
|
|
18
|
+
This is the "heavy duty" way of generating stimuli, mostly for experimenting
|
|
19
|
+
with new Perlin-based stimuli. If you would just like to generate zebra
|
|
20
|
+
noise, use the "zebra_noise" function instead, which is faster and more
|
|
21
|
+
memory efficient.
|
|
22
|
+
|
|
23
|
+
To generate noise, instantiate this method. This will automatically
|
|
24
|
+
generate Perlin noise which you can save, filter, etc.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, xsize, ysize, tdur, levels=10, xyscale=.2, tscale=50, fps=30, xscale=1.0, yscale=1.0, seed=0, demean="both", cachedir="perlcache", delay_batch=False):
|
|
28
|
+
"""Initialise Perlin noise stimulus.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
xsize,ysize : int > 0
|
|
33
|
+
The number of pixels in the x or y dimension
|
|
34
|
+
tdur : int > 0
|
|
35
|
+
The duration of the stimulus, in seconds. Sometimes will be slightly longer.
|
|
36
|
+
levels : int > 0, default: 10
|
|
37
|
+
The number of spatial scales to include. The default is usually okay.
|
|
38
|
+
xyscale : float ∈ [0,1], default: 0.5
|
|
39
|
+
The spatial grain scale of the noise, larger is more granular
|
|
40
|
+
xscale : float, default: 1.0
|
|
41
|
+
The spatial scale in x, larger is a bigger scale
|
|
42
|
+
yscale : float, default: 1.0
|
|
43
|
+
The spatial scale in y, larger is a bigger scale
|
|
44
|
+
tscale : int > 0, default: 1
|
|
45
|
+
The temporal scaling of the noise, larger is slower.
|
|
46
|
+
fps : int > 0, default: 30
|
|
47
|
+
Frames per second of the stimulus
|
|
48
|
+
seed : int ∈ [0, 255], default: 0
|
|
49
|
+
Random seed for the noise
|
|
50
|
+
demean : {'both', 'time', 'space', 'none'}, default: 'both'
|
|
51
|
+
Dimensions across which to fix the mean to zero
|
|
52
|
+
cachedir : str
|
|
53
|
+
A file path to the cache directory. Will contain large files.
|
|
54
|
+
delay_batch : bool
|
|
55
|
+
If true, do not immediately generate perlin noise. Manually call self.generate_batch() instead.
|
|
56
|
+
|
|
57
|
+
Notes
|
|
58
|
+
-----
|
|
59
|
+
This causes data to be written to the disk and long-running
|
|
60
|
+
computations to be performed
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
assert demean in ["both", "time", "space", "none"]
|
|
64
|
+
tsize = int(tdur*fps)
|
|
65
|
+
tscale = tscale
|
|
66
|
+
self.ratio = xsize/ysize*XYSCALEBASE
|
|
67
|
+
assert self.ratio == int(self.ratio), f"Ratio between x and y times {XYSCALEBASE} must be an integer"
|
|
68
|
+
self.ratio = int(self.ratio)
|
|
69
|
+
textra = (tscale - (tsize % tscale)) % tscale
|
|
70
|
+
if textra > 0:
|
|
71
|
+
warnings.warn(f"Adding {textra} extra timepoints to make tscale a multiple of tdur")
|
|
72
|
+
tsize += round(textra)
|
|
73
|
+
self.size = (xsize, ysize, tsize)
|
|
74
|
+
assert self.size[0] >= self.size[1], "Wrong orientation"
|
|
75
|
+
self.fps = fps
|
|
76
|
+
self.seed = seed
|
|
77
|
+
self.cachedir = Path(cachedir)
|
|
78
|
+
self.cachedir.mkdir(exist_ok=True)
|
|
79
|
+
self.tmpdir = Path(tempfile.mkdtemp())
|
|
80
|
+
self.xyscale = xyscale
|
|
81
|
+
self.tscale = tscale
|
|
82
|
+
self.levels = levels
|
|
83
|
+
self.demean = demean
|
|
84
|
+
self.xscale = xscale
|
|
85
|
+
self.yscale = yscale
|
|
86
|
+
self.batch_size = int(400000000/(self.size[0]*self.size[1]))
|
|
87
|
+
#self.batch_size = int(400000/(self.size[0]*self.size[1]))
|
|
88
|
+
if self.batch_size % 2 == 1: # Make sure it is an even number
|
|
89
|
+
self.batch_size += 1
|
|
90
|
+
if not delay_batch:
|
|
91
|
+
self.generate_batch()
|
|
92
|
+
def cache_filename(self, batch=None):
|
|
93
|
+
"""Return the filename for the cache.
|
|
94
|
+
|
|
95
|
+
The cache is split up into different batches, saved in different files
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
batch : int, 'stats', or None
|
|
100
|
+
The ID of the batch to find.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
str
|
|
105
|
+
The filename of the cache
|
|
106
|
+
"""
|
|
107
|
+
h = hashlib.md5(str((self.size, self.fps, self.seed, self.xyscale, self.tscale, self.levels, self.demean)).encode()).hexdigest()
|
|
108
|
+
if batch is None:
|
|
109
|
+
return str(self.cachedir.joinpath(f"perlcache_{h}.npy"))
|
|
110
|
+
if batch == "stats":
|
|
111
|
+
return str(self.cachedir.joinpath(f"perlcache_{h}_stats.npz"))
|
|
112
|
+
return str(self.cachedir.joinpath(f"perlcache_{h}_{batch}.npy"))
|
|
113
|
+
def generate_frame(self, t=0, filters=[]):
|
|
114
|
+
"""Generate and return a single image of noise.
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
t : float
|
|
120
|
+
the frame at which to generate the image
|
|
121
|
+
filters : list of str and/or (str, ...) tuples
|
|
122
|
+
A list of filters to apply to the stimulus. If a filter requires
|
|
123
|
+
parameters, pass a tuple, where the first element is the name of
|
|
124
|
+
the filter and the subsequent elements are the parameters.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
2-dimensional ndarray
|
|
129
|
+
An image of the noise
|
|
130
|
+
"""
|
|
131
|
+
arr = generate_frames(self.size[0], self.size[1], self.size[2], timepoints=[t] if not hasattr(t, "__iter__") else t, levels=self.levels,
|
|
132
|
+
xyscale=self.xyscale, tscale=self.tscale, xscale=self.xscale,
|
|
133
|
+
yscale=self.yscale, fps=self.fps, seed=self.seed)
|
|
134
|
+
if self.demean in ["both", "time"]:
|
|
135
|
+
arr -= np.mean(arr, axis=(0,1), keepdims=True)
|
|
136
|
+
arr = apply_filters(arr, filters)
|
|
137
|
+
return arr.squeeze()
|
|
138
|
+
|
|
139
|
+
def generate_batch(self):
|
|
140
|
+
"""Create the stimulus
|
|
141
|
+
|
|
142
|
+
Runs the _perlin C module and saves the output in batches.
|
|
143
|
+
|
|
144
|
+
If this function has already been run before and has been cached on the
|
|
145
|
+
filesytem, automatically load the statistics from it. Otherwise,
|
|
146
|
+
generate the stimuli and cache them.
|
|
147
|
+
"""
|
|
148
|
+
# If the cache already exists, load the statistics (used for
|
|
149
|
+
# normalisation) and exit immediately.
|
|
150
|
+
if os.path.isfile(self.cache_filename("stats")):
|
|
151
|
+
stats = np.load(self.cache_filename("stats"))
|
|
152
|
+
self.min_ = stats['min_']
|
|
153
|
+
self.max_ = stats['max_']
|
|
154
|
+
self.nframes = stats['nframes']
|
|
155
|
+
return
|
|
156
|
+
# Generate the stimuli and save the means and mins/maxes. If we are
|
|
157
|
+
# demeaning across space, we won't end up using the mins or maxes saved
|
|
158
|
+
# here.
|
|
159
|
+
means_t = []
|
|
160
|
+
means_xy = []
|
|
161
|
+
weights = []
|
|
162
|
+
mins = []
|
|
163
|
+
maxes = []
|
|
164
|
+
for k in range(0, int(np.ceil(self.size[2]/self.batch_size))):
|
|
165
|
+
print(f"Generating batch {k}")
|
|
166
|
+
arr = generate_frames(self.size[0], self.size[1], tsize=self.size[2],
|
|
167
|
+
timepoints=list(range(k*self.batch_size,min(self.size[2],(k+1)*self.batch_size))),
|
|
168
|
+
levels=self.levels, xyscale=self.xyscale, tscale=self.tscale, xscale=self.xscale,
|
|
169
|
+
yscale=self.yscale, fps=self.fps, seed=self.seed)
|
|
170
|
+
if self.demean in ["both", "time"]:
|
|
171
|
+
arr -= np.mean(arr, axis=(0,1), keepdims=True)
|
|
172
|
+
mins.append(np.min(arr))
|
|
173
|
+
maxes.append(np.max(arr))
|
|
174
|
+
means_t.append(np.mean(arr, axis=2))
|
|
175
|
+
weights.append(arr.shape[2])
|
|
176
|
+
np.save(self.cache_filename(k), arr.astype("float16"))
|
|
177
|
+
del arr
|
|
178
|
+
# Compute the spatial mean (across t)
|
|
179
|
+
mean_t = np.sum(means_t*(np.asarray(weights)[:,None,None]), axis=0)/np.sum(weights)
|
|
180
|
+
nframes = np.sum(weights)
|
|
181
|
+
# Optionally remove the spatial mean. If so, iterate through and
|
|
182
|
+
# update all of the saved data files to remove the spatial mean.
|
|
183
|
+
if self.demean in ["both", "space"]:
|
|
184
|
+
k = 0
|
|
185
|
+
mins = []
|
|
186
|
+
maxes = []
|
|
187
|
+
while os.path.isfile(self.cache_filename(k)):
|
|
188
|
+
arr = np.load(self.cache_filename(k)).astype("float32")
|
|
189
|
+
arr -= mean_t[:,:,None]
|
|
190
|
+
mins.append(np.min(arr))
|
|
191
|
+
maxes.append(np.max(arr))
|
|
192
|
+
np.save(self.cache_filename(k), arr.astype("float16"))
|
|
193
|
+
del arr
|
|
194
|
+
k += 1
|
|
195
|
+
min_ = np.min(mins)
|
|
196
|
+
max_ = np.max(maxes)
|
|
197
|
+
# Save the mins and maxes.
|
|
198
|
+
np.savez_compressed(self.cache_filename("stats"), min_=min_, max_=max_, nframes=nframes)
|
|
199
|
+
self.min_ = min_
|
|
200
|
+
self.max_ = max_
|
|
201
|
+
self.nframes = nframes
|
|
202
|
+
def save_grey_pad(self, fn, dur, bitrate=20):
|
|
203
|
+
"""Create a grey screen video which can be used to pad"""
|
|
204
|
+
one_frame = np.load(self.cache_filename(0), mmap_mode='r')[:,:,0].astype('uint8') * 0 + 127
|
|
205
|
+
imageio.imsave(self.tmpdir.joinpath("_grey.tif"), one_frame, format="pillow", compression="tiff_adobe_deflate")
|
|
206
|
+
n_frames = int(dur * self.fps)
|
|
207
|
+
for i in range(0, n_frames):
|
|
208
|
+
os.link(self.tmpdir.joinpath("_grey.tif"), self.tmpdir.joinpath(f"_frame{i:05}.tif"))
|
|
209
|
+
if fn[-4:] != ".mp4":
|
|
210
|
+
fn += ".mp4"
|
|
211
|
+
call([get_ffmpeg_exe(), "-r", str(self.fps), "-i", str(self.tmpdir.joinpath("_frame%5d.tif")), "-c:v", "mpeg2video", "-an", "-b:v", f"{bitrate}M", fn])
|
|
212
|
+
for p in self.tmpdir.glob("_frame*.tif"):
|
|
213
|
+
p.unlink()
|
|
214
|
+
self.tmpdir.joinpath("_grey.tif").unlink()
|
|
215
|
+
def save_video(self, fn, loop=1, filters=[], bitrate=20):
|
|
216
|
+
"""Save the filtered stimulus.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
fn : str
|
|
221
|
+
The file name to save the video
|
|
222
|
+
loop : int > 0
|
|
223
|
+
The number of times the stimulus should loop in the saved video
|
|
224
|
+
filters : list of str and/or (str, ...) tuples
|
|
225
|
+
A list of filters to apply to the stimulus. If a filter requires
|
|
226
|
+
parameters, pass a tuple, where the first element is the name of
|
|
227
|
+
the filter and the subsequent elements are the parameters.
|
|
228
|
+
bitrate : int > 0
|
|
229
|
+
The bitrate in megabits per second. The default is good for binary
|
|
230
|
+
videos and pure noise, but a higher value may be necessary if
|
|
231
|
+
using the "wood" filter.
|
|
232
|
+
"""
|
|
233
|
+
if Path(fn).exists():
|
|
234
|
+
raise IOError("Output video file already exists!")
|
|
235
|
+
i = 0
|
|
236
|
+
k = 0
|
|
237
|
+
filtind = filter_frames_index_function(filters, nframes=self.nframes)
|
|
238
|
+
while os.path.isfile(self.cache_filename(k)):
|
|
239
|
+
data = np.load(self.cache_filename(k)).astype("float32")
|
|
240
|
+
# Renormalise using precomputed mins/maxes
|
|
241
|
+
data -= self.min_
|
|
242
|
+
data *= 1/(self.max_-self.min_)
|
|
243
|
+
# Apply filters
|
|
244
|
+
for f in filters:
|
|
245
|
+
if isinstance(f, str):
|
|
246
|
+
n = f
|
|
247
|
+
args = []
|
|
248
|
+
else:
|
|
249
|
+
n = f[0]
|
|
250
|
+
args = f[1:]
|
|
251
|
+
data = filter_frames(data, n, *args)
|
|
252
|
+
data = discretize(data)
|
|
253
|
+
assert data.dtype == 'uint8'
|
|
254
|
+
for j in range(0, data.shape[2]):
|
|
255
|
+
imageio.imsave(self.tmpdir.joinpath(f"_frame{filtind(i):05}.tif"), data[:,:,j], format="pillow", compression="tiff_adobe_deflate")
|
|
256
|
+
i += 1
|
|
257
|
+
k += 1
|
|
258
|
+
del data
|
|
259
|
+
n_frames = i
|
|
260
|
+
for j in range(1, loop):
|
|
261
|
+
for i in range(0, n_frames):
|
|
262
|
+
os.link(self.tmpdir.joinpath(f"_frame{i:05}.tif"), self.tmpdir.joinpath(f"_frame{(i+j*n_frames):05}.tif"))
|
|
263
|
+
if fn[-4:] != ".mp4":
|
|
264
|
+
fn += ".mp4"
|
|
265
|
+
call([get_ffmpeg_exe(), "-r", str(self.fps), "-i", str(self.tmpdir.joinpath("_frame%5d.tif")), "-c:v", "mpeg2video", "-an", "-b:v", f"{bitrate}M", fn])
|
|
266
|
+
for p in self.tmpdir.glob("_frame*.tif"):
|
|
267
|
+
p.unlink()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.ndimage
|
|
3
|
+
from . import _perlin
|
|
4
|
+
|
|
5
|
+
XYSCALEBASE = 100
|
|
6
|
+
|
|
7
|
+
def filter_frames(im, filt, *args):
|
|
8
|
+
"""Apply a filter/transformation to an image batch
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
im : 3D float ndarray, values ∈ [0,1]
|
|
13
|
+
Frames to filter
|
|
14
|
+
filt : str
|
|
15
|
+
The name of the filter
|
|
16
|
+
*args : tuple
|
|
17
|
+
Extra arguments are passed to the filter
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
im : 3D float ndarray, values ∈ [0,1]
|
|
22
|
+
Filtered noise movie
|
|
23
|
+
"""
|
|
24
|
+
if filt == "threshold":
|
|
25
|
+
return (im>args[0]).astype(np.float32)
|
|
26
|
+
if filt == "softthresh":
|
|
27
|
+
return 1/(1+np.exp(-args[0]*(im-.5)))
|
|
28
|
+
if filt == "comb":
|
|
29
|
+
return (im//args[0] % 2 == 1).astype(np.float32)
|
|
30
|
+
if filt == "invert":
|
|
31
|
+
return 1-im
|
|
32
|
+
if filt == "reverse":
|
|
33
|
+
return im # We need to use filter_index_function for this
|
|
34
|
+
if filt == "blur":
|
|
35
|
+
return np.asarray([scipy.ndimage.filters.gaussian_filter(im.astype(np.float32)[:,:,i], args[0], mode='wrap') for i in range(0, im.shape[2])]).transpose([1,2,0])
|
|
36
|
+
if filt == "wood":
|
|
37
|
+
return (im % args[0]) / args[0]
|
|
38
|
+
if filt == "center":
|
|
39
|
+
return 1-(np.abs(im-.5)*2)
|
|
40
|
+
if filt == "photodiode":
|
|
41
|
+
im = im.copy()
|
|
42
|
+
s = args[0]
|
|
43
|
+
im[:s,-s:,::2] = 0
|
|
44
|
+
im[:s,-s:,1::2] = 1
|
|
45
|
+
return im
|
|
46
|
+
if filt == "photodiode_anywhere":
|
|
47
|
+
im = im.copy()
|
|
48
|
+
x = args[0]
|
|
49
|
+
y = args[1]
|
|
50
|
+
s = args[2]
|
|
51
|
+
im[y:(y+s),x:(x+s),::2] = 0
|
|
52
|
+
im[y:(y+s),x:(x+s),1::2] = 1
|
|
53
|
+
return im
|
|
54
|
+
if filt == "photodiode_b2":
|
|
55
|
+
im = im.copy()
|
|
56
|
+
s = 125
|
|
57
|
+
im[:s,-s:,::2] = 0
|
|
58
|
+
im[:s,-s:,1::2] = 1
|
|
59
|
+
return im
|
|
60
|
+
if filt == "photodiode_fusi":
|
|
61
|
+
im = im.copy()
|
|
62
|
+
s = 75
|
|
63
|
+
im[:s,-s:,::2] = 0
|
|
64
|
+
im[:s,-s:,1::2] = 1
|
|
65
|
+
return im
|
|
66
|
+
if filt == "photodiode_bscope":
|
|
67
|
+
im = im.copy()
|
|
68
|
+
s = 100
|
|
69
|
+
im[-s:,:s,::2] = 0
|
|
70
|
+
im[-s:,:s,1::2] = 1
|
|
71
|
+
return im
|
|
72
|
+
if callable(filt):
|
|
73
|
+
return filt(im)
|
|
74
|
+
raise ValueError("Invalid filter specified")
|
|
75
|
+
|
|
76
|
+
def apply_filters(arr, filters):
|
|
77
|
+
for f in filters:
|
|
78
|
+
if isinstance(f, str):
|
|
79
|
+
n = f
|
|
80
|
+
args = []
|
|
81
|
+
else:
|
|
82
|
+
n = f[0]
|
|
83
|
+
args = f[1:]
|
|
84
|
+
arr = filter_frames(arr, n, *args)
|
|
85
|
+
return arr
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def filter_frames_index_function(filters, nframes):
|
|
89
|
+
"""Reordering frames in the video based on the filter.
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
filters : list of strings or tuples
|
|
95
|
+
the list of filters passed to save_video
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
function mapping int -> int
|
|
100
|
+
Reindexing function
|
|
101
|
+
|
|
102
|
+
Notes
|
|
103
|
+
-----
|
|
104
|
+
Some filters may need to operate on the global video instead of in
|
|
105
|
+
batches. However, for large videos, batches are necessary due to
|
|
106
|
+
limited amounts of RAM. Thus, this function should return another
|
|
107
|
+
function which takes an index as input and outputs a new index,
|
|
108
|
+
remapping the initial noise frame to the output video frame. This
|
|
109
|
+
was primarily designed to support reversing the video, but it might be
|
|
110
|
+
useful for other things too.
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
if "reverse" in filters:
|
|
114
|
+
return lambda x : nframes - x - 1
|
|
115
|
+
return lambda x : x
|
|
116
|
+
|
|
117
|
+
def discretize(im):
|
|
118
|
+
"""Convert movie to an unsigned 8-bit integer
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
im : 3D float ndarray, values ∈ [0,1]
|
|
123
|
+
Noise movie
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
3D int ndarray, values ∈ [0,255]
|
|
128
|
+
Noise movie
|
|
129
|
+
"""
|
|
130
|
+
im *= 255
|
|
131
|
+
ret = im.astype(np.uint8)
|
|
132
|
+
return ret
|
|
133
|
+
|
|
134
|
+
def generate_frames(xsize, ysize, tsize, timepoints, levels=10, xyscale=.5, tscale=1, xscale=1.0, yscale=1.0, fps=30, seed=0):
|
|
135
|
+
"""Preprocess arguments before passing to the C implementation of Perlin noise.
|
|
136
|
+
"""
|
|
137
|
+
# Use the temporal scale and number of timepoints to compute how many
|
|
138
|
+
# units to make the stimulus across the temporal dimension
|
|
139
|
+
tunits = int(tsize/(tscale*(fps/30)))
|
|
140
|
+
ts_all = np.arange(0, tsize, dtype="float32")/(tscale*(fps/30))
|
|
141
|
+
ratio = int(xsize/ysize*XYSCALEBASE)
|
|
142
|
+
arr = _perlin.make_perlin(np.arange(0, xsize, dtype="float32")/ysize/xscale, # Yes, divide by y size
|
|
143
|
+
np.arange(0, ysize, dtype="float32")/ysize/yscale,
|
|
144
|
+
ts_all[timepoints],
|
|
145
|
+
octaves=levels,
|
|
146
|
+
persistence=xyscale,
|
|
147
|
+
repeatx=ratio,
|
|
148
|
+
repeaty=XYSCALEBASE,
|
|
149
|
+
repeatz=tunits,
|
|
150
|
+
base=seed)
|
|
151
|
+
arr = arr.swapaxes(0,1)
|
|
152
|
+
return arr
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: zebranoise
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zebra noise stimuli for Python
|
|
5
|
+
Home-page: https://github.com/mwshinn/zebra_noise
|
|
6
|
+
Author: Max Shinn
|
|
7
|
+
Author-email: m.shinn@ucl.ac.uk
|
|
8
|
+
License: UNKNOWN
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: C
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE.txt
|
|
19
|
+
|
|
20
|
+
This library will efficiently generate visual stimulus videos using Perlin noise, including zebra noise. The core algorithm is in optimized C.
|
|
21
|
+
|
|
22
|
+
[Example zebra noise](http://www.youtube.com/watch?v=-SyjgbNCP4Q)
|
|
23
|
+
|
|
24
|
+
[](http://www.youtube.com/watch?v=-SyjgbNCP4Q "Example zebra noise")
|
|
25
|
+
|
|
26
|
+
# Installation
|
|
27
|
+
|
|
28
|
+
## From pypi
|
|
29
|
+
|
|
30
|
+
Run:
|
|
31
|
+
|
|
32
|
+
pip install zebranoise
|
|
33
|
+
|
|
34
|
+
## From source
|
|
35
|
+
|
|
36
|
+
Make sure you have a C compiler installed. The easiest way to do this is by installing Cython.
|
|
37
|
+
|
|
38
|
+
Then, install using the standard
|
|
39
|
+
|
|
40
|
+
python setup.py install
|
|
41
|
+
|
|
42
|
+
This will compile the sources and install.
|
|
43
|
+
|
|
44
|
+
I have only tested this on Linux.
|
|
45
|
+
|
|
46
|
+
# Usage
|
|
47
|
+
|
|
48
|
+
## Basic usage
|
|
49
|
+
|
|
50
|
+
If you would like to generate a zebra noise of size 640x480 for 2 minutes, you can run
|
|
51
|
+
|
|
52
|
+
import zebranoise
|
|
53
|
+
zebranoise.zebra_noise("output.mp4", xsize=640, ysize=480, tdur=60*2, fps=30, seed=0)
|
|
54
|
+
|
|
55
|
+
Other parameters control the spatial and temporal frequencies. For instance,
|
|
56
|
+
|
|
57
|
+
zebranoise.zebra_noise("output2.mp4", xsize=640, ysize=480, tdur=60*2, levels=10, xyscale=.2, tscale=50, fps=30, xscale=1.0, yscale=1.0, seed=0)
|
|
58
|
+
|
|
59
|
+
The meaning of these parameters is:
|
|
60
|
+
|
|
61
|
+
- **xsize** and **ysize**: the x and y dimensions of the output video. (Sometimes
|
|
62
|
+
these will be rounded up to multiples of 16.)
|
|
63
|
+
- **tdur**: the duration in seconds
|
|
64
|
+
- **levels**: The number of octaves to use when approximating the 1/f spectrum.
|
|
65
|
+
The default of 10 should be more than enough.
|
|
66
|
+
- **xyscale**: The spatial scale of the Perlin noise, from 0 to 1. Low values
|
|
67
|
+
will make the video smoother and high values choppier.
|
|
68
|
+
- **tscale**: The speed of the video
|
|
69
|
+
- **xscale** and **yscale**: Resize the x and y dimensions of the output.
|
|
70
|
+
- **fps**: Frames per second
|
|
71
|
+
- **seed**: Random seed
|
|
72
|
+
|
|
73
|
+
## Advanced usage
|
|
74
|
+
|
|
75
|
+
This package can be used to generate new stimuli based on Perlin noise.
|
|
76
|
+
|
|
77
|
+
To generate a Perlin noise-based video, follow two steps with the package. First, generate the Perlin noise. This is the most time consuming step, and will also use a lot of temporary disk space. This is accomplished with:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
noise = zebranoise.PerlinStimulus(xsize=400, ysize=100, tdur=60*2, xyscale=.2, tscale=50, seed=0)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
See the function documentation for perlstim.Perl for more information about modifying the properties of the noise.
|
|
84
|
+
|
|
85
|
+
Second, apply "filters" to the Perlin noise and save the resulting video. Filters include thresholds, sync squares in the corner, etc. If using a filter with no arguments, just pass a string naming the filter. If using a filter with arguments, pass a tuple where the first element is the name of the filter as a string, and the remaining elements are the arguments. For example,
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
noise.save_video("noise.mp4", filters=["reverse", ("comb", .05)])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The above example applies the "reverse" filter (with no arguments) and the "comb" filter with the argument 0.1.
|
|
92
|
+
|
|
93
|
+
## Filters
|
|
94
|
+
|
|
95
|
+
The following filters are currently defined:
|
|
96
|
+
|
|
97
|
+
- "threshold" (1 argument): set all values above the threshold argument to white, and all below to black.
|
|
98
|
+
- "softthresh" (1 argument): set values much greater than 0 to white and much less than zero to blac. Use shades of grey for intermediate values, according to a sigmoid with a temperature given as the argument.
|
|
99
|
+
- "comb" (1 argument): Alternate black and white thresholds at intervals given by the argument.
|
|
100
|
+
- "reverse" (0 arguments): play the video in reverse
|
|
101
|
+
- "invert" (0 arguments): switch white and black
|
|
102
|
+
- "wood" (1 argument): similar to "comb", but use a sawtooth wave
|
|
103
|
+
- "blur" (1 argument): Gaussian blur with the given spatial standard deviation
|
|
104
|
+
- "photodiode" (1 argument): Draw a sync square in the corner, with a size given by the argument.
|
|
105
|
+
- "photodiode_anywhere" (3 arguments): Draw a sync square at the coordinates given by the first two arguments (x and y), and of size given by argument 3.
|
|
106
|
+
- [any function] (0 arguments): If you pass a function, the function will be applied to each chunk of the video. Chunks consist of the entire x and y but a subset of z, guaranteed to be an even number.
|
|
107
|
+
|
|
108
|
+
## Example
|
|
109
|
+
|
|
110
|
+
To get started, try the following:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import zebranoise
|
|
114
|
+
stim = zebranoise.PerlinStimulus(xsize=480, ysize=128, tdur=60*5, xyscale=.2, tscale=50)
|
|
115
|
+
stim.save_video("perlin_stimulus.mp4", loop=1, filters=[("comb", .08), ("photodiode", 30)])
|
|
116
|
+
```
|
|
117
|
+
# Other information
|
|
118
|
+
|
|
119
|
+
Much of the C code is based on [Casey Duncan's "noise" package for Python](https://github.com/caseman/noise), under the MIT license.
|
|
120
|
+
|
|
121
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
zebranoise/__init__.py
|
|
6
|
+
zebranoise/_perlin.c
|
|
7
|
+
zebranoise/_version.py
|
|
8
|
+
zebranoise/easy.py
|
|
9
|
+
zebranoise/perlin_stimulus.py
|
|
10
|
+
zebranoise/util.py
|
|
11
|
+
zebranoise.egg-info/PKG-INFO
|
|
12
|
+
zebranoise.egg-info/SOURCES.txt
|
|
13
|
+
zebranoise.egg-info/dependency_links.txt
|
|
14
|
+
zebranoise.egg-info/requires.txt
|
|
15
|
+
zebranoise.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zebranoise
|