tinybrain 1.4.1__tar.gz → 1.5.1__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.
Files changed (33) hide show
  1. {tinybrain-1.4.1 → tinybrain-1.5.1}/.github/workflows/build_wheel.yml +3 -3
  2. {tinybrain-1.4.1 → tinybrain-1.5.1}/.github/workflows/run_tests.yml +2 -2
  3. {tinybrain-1.4.1 → tinybrain-1.5.1}/ChangeLog +13 -0
  4. tinybrain-1.5.1/PKG-INFO +151 -0
  5. {tinybrain-1.4.1 → tinybrain-1.5.1}/automated_test.py +93 -42
  6. {tinybrain-1.4.1 → tinybrain-1.5.1}/pyproject.toml +1 -1
  7. {tinybrain-1.4.1 → tinybrain-1.5.1}/setup.py +1 -1
  8. {tinybrain-1.4.1 → tinybrain-1.5.1}/tinybrain/downsample.py +17 -3
  9. tinybrain-1.5.1/tinybrain.egg-info/PKG-INFO +151 -0
  10. tinybrain-1.5.1/tinybrain.egg-info/pbr.json +1 -0
  11. tinybrain-1.4.1/PKG-INFO +0 -150
  12. tinybrain-1.4.1/tinybrain.egg-info/PKG-INFO +0 -150
  13. tinybrain-1.4.1/tinybrain.egg-info/pbr.json +0 -1
  14. {tinybrain-1.4.1 → tinybrain-1.5.1}/AUTHORS +0 -0
  15. {tinybrain-1.4.1 → tinybrain-1.5.1}/LICENSE +0 -0
  16. {tinybrain-1.4.1 → tinybrain-1.5.1}/README.md +0 -0
  17. {tinybrain-1.4.1 → tinybrain-1.5.1}/build_linux.sh +0 -0
  18. {tinybrain-1.4.1 → tinybrain-1.5.1}/manylinux1.Dockerfile +0 -0
  19. {tinybrain-1.4.1 → tinybrain-1.5.1}/manylinux2010.Dockerfile +0 -0
  20. {tinybrain-1.4.1 → tinybrain-1.5.1}/manylinux2014.Dockerfile +0 -0
  21. {tinybrain-1.4.1 → tinybrain-1.5.1}/perf.py +0 -0
  22. {tinybrain-1.4.1 → tinybrain-1.5.1}/requirements.txt +0 -0
  23. {tinybrain-1.4.1 → tinybrain-1.5.1}/requirements_dev.txt +0 -0
  24. {tinybrain-1.4.1 → tinybrain-1.5.1}/setup.cfg +0 -0
  25. {tinybrain-1.4.1 → tinybrain-1.5.1}/tinybrain/__init__.py +0 -0
  26. {tinybrain-1.4.1 → tinybrain-1.5.1}/tinybrain/accelerated.hpp +0 -0
  27. {tinybrain-1.4.1 → tinybrain-1.5.1}/tinybrain/accelerated.pyx +0 -0
  28. {tinybrain-1.4.1 → tinybrain-1.5.1}/tinybrain.egg-info/SOURCES.txt +0 -0
  29. {tinybrain-1.4.1 → tinybrain-1.5.1}/tinybrain.egg-info/dependency_links.txt +0 -0
  30. {tinybrain-1.4.1 → tinybrain-1.5.1}/tinybrain.egg-info/not-zip-safe +0 -0
  31. {tinybrain-1.4.1 → tinybrain-1.5.1}/tinybrain.egg-info/requires.txt +0 -0
  32. {tinybrain-1.4.1 → tinybrain-1.5.1}/tinybrain.egg-info/top_level.txt +0 -0
  33. {tinybrain-1.4.1 → tinybrain-1.5.1}/tox.ini +0 -0
@@ -6,7 +6,7 @@ on:
6
6
  tags:
7
7
  - '*'
8
8
  env:
9
- CIBW_SKIP: cp27-* cp33-* cp34-* cp35-* cp36-* cp37-* pp* *-musllinux*
9
+ CIBW_SKIP: pp* *-musllinux*
10
10
 
11
11
  jobs:
12
12
  build_wheels:
@@ -28,11 +28,11 @@ jobs:
28
28
  uses: docker/setup-qemu-action@v1
29
29
 
30
30
  - name: Build wheels
31
- uses: pypa/cibuildwheel@v2.16.2
31
+ uses: pypa/cibuildwheel@v2.19.1
32
32
  # to supply options, put them in 'env', like:
33
33
  env:
34
34
  CIBW_ARCHS_LINUX: ${{matrix.arch}}
35
- CIBW_BEFORE_BUILD: pip install oldest-supported-numpy setuptools wheel cython
35
+ CIBW_BEFORE_BUILD: pip install numpy setuptools wheel cython
36
36
 
37
37
  - uses: actions/upload-artifact@v2
38
38
  with:
@@ -15,7 +15,7 @@ jobs:
15
15
  strategy:
16
16
  matrix:
17
17
  os: [ubuntu-20.04, macos-latest, windows-2019]
18
- python-version: ["3.7", "3.8", "3.9", "3.10"]
18
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
19
19
 
20
20
  steps:
21
21
  - name: Set up Python ${{ matrix.python-version }}
@@ -28,7 +28,7 @@ jobs:
28
28
  - name: Install dependencies
29
29
  run: |
30
30
  python -m pip install --upgrade pip
31
- python -m pip install pytest -r requirements.txt -r requirements_dev.txt
31
+ python -m pip install pytest -r requirements.txt -r requirements_dev.txt setuptools wheel
32
32
 
33
33
  - name: Compile
34
34
  run: python setup.py develop
@@ -1,6 +1,19 @@
1
1
  CHANGES
2
2
  =======
3
3
 
4
+ 1.5.1
5
+ -----
6
+
7
+ * ci: rebuild for numpy 2.0
8
+
9
+ 1.5.0
10
+ -----
11
+
12
+ * ci: install setuptools, wheel
13
+ * ci: update cibuildwheel to non-buggy version
14
+ * ci: update tests, -3.7, +3.11, +3.12
15
+ * feat: add min pooling
16
+
4
17
  1.4.1
5
18
  -----
6
19
 
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.1
2
+ Name: tinybrain
3
+ Version: 1.5.1
4
+ Summary: Image pyramid generation specialized for connectomics data types and procedures.
5
+ Home-page: https://github.com/seung-lab/tinybrain/
6
+ Author: William Silversmith
7
+ Author-email: ws9@princeton.edu
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ License-File: AUTHORS
22
+ Requires-Dist: numpy
23
+
24
+ [![Build Status](https://travis-ci.org/seung-lab/tinybrain.svg?branch=master)](https://travis-ci.org/seung-lab/tinybrain) [![PyPI version](https://badge.fury.io/py/tinybrain.svg)](https://badge.fury.io/py/tinybrain)
25
+
26
+ # tinybrain
27
+
28
+ Image pyramid generation specialized for connectomics data types and procedures. If your brain wasn't tiny before, it will be now.
29
+
30
+ ```python
31
+ import tinybrain
32
+
33
+ img = load_3d_em_stack()
34
+
35
+ # 2x2 and 2x2x2 downsamples are on a fast path.
36
+ # e.g. (2,2), (2,2,1), (2,2,1,1), (2,2,2), (2,2,2,1)
37
+ img_pyramid = tinybrain.downsample_with_averaging(img, factor=(2,2,1), num_mips=5, sparse=False)
38
+
39
+ labels = load_3d_labels()
40
+ label_pyramid = tinybrain.downsample_segmentation(labels, factor=(2,2,1), num_mips=5, sparse=False))
41
+ ```
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install numpy
47
+ pip install tinybrain
48
+ ```
49
+
50
+ ## Motivation
51
+
52
+ Image hierarchy generation in connectomics uses a few different techniques for
53
+ visualizing data, but predominantly we create image pyramids of uint8 grayscale images using 2x2 average pooling and of uint8 to uint64 segmentation labels using 2x2 mode pooling. When images become very large and people wish to visualze upper mip levels using three axes at once, it becomes desirable to perform 2x2x2 downsamples to maintain isotropy.
54
+
55
+ It's possible to compute both of these using numpy, however as multiple packages found it useful to copy the downsample functions, it makes sense to formalize these functions into a seperate library located on PyPI.
56
+
57
+ Given the disparate circumstances that they will be used in, these functions should work
58
+ fast as possible with low memory usage and avoid numerical issues such as integer truncation
59
+ while generating multiple mip levels.
60
+
61
+ ## Considerations: downsample_with_averaging
62
+
63
+ It's advisable to generate multiple mip levels at once rather than recursively computing
64
+ new images as for integer type images, this leads to integer truncation issues. In the common
65
+ case of 2x2x1 downsampling, a recursively computed image would lose 0.75 brightness per a
66
+ mip level. Therefore, take advantage of the `num_mips` argument which strikes a balance
67
+ that limits integer truncation loss to once every 4 mip levels. This compromise allows
68
+ for the use of integer arithmatic and no more memory usage than 2x the input image including
69
+ the output downsamples. If you seek to eliminate the loss beyond 4 mip levels, try promoting
70
+ the type before downsampling. 2x2x2x1 downsamples truncate every 8 mip levels.
71
+
72
+ A C++ high performance path is triggered for 2x2x1x1 and 2x2x2x1 downsample factors on uint8, uint16, float32,
73
+ and float64 data types in Fortran order. Other factors, data types, and orderings are computed using a numpy pathway that is much slower and more memory intensive.
74
+
75
+ We also include a sparse mode for downsampling 2x2x2 patches, which prevents "ghosting" where one z-slice overlaps a black region on the next slice and becomes semi-transparent after downsampling. We deal with this by neglecting the background pixels from the averaging operation.
76
+
77
+ ### Example Benchmark
78
+
79
+ On a 1024x1024x100 uint8 image I ran the following code. PIL and OpenCV are actually much faster than this benchmark shows because most of the time is spent writing to the numpy array. tinybrain has a large advantage working on 3D and 4D arrays. Of course, this is a very simple benchmark and it may be possible to tune each of these approaches. On single slices, Pillow was faster than tinybrain.
80
+
81
+ ```python
82
+ img = np.load("image.npy")
83
+
84
+ s = time.time()
85
+ downsample_with_averaging(img, (2,2,1))
86
+ print("Original ", time.time() - s)
87
+
88
+ s = time.time()
89
+ out = tinybrain.downsample_with_averaging(img, (2,2,1))
90
+ print("tinybrain ", time.time() - s)
91
+
92
+ s = time.time()
93
+ out = np.zeros(shape=(512,512,100))
94
+ for z in range(img.shape[2]):
95
+ out[:,:,z] = cv2.resize(img[:,:,z], dsize=(512, 512) )
96
+ print("OpenCV ", time.time() - s)
97
+
98
+ s = time.time()
99
+ out = np.zeros(shape=(512,512,100))
100
+ for z in range(img.shape[2]):
101
+ pilimg = Image.fromarray(img[:,:,z])
102
+ out[:,:,z] = pilimg.resize( (512, 512) )
103
+ print("Pillow ", time.time() - s)
104
+
105
+ # Method Run Time Rel. Perf.
106
+ # Original 1820 ms +/- 3.73 ms 1.0x
107
+ # tinybrain 67 ms +/- 0.40 ms 27.2x
108
+ # OpenCV 469 ms +/- 1.12 ms 3.9x
109
+ # Pillow 937 ms +/- 7.63 ms 1.9x
110
+ ```
111
+
112
+ Here's the output from `perf.py` on an Apple Silicon 2021 Macbook Pro M1.
113
+ Note that the image used was a random 2048x2048x64 array that was a uint8
114
+ for average pooling and a uint64 for mode pooling to represent real use cases more fairly. In the table, read it as 2D or 3D downsamples, generating a single or multiple mip levels, with sparse mode enabled or disabled. The speed values are in megavoxels per a second and are the mean of ten runs.
115
+
116
+
117
+ | dwnsmpl | mips | sparse | AVG (MVx/sec) | MODE (MVx/sec) |
118
+ |----------|---------|-----------|------------------|-------------------|
119
+ | 2x2 | 1 | N | 3856.07 | 1057.87 |
120
+ | 2x2 | 2 | N | 2685.80 | 1062.69 |
121
+ | 2x2 | 1 | Y | N/A | 129.64 |
122
+ | 2x2 | 2 | Y | N/A | 81.62 |
123
+ | 2x2x2 | 1 | N | 4468.55 | 336.85 |
124
+ | 2x2x2 | 2 | N | 2867.80 | 298.45 |
125
+ | 2x2x2 | 1 | Y | 1389.47 | 337.87 |
126
+ | 2x2x2 | 2 | Y | 1259.58 | 293.84 |
127
+
128
+ As the downsampling code's performance is data dependent due to branching, I also used [`connectomics.npy`](https://github.com/seung-lab/connected-components-3d/blob/master/benchmarks/connectomics.npy.gz) (512<sup>3</sup> uint32 extended to uint64) to see how that affected performance. This data comes from mouse visual cortex and has many equal adjacent voxels. In this volume, the 2x2x2 non-sparse mode is much faster as the "instant" majority detection can skip examining half the voxels in many cases.
129
+
130
+ | dwnsmpl | mips | sparse | MODE (MVx/sec) |
131
+ |----------|---------|-----------|-------------------|
132
+ | 2x2 | 1 | N | 1078.09 |
133
+ | 2x2 | 2 | N | 1030.90 |
134
+ | 2x2 | 1 | Y | 146.15 |
135
+ | 2x2 | 2 | Y | 69.25 |
136
+ | 2x2x2 | 1 | N | 1966.74 |
137
+ | 2x2x2 | 2 | N | 1790.60 |
138
+ | 2x2x2 | 1 | Y | 2041.96 |
139
+ | 2x2x2 | 2 | Y | 1758.42 |
140
+
141
+
142
+ ## Considerations: downsample_segmentation
143
+
144
+ The `downsample_segmentation` function performs mode pooling operations provided the downsample factor is a power of two, including in three dimensions. If the factor is a non-power of two, striding is used. The mode pooling, which is usually what you want, is computed recursively. Mode pooling is superior to striding, but the recursive calculation can introduce defects at mip levels higher than 1. This may be improved in the future.
145
+
146
+ The way the calculation is actually done uses an ensemble of several different methods. For (2,2,1,1) and (2,2,2,1) downsamples, a Cython fast, low memory path is selected. (2,2,1,1) implements [*countless if*](https://towardsdatascience.com/countless-high-performance-2x-downsampling-of-labeled-images-using-python-and-numpy-e70ad3275589). (2,2,2,1) uses a combination of counting and "instant" majority detection. For (4,4,1) or other 2D powers of two, the [*countless 2d*](https://towardsdatascience.com/countless-high-performance-2x-downsampling-of-labeled-images-using-python-and-numpy-e70ad3275589) algorithm is used. For (4,4,4), (8,8,8) etc, the [*dynamic countless 3d*](https://towardsdatascience.com/countless-3d-vectorized-2x-downsampling-of-labeled-volume-images-using-python-and-numpy-59d686c2f75) algorithm is used. For 2D powers of two, [*stippled countless 2d*](https://medium.com/@willsilversmith/countless-2d-inflated-2x-downsampling-of-labeled-images-holding-zero-values-as-background-4d13a7675f2d) is used if the sparse flag is enabled. For all other configurations, striding is used.
147
+
148
+ Countless 2d paths are also fast, but use slightly more memory and time. Countless 3D is okay for (2,2,2) and (4,4,4) but will use time and memory exponential in the product of dimensions. This state of affairs could be improved by implementing a counting based algorithm in Cython/C++ for arbitrary factors that doesn't compute recursively. The countless algorithms were developed before I knew how to write Cython and package libraries. However, C++ implementations of countless are much faster than counting for computing the first 2x2x1 mip level. In particular, an AVX2 SIMD implementation can saturate memory bandwidth.
149
+
150
+ Documentation for the countless algorithm family is located here: https://github.com/william-silversmith/countless
151
+
@@ -360,56 +360,56 @@ def test_downsample_segmentation_4x_x():
360
360
  result, = downsamplefn(result, (1,2,2))
361
361
  assert result.shape == (1024, 16, 128, 1)
362
362
 
363
- def test_downsample_max_pooling():
364
- for dtype in (np.int8, np.float32):
365
- cases = [
366
- np.array([ [ -1, 0 ], [ 0, 0 ] ], dtype=dtype),
367
- np.array([ [ 0, 0 ], [ 0, 0 ] ], dtype=dtype),
368
- np.array([ [ 0, 1 ], [ 0, 0 ] ], dtype=dtype),
369
- np.array([ [ 0, 1 ], [ 1, 0 ] ], dtype=dtype),
370
- np.array([ [ 0, 1 ], [ 0, 2 ] ], dtype=dtype)
371
- ]
372
-
373
- for i in range(len(cases)):
374
- case = cases[i]
375
- result, = tinybrain.downsample.downsample_with_max_pooling(case, (1, 1))
376
- assert np.all(result == cases[i])
363
+ @pytest.mark.parametrize("dtype", [np.int8, np.float32])
364
+ def test_downsample_max_pooling(dtype):
365
+ cases = [
366
+ np.array([ [ -1, 0 ], [ 0, 0 ] ], dtype=dtype),
367
+ np.array([ [ 0, 0 ], [ 0, 0 ] ], dtype=dtype),
368
+ np.array([ [ 0, 1 ], [ 0, 0 ] ], dtype=dtype),
369
+ np.array([ [ 0, 1 ], [ 1, 0 ] ], dtype=dtype),
370
+ np.array([ [ 0, 1 ], [ 0, 2 ] ], dtype=dtype)
371
+ ]
377
372
 
378
- answers = [ 0, 0, 1, 1, 2 ]
373
+ for i in range(len(cases)):
374
+ case = cases[i]
375
+ result, = tinybrain.downsample.downsample_with_max_pooling(case, (1, 1))
376
+ assert np.all(result == cases[i])
379
377
 
380
- for i in range(len(cases)):
381
- case = cases[i]
382
- result, = tinybrain.downsample.downsample_with_max_pooling(case, (2, 2))
383
- assert result == answers[i]
378
+ answers = [ 0, 0, 1, 1, 2 ]
384
379
 
380
+ for i in range(len(cases)):
381
+ case = cases[i]
382
+ result, = tinybrain.downsample.downsample_with_max_pooling(case, (2, 2))
383
+ assert result == answers[i]
385
384
 
386
- cast = lambda arr: np.array(arr, dtype=dtype)
387
385
 
388
- answers = list(map(cast, [
389
- [[ 0, 0 ]],
390
- [[ 0, 0 ]],
391
- [[ 0, 1 ]],
392
- [[ 1, 1 ]],
393
- [[ 0, 2 ]],
394
- ]))
386
+ cast = lambda arr: np.array(arr, dtype=dtype)
395
387
 
396
- for i in range(len(cases)):
397
- case = cases[i]
398
- result, = tinybrain.downsample.downsample_with_max_pooling(case, (2, 1))
399
- assert np.all(result == answers[i])
388
+ answers = list(map(cast, [
389
+ [[ 0, 0 ]],
390
+ [[ 0, 0 ]],
391
+ [[ 0, 1 ]],
392
+ [[ 1, 1 ]],
393
+ [[ 0, 2 ]],
394
+ ]))
400
395
 
401
- answers = list(map(cast, [
402
- [[ 0 ], [ 0 ]],
403
- [[ 0 ], [ 0 ]],
404
- [[ 1 ], [ 0 ]],
405
- [[ 1 ], [ 1 ]],
406
- [[ 1 ], [ 2 ]],
407
- ]))
396
+ for i in range(len(cases)):
397
+ case = cases[i]
398
+ result, = tinybrain.downsample.downsample_with_max_pooling(case, (2, 1))
399
+ assert np.all(result == answers[i])
400
+
401
+ answers = list(map(cast, [
402
+ [[ 0 ], [ 0 ]],
403
+ [[ 0 ], [ 0 ]],
404
+ [[ 1 ], [ 0 ]],
405
+ [[ 1 ], [ 1 ]],
406
+ [[ 1 ], [ 2 ]],
407
+ ]))
408
408
 
409
- for i in range(len(cases)):
410
- case = cases[i]
411
- result, = tinybrain.downsample.downsample_with_max_pooling(case, (1, 2))
412
- assert np.all(result == answers[i])
409
+ for i in range(len(cases)):
410
+ case = cases[i]
411
+ result, = tinybrain.downsample.downsample_with_max_pooling(case, (1, 2))
412
+ assert np.all(result == answers[i])
413
413
 
414
414
  result, = tinybrain.downsample.downsample_with_max_pooling(image4x4x4, (2, 2, 2))
415
415
  answer = cast([
@@ -449,6 +449,57 @@ def test_downsample_max_pooling():
449
449
 
450
450
  assert np.all(result == answer)
451
451
 
452
+ @pytest.mark.parametrize("dtype", [np.int8, np.float32])
453
+ def test_downsample_min_pooling(dtype):
454
+ cases = [
455
+ np.array([ [ -1, 0 ], [ 0, 0 ] ], dtype=dtype),
456
+ np.array([ [ 0, 0 ], [ 0, 0 ] ], dtype=dtype),
457
+ np.array([ [ 0, 1 ], [ 0, 0 ] ], dtype=dtype),
458
+ np.array([ [ 0, 1 ], [ 1, 0 ] ], dtype=dtype),
459
+ np.array([ [ 0, 1 ], [ 0, 2 ] ], dtype=dtype)
460
+ ]
461
+
462
+ for i in range(len(cases)):
463
+ case = cases[i]
464
+ result, = tinybrain.downsample.downsample_with_min_pooling(case, (1, 1))
465
+ assert np.all(result == cases[i])
466
+
467
+ answers = [ -1, 0, 0, 0, 0 ]
468
+
469
+ for i in range(len(cases)):
470
+ case = cases[i]
471
+ result, = tinybrain.downsample.downsample_with_min_pooling(case, (2, 2))
472
+ assert result == answers[i]
473
+
474
+ cast = lambda arr: np.array(arr, dtype=dtype)
475
+
476
+ answers = list(map(cast, [
477
+ [[ -1, 0 ]],
478
+ [[ 0, 0 ]],
479
+ [[ 0, 0 ]],
480
+ [[ 0, 0 ]],
481
+ [[ 0, 1 ]],
482
+ ]))
483
+
484
+ for i in range(len(cases)):
485
+ case = cases[i]
486
+ result, = tinybrain.downsample.downsample_with_min_pooling(case, (2, 1))
487
+ print(result)
488
+ assert np.all(result == answers[i])
489
+
490
+ answers = list(map(cast, [
491
+ [[ -1 ], [ 0 ]],
492
+ [[ 0 ], [ 0 ]],
493
+ [[ 0 ], [ 0 ]],
494
+ [[ 0 ], [ 0 ]],
495
+ [[ 0 ], [ 0 ]],
496
+ ]))
497
+
498
+ for i in range(len(cases)):
499
+ case = cases[i]
500
+ result, = tinybrain.downsample.downsample_with_min_pooling(case, (1, 2))
501
+ assert np.all(result == answers[i])
502
+
452
503
  def test_countless3d():
453
504
  def test_all_cases(fn):
454
505
  alldifferent = [
@@ -2,5 +2,5 @@
2
2
  requires = [
3
3
  "setuptools",
4
4
  "wheel",
5
- "oldest-supported-numpy",
5
+ "numpy",
6
6
  ]
@@ -34,7 +34,7 @@ if sys.platform == 'darwin':
34
34
  setuptools.setup(
35
35
  setup_requires=['pbr', 'numpy','cython'],
36
36
  install_requires=['numpy'],
37
- python_requires=">=3.7",
37
+ python_requires=">=3.8",
38
38
  ext_modules=[
39
39
  setuptools.Extension(
40
40
  'tinybrain.accelerated',
@@ -154,11 +154,25 @@ def downsample_with_max_pooling(array, factor, num_mips=1):
154
154
  """
155
155
  results = []
156
156
  for mip in range(num_mips):
157
- array = _downsample_with_max_pooling(array, factor)
157
+ array = _downsample_with(array, factor, fn=np.maximum)
158
158
  results.append(array)
159
159
  return results
160
160
 
161
- def _downsample_with_max_pooling(array, factor):
161
+ def downsample_with_min_pooling(array, factor, num_mips=1):
162
+ """
163
+ Downsample by picking the minimum value within a
164
+ cuboid specified by factor. That is, a reduction factor
165
+ of 2x2 works by summarizing many 2x2 cuboids. If factor's
166
+ length is smaller than array.shape, the remaining factors will
167
+ be filled with 1.
168
+ """
169
+ results = []
170
+ for mip in range(num_mips):
171
+ array = _downsample_with(array, factor, fn=np.minimum)
172
+ results.append(array)
173
+ return results
174
+
175
+ def _downsample_with(array, factor, fn):
162
176
  """
163
177
  Downsample by picking the maximum value within a
164
178
  cuboid specified by factor. That is, a reduction factor
@@ -179,7 +193,7 @@ def _downsample_with_max_pooling(array, factor):
179
193
  output = sections[0].copy()
180
194
 
181
195
  for section in sections[1:]:
182
- np.maximum(output, section, output)
196
+ fn(output, section, output)
183
197
 
184
198
  return output
185
199
 
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.1
2
+ Name: tinybrain
3
+ Version: 1.5.1
4
+ Summary: Image pyramid generation specialized for connectomics data types and procedures.
5
+ Home-page: https://github.com/seung-lab/tinybrain/
6
+ Author: William Silversmith
7
+ Author-email: ws9@princeton.edu
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ License-File: AUTHORS
22
+ Requires-Dist: numpy
23
+
24
+ [![Build Status](https://travis-ci.org/seung-lab/tinybrain.svg?branch=master)](https://travis-ci.org/seung-lab/tinybrain) [![PyPI version](https://badge.fury.io/py/tinybrain.svg)](https://badge.fury.io/py/tinybrain)
25
+
26
+ # tinybrain
27
+
28
+ Image pyramid generation specialized for connectomics data types and procedures. If your brain wasn't tiny before, it will be now.
29
+
30
+ ```python
31
+ import tinybrain
32
+
33
+ img = load_3d_em_stack()
34
+
35
+ # 2x2 and 2x2x2 downsamples are on a fast path.
36
+ # e.g. (2,2), (2,2,1), (2,2,1,1), (2,2,2), (2,2,2,1)
37
+ img_pyramid = tinybrain.downsample_with_averaging(img, factor=(2,2,1), num_mips=5, sparse=False)
38
+
39
+ labels = load_3d_labels()
40
+ label_pyramid = tinybrain.downsample_segmentation(labels, factor=(2,2,1), num_mips=5, sparse=False))
41
+ ```
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install numpy
47
+ pip install tinybrain
48
+ ```
49
+
50
+ ## Motivation
51
+
52
+ Image hierarchy generation in connectomics uses a few different techniques for
53
+ visualizing data, but predominantly we create image pyramids of uint8 grayscale images using 2x2 average pooling and of uint8 to uint64 segmentation labels using 2x2 mode pooling. When images become very large and people wish to visualze upper mip levels using three axes at once, it becomes desirable to perform 2x2x2 downsamples to maintain isotropy.
54
+
55
+ It's possible to compute both of these using numpy, however as multiple packages found it useful to copy the downsample functions, it makes sense to formalize these functions into a seperate library located on PyPI.
56
+
57
+ Given the disparate circumstances that they will be used in, these functions should work
58
+ fast as possible with low memory usage and avoid numerical issues such as integer truncation
59
+ while generating multiple mip levels.
60
+
61
+ ## Considerations: downsample_with_averaging
62
+
63
+ It's advisable to generate multiple mip levels at once rather than recursively computing
64
+ new images as for integer type images, this leads to integer truncation issues. In the common
65
+ case of 2x2x1 downsampling, a recursively computed image would lose 0.75 brightness per a
66
+ mip level. Therefore, take advantage of the `num_mips` argument which strikes a balance
67
+ that limits integer truncation loss to once every 4 mip levels. This compromise allows
68
+ for the use of integer arithmatic and no more memory usage than 2x the input image including
69
+ the output downsamples. If you seek to eliminate the loss beyond 4 mip levels, try promoting
70
+ the type before downsampling. 2x2x2x1 downsamples truncate every 8 mip levels.
71
+
72
+ A C++ high performance path is triggered for 2x2x1x1 and 2x2x2x1 downsample factors on uint8, uint16, float32,
73
+ and float64 data types in Fortran order. Other factors, data types, and orderings are computed using a numpy pathway that is much slower and more memory intensive.
74
+
75
+ We also include a sparse mode for downsampling 2x2x2 patches, which prevents "ghosting" where one z-slice overlaps a black region on the next slice and becomes semi-transparent after downsampling. We deal with this by neglecting the background pixels from the averaging operation.
76
+
77
+ ### Example Benchmark
78
+
79
+ On a 1024x1024x100 uint8 image I ran the following code. PIL and OpenCV are actually much faster than this benchmark shows because most of the time is spent writing to the numpy array. tinybrain has a large advantage working on 3D and 4D arrays. Of course, this is a very simple benchmark and it may be possible to tune each of these approaches. On single slices, Pillow was faster than tinybrain.
80
+
81
+ ```python
82
+ img = np.load("image.npy")
83
+
84
+ s = time.time()
85
+ downsample_with_averaging(img, (2,2,1))
86
+ print("Original ", time.time() - s)
87
+
88
+ s = time.time()
89
+ out = tinybrain.downsample_with_averaging(img, (2,2,1))
90
+ print("tinybrain ", time.time() - s)
91
+
92
+ s = time.time()
93
+ out = np.zeros(shape=(512,512,100))
94
+ for z in range(img.shape[2]):
95
+ out[:,:,z] = cv2.resize(img[:,:,z], dsize=(512, 512) )
96
+ print("OpenCV ", time.time() - s)
97
+
98
+ s = time.time()
99
+ out = np.zeros(shape=(512,512,100))
100
+ for z in range(img.shape[2]):
101
+ pilimg = Image.fromarray(img[:,:,z])
102
+ out[:,:,z] = pilimg.resize( (512, 512) )
103
+ print("Pillow ", time.time() - s)
104
+
105
+ # Method Run Time Rel. Perf.
106
+ # Original 1820 ms +/- 3.73 ms 1.0x
107
+ # tinybrain 67 ms +/- 0.40 ms 27.2x
108
+ # OpenCV 469 ms +/- 1.12 ms 3.9x
109
+ # Pillow 937 ms +/- 7.63 ms 1.9x
110
+ ```
111
+
112
+ Here's the output from `perf.py` on an Apple Silicon 2021 Macbook Pro M1.
113
+ Note that the image used was a random 2048x2048x64 array that was a uint8
114
+ for average pooling and a uint64 for mode pooling to represent real use cases more fairly. In the table, read it as 2D or 3D downsamples, generating a single or multiple mip levels, with sparse mode enabled or disabled. The speed values are in megavoxels per a second and are the mean of ten runs.
115
+
116
+
117
+ | dwnsmpl | mips | sparse | AVG (MVx/sec) | MODE (MVx/sec) |
118
+ |----------|---------|-----------|------------------|-------------------|
119
+ | 2x2 | 1 | N | 3856.07 | 1057.87 |
120
+ | 2x2 | 2 | N | 2685.80 | 1062.69 |
121
+ | 2x2 | 1 | Y | N/A | 129.64 |
122
+ | 2x2 | 2 | Y | N/A | 81.62 |
123
+ | 2x2x2 | 1 | N | 4468.55 | 336.85 |
124
+ | 2x2x2 | 2 | N | 2867.80 | 298.45 |
125
+ | 2x2x2 | 1 | Y | 1389.47 | 337.87 |
126
+ | 2x2x2 | 2 | Y | 1259.58 | 293.84 |
127
+
128
+ As the downsampling code's performance is data dependent due to branching, I also used [`connectomics.npy`](https://github.com/seung-lab/connected-components-3d/blob/master/benchmarks/connectomics.npy.gz) (512<sup>3</sup> uint32 extended to uint64) to see how that affected performance. This data comes from mouse visual cortex and has many equal adjacent voxels. In this volume, the 2x2x2 non-sparse mode is much faster as the "instant" majority detection can skip examining half the voxels in many cases.
129
+
130
+ | dwnsmpl | mips | sparse | MODE (MVx/sec) |
131
+ |----------|---------|-----------|-------------------|
132
+ | 2x2 | 1 | N | 1078.09 |
133
+ | 2x2 | 2 | N | 1030.90 |
134
+ | 2x2 | 1 | Y | 146.15 |
135
+ | 2x2 | 2 | Y | 69.25 |
136
+ | 2x2x2 | 1 | N | 1966.74 |
137
+ | 2x2x2 | 2 | N | 1790.60 |
138
+ | 2x2x2 | 1 | Y | 2041.96 |
139
+ | 2x2x2 | 2 | Y | 1758.42 |
140
+
141
+
142
+ ## Considerations: downsample_segmentation
143
+
144
+ The `downsample_segmentation` function performs mode pooling operations provided the downsample factor is a power of two, including in three dimensions. If the factor is a non-power of two, striding is used. The mode pooling, which is usually what you want, is computed recursively. Mode pooling is superior to striding, but the recursive calculation can introduce defects at mip levels higher than 1. This may be improved in the future.
145
+
146
+ The way the calculation is actually done uses an ensemble of several different methods. For (2,2,1,1) and (2,2,2,1) downsamples, a Cython fast, low memory path is selected. (2,2,1,1) implements [*countless if*](https://towardsdatascience.com/countless-high-performance-2x-downsampling-of-labeled-images-using-python-and-numpy-e70ad3275589). (2,2,2,1) uses a combination of counting and "instant" majority detection. For (4,4,1) or other 2D powers of two, the [*countless 2d*](https://towardsdatascience.com/countless-high-performance-2x-downsampling-of-labeled-images-using-python-and-numpy-e70ad3275589) algorithm is used. For (4,4,4), (8,8,8) etc, the [*dynamic countless 3d*](https://towardsdatascience.com/countless-3d-vectorized-2x-downsampling-of-labeled-volume-images-using-python-and-numpy-59d686c2f75) algorithm is used. For 2D powers of two, [*stippled countless 2d*](https://medium.com/@willsilversmith/countless-2d-inflated-2x-downsampling-of-labeled-images-holding-zero-values-as-background-4d13a7675f2d) is used if the sparse flag is enabled. For all other configurations, striding is used.
147
+
148
+ Countless 2d paths are also fast, but use slightly more memory and time. Countless 3D is okay for (2,2,2) and (4,4,4) but will use time and memory exponential in the product of dimensions. This state of affairs could be improved by implementing a counting based algorithm in Cython/C++ for arbitrary factors that doesn't compute recursively. The countless algorithms were developed before I knew how to write Cython and package libraries. However, C++ implementations of countless are much faster than counting for computing the first 2x2x1 mip level. In particular, an AVX2 SIMD implementation can saturate memory bandwidth.
149
+
150
+ Documentation for the countless algorithm family is located here: https://github.com/william-silversmith/countless
151
+
@@ -0,0 +1 @@
1
+ {"git_version": "7978ea7", "is_release": true}
tinybrain-1.4.1/PKG-INFO DELETED
@@ -1,150 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: tinybrain
3
- Version: 1.4.1
4
- Summary: Image pyramid generation specialized for connectomics data types and procedures.
5
- Home-page: https://github.com/seung-lab/tinybrain/
6
- Author: William Silversmith
7
- Author-email: ws9@princeton.edu
8
- License: UNKNOWN
9
- Description: [![Build Status](https://travis-ci.org/seung-lab/tinybrain.svg?branch=master)](https://travis-ci.org/seung-lab/tinybrain) [![PyPI version](https://badge.fury.io/py/tinybrain.svg)](https://badge.fury.io/py/tinybrain)
10
-
11
- # tinybrain
12
-
13
- Image pyramid generation specialized for connectomics data types and procedures. If your brain wasn't tiny before, it will be now.
14
-
15
- ```python
16
- import tinybrain
17
-
18
- img = load_3d_em_stack()
19
-
20
- # 2x2 and 2x2x2 downsamples are on a fast path.
21
- # e.g. (2,2), (2,2,1), (2,2,1,1), (2,2,2), (2,2,2,1)
22
- img_pyramid = tinybrain.downsample_with_averaging(img, factor=(2,2,1), num_mips=5, sparse=False)
23
-
24
- labels = load_3d_labels()
25
- label_pyramid = tinybrain.downsample_segmentation(labels, factor=(2,2,1), num_mips=5, sparse=False))
26
- ```
27
-
28
- ## Installation
29
-
30
- ```bash
31
- pip install numpy
32
- pip install tinybrain
33
- ```
34
-
35
- ## Motivation
36
-
37
- Image hierarchy generation in connectomics uses a few different techniques for
38
- visualizing data, but predominantly we create image pyramids of uint8 grayscale images using 2x2 average pooling and of uint8 to uint64 segmentation labels using 2x2 mode pooling. When images become very large and people wish to visualze upper mip levels using three axes at once, it becomes desirable to perform 2x2x2 downsamples to maintain isotropy.
39
-
40
- It's possible to compute both of these using numpy, however as multiple packages found it useful to copy the downsample functions, it makes sense to formalize these functions into a seperate library located on PyPI.
41
-
42
- Given the disparate circumstances that they will be used in, these functions should work
43
- fast as possible with low memory usage and avoid numerical issues such as integer truncation
44
- while generating multiple mip levels.
45
-
46
- ## Considerations: downsample_with_averaging
47
-
48
- It's advisable to generate multiple mip levels at once rather than recursively computing
49
- new images as for integer type images, this leads to integer truncation issues. In the common
50
- case of 2x2x1 downsampling, a recursively computed image would lose 0.75 brightness per a
51
- mip level. Therefore, take advantage of the `num_mips` argument which strikes a balance
52
- that limits integer truncation loss to once every 4 mip levels. This compromise allows
53
- for the use of integer arithmatic and no more memory usage than 2x the input image including
54
- the output downsamples. If you seek to eliminate the loss beyond 4 mip levels, try promoting
55
- the type before downsampling. 2x2x2x1 downsamples truncate every 8 mip levels.
56
-
57
- A C++ high performance path is triggered for 2x2x1x1 and 2x2x2x1 downsample factors on uint8, uint16, float32,
58
- and float64 data types in Fortran order. Other factors, data types, and orderings are computed using a numpy pathway that is much slower and more memory intensive.
59
-
60
- We also include a sparse mode for downsampling 2x2x2 patches, which prevents "ghosting" where one z-slice overlaps a black region on the next slice and becomes semi-transparent after downsampling. We deal with this by neglecting the background pixels from the averaging operation.
61
-
62
- ### Example Benchmark
63
-
64
- On a 1024x1024x100 uint8 image I ran the following code. PIL and OpenCV are actually much faster than this benchmark shows because most of the time is spent writing to the numpy array. tinybrain has a large advantage working on 3D and 4D arrays. Of course, this is a very simple benchmark and it may be possible to tune each of these approaches. On single slices, Pillow was faster than tinybrain.
65
-
66
- ```python
67
- img = np.load("image.npy")
68
-
69
- s = time.time()
70
- downsample_with_averaging(img, (2,2,1))
71
- print("Original ", time.time() - s)
72
-
73
- s = time.time()
74
- out = tinybrain.downsample_with_averaging(img, (2,2,1))
75
- print("tinybrain ", time.time() - s)
76
-
77
- s = time.time()
78
- out = np.zeros(shape=(512,512,100))
79
- for z in range(img.shape[2]):
80
- out[:,:,z] = cv2.resize(img[:,:,z], dsize=(512, 512) )
81
- print("OpenCV ", time.time() - s)
82
-
83
- s = time.time()
84
- out = np.zeros(shape=(512,512,100))
85
- for z in range(img.shape[2]):
86
- pilimg = Image.fromarray(img[:,:,z])
87
- out[:,:,z] = pilimg.resize( (512, 512) )
88
- print("Pillow ", time.time() - s)
89
-
90
- # Method Run Time Rel. Perf.
91
- # Original 1820 ms +/- 3.73 ms 1.0x
92
- # tinybrain 67 ms +/- 0.40 ms 27.2x
93
- # OpenCV 469 ms +/- 1.12 ms 3.9x
94
- # Pillow 937 ms +/- 7.63 ms 1.9x
95
- ```
96
-
97
- Here's the output from `perf.py` on an Apple Silicon 2021 Macbook Pro M1.
98
- Note that the image used was a random 2048x2048x64 array that was a uint8
99
- for average pooling and a uint64 for mode pooling to represent real use cases more fairly. In the table, read it as 2D or 3D downsamples, generating a single or multiple mip levels, with sparse mode enabled or disabled. The speed values are in megavoxels per a second and are the mean of ten runs.
100
-
101
-
102
- | dwnsmpl | mips | sparse | AVG (MVx/sec) | MODE (MVx/sec) |
103
- |----------|---------|-----------|------------------|-------------------|
104
- | 2x2 | 1 | N | 3856.07 | 1057.87 |
105
- | 2x2 | 2 | N | 2685.80 | 1062.69 |
106
- | 2x2 | 1 | Y | N/A | 129.64 |
107
- | 2x2 | 2 | Y | N/A | 81.62 |
108
- | 2x2x2 | 1 | N | 4468.55 | 336.85 |
109
- | 2x2x2 | 2 | N | 2867.80 | 298.45 |
110
- | 2x2x2 | 1 | Y | 1389.47 | 337.87 |
111
- | 2x2x2 | 2 | Y | 1259.58 | 293.84 |
112
-
113
- As the downsampling code's performance is data dependent due to branching, I also used [`connectomics.npy`](https://github.com/seung-lab/connected-components-3d/blob/master/benchmarks/connectomics.npy.gz) (512<sup>3</sup> uint32 extended to uint64) to see how that affected performance. This data comes from mouse visual cortex and has many equal adjacent voxels. In this volume, the 2x2x2 non-sparse mode is much faster as the "instant" majority detection can skip examining half the voxels in many cases.
114
-
115
- | dwnsmpl | mips | sparse | MODE (MVx/sec) |
116
- |----------|---------|-----------|-------------------|
117
- | 2x2 | 1 | N | 1078.09 |
118
- | 2x2 | 2 | N | 1030.90 |
119
- | 2x2 | 1 | Y | 146.15 |
120
- | 2x2 | 2 | Y | 69.25 |
121
- | 2x2x2 | 1 | N | 1966.74 |
122
- | 2x2x2 | 2 | N | 1790.60 |
123
- | 2x2x2 | 1 | Y | 2041.96 |
124
- | 2x2x2 | 2 | Y | 1758.42 |
125
-
126
-
127
- ## Considerations: downsample_segmentation
128
-
129
- The `downsample_segmentation` function performs mode pooling operations provided the downsample factor is a power of two, including in three dimensions. If the factor is a non-power of two, striding is used. The mode pooling, which is usually what you want, is computed recursively. Mode pooling is superior to striding, but the recursive calculation can introduce defects at mip levels higher than 1. This may be improved in the future.
130
-
131
- The way the calculation is actually done uses an ensemble of several different methods. For (2,2,1,1) and (2,2,2,1) downsamples, a Cython fast, low memory path is selected. (2,2,1,1) implements [*countless if*](https://towardsdatascience.com/countless-high-performance-2x-downsampling-of-labeled-images-using-python-and-numpy-e70ad3275589). (2,2,2,1) uses a combination of counting and "instant" majority detection. For (4,4,1) or other 2D powers of two, the [*countless 2d*](https://towardsdatascience.com/countless-high-performance-2x-downsampling-of-labeled-images-using-python-and-numpy-e70ad3275589) algorithm is used. For (4,4,4), (8,8,8) etc, the [*dynamic countless 3d*](https://towardsdatascience.com/countless-3d-vectorized-2x-downsampling-of-labeled-volume-images-using-python-and-numpy-59d686c2f75) algorithm is used. For 2D powers of two, [*stippled countless 2d*](https://medium.com/@willsilversmith/countless-2d-inflated-2x-downsampling-of-labeled-images-holding-zero-values-as-background-4d13a7675f2d) is used if the sparse flag is enabled. For all other configurations, striding is used.
132
-
133
- Countless 2d paths are also fast, but use slightly more memory and time. Countless 3D is okay for (2,2,2) and (4,4,4) but will use time and memory exponential in the product of dimensions. This state of affairs could be improved by implementing a counting based algorithm in Cython/C++ for arbitrary factors that doesn't compute recursively. The countless algorithms were developed before I knew how to write Cython and package libraries. However, C++ implementations of countless are much faster than counting for computing the first 2x2x1 mip level. In particular, an AVX2 SIMD implementation can saturate memory bandwidth.
134
-
135
- Documentation for the countless algorithm family is located here: https://github.com/william-silversmith/countless
136
-
137
-
138
- Platform: UNKNOWN
139
- Classifier: Intended Audience :: Developers
140
- Classifier: Development Status :: 5 - Production/Stable
141
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
142
- Classifier: Programming Language :: Python :: 3
143
- Classifier: Programming Language :: Python :: 3.8
144
- Classifier: Programming Language :: Python :: 3.9
145
- Classifier: Programming Language :: Python :: 3.10
146
- Classifier: Programming Language :: Python :: 3.11
147
- Classifier: Programming Language :: Python :: 3.12
148
- Classifier: Topic :: Scientific/Engineering
149
- Requires-Python: >=3.7
150
- Description-Content-Type: text/markdown
@@ -1,150 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: tinybrain
3
- Version: 1.4.1
4
- Summary: Image pyramid generation specialized for connectomics data types and procedures.
5
- Home-page: https://github.com/seung-lab/tinybrain/
6
- Author: William Silversmith
7
- Author-email: ws9@princeton.edu
8
- License: UNKNOWN
9
- Description: [![Build Status](https://travis-ci.org/seung-lab/tinybrain.svg?branch=master)](https://travis-ci.org/seung-lab/tinybrain) [![PyPI version](https://badge.fury.io/py/tinybrain.svg)](https://badge.fury.io/py/tinybrain)
10
-
11
- # tinybrain
12
-
13
- Image pyramid generation specialized for connectomics data types and procedures. If your brain wasn't tiny before, it will be now.
14
-
15
- ```python
16
- import tinybrain
17
-
18
- img = load_3d_em_stack()
19
-
20
- # 2x2 and 2x2x2 downsamples are on a fast path.
21
- # e.g. (2,2), (2,2,1), (2,2,1,1), (2,2,2), (2,2,2,1)
22
- img_pyramid = tinybrain.downsample_with_averaging(img, factor=(2,2,1), num_mips=5, sparse=False)
23
-
24
- labels = load_3d_labels()
25
- label_pyramid = tinybrain.downsample_segmentation(labels, factor=(2,2,1), num_mips=5, sparse=False))
26
- ```
27
-
28
- ## Installation
29
-
30
- ```bash
31
- pip install numpy
32
- pip install tinybrain
33
- ```
34
-
35
- ## Motivation
36
-
37
- Image hierarchy generation in connectomics uses a few different techniques for
38
- visualizing data, but predominantly we create image pyramids of uint8 grayscale images using 2x2 average pooling and of uint8 to uint64 segmentation labels using 2x2 mode pooling. When images become very large and people wish to visualze upper mip levels using three axes at once, it becomes desirable to perform 2x2x2 downsamples to maintain isotropy.
39
-
40
- It's possible to compute both of these using numpy, however as multiple packages found it useful to copy the downsample functions, it makes sense to formalize these functions into a seperate library located on PyPI.
41
-
42
- Given the disparate circumstances that they will be used in, these functions should work
43
- fast as possible with low memory usage and avoid numerical issues such as integer truncation
44
- while generating multiple mip levels.
45
-
46
- ## Considerations: downsample_with_averaging
47
-
48
- It's advisable to generate multiple mip levels at once rather than recursively computing
49
- new images as for integer type images, this leads to integer truncation issues. In the common
50
- case of 2x2x1 downsampling, a recursively computed image would lose 0.75 brightness per a
51
- mip level. Therefore, take advantage of the `num_mips` argument which strikes a balance
52
- that limits integer truncation loss to once every 4 mip levels. This compromise allows
53
- for the use of integer arithmatic and no more memory usage than 2x the input image including
54
- the output downsamples. If you seek to eliminate the loss beyond 4 mip levels, try promoting
55
- the type before downsampling. 2x2x2x1 downsamples truncate every 8 mip levels.
56
-
57
- A C++ high performance path is triggered for 2x2x1x1 and 2x2x2x1 downsample factors on uint8, uint16, float32,
58
- and float64 data types in Fortran order. Other factors, data types, and orderings are computed using a numpy pathway that is much slower and more memory intensive.
59
-
60
- We also include a sparse mode for downsampling 2x2x2 patches, which prevents "ghosting" where one z-slice overlaps a black region on the next slice and becomes semi-transparent after downsampling. We deal with this by neglecting the background pixels from the averaging operation.
61
-
62
- ### Example Benchmark
63
-
64
- On a 1024x1024x100 uint8 image I ran the following code. PIL and OpenCV are actually much faster than this benchmark shows because most of the time is spent writing to the numpy array. tinybrain has a large advantage working on 3D and 4D arrays. Of course, this is a very simple benchmark and it may be possible to tune each of these approaches. On single slices, Pillow was faster than tinybrain.
65
-
66
- ```python
67
- img = np.load("image.npy")
68
-
69
- s = time.time()
70
- downsample_with_averaging(img, (2,2,1))
71
- print("Original ", time.time() - s)
72
-
73
- s = time.time()
74
- out = tinybrain.downsample_with_averaging(img, (2,2,1))
75
- print("tinybrain ", time.time() - s)
76
-
77
- s = time.time()
78
- out = np.zeros(shape=(512,512,100))
79
- for z in range(img.shape[2]):
80
- out[:,:,z] = cv2.resize(img[:,:,z], dsize=(512, 512) )
81
- print("OpenCV ", time.time() - s)
82
-
83
- s = time.time()
84
- out = np.zeros(shape=(512,512,100))
85
- for z in range(img.shape[2]):
86
- pilimg = Image.fromarray(img[:,:,z])
87
- out[:,:,z] = pilimg.resize( (512, 512) )
88
- print("Pillow ", time.time() - s)
89
-
90
- # Method Run Time Rel. Perf.
91
- # Original 1820 ms +/- 3.73 ms 1.0x
92
- # tinybrain 67 ms +/- 0.40 ms 27.2x
93
- # OpenCV 469 ms +/- 1.12 ms 3.9x
94
- # Pillow 937 ms +/- 7.63 ms 1.9x
95
- ```
96
-
97
- Here's the output from `perf.py` on an Apple Silicon 2021 Macbook Pro M1.
98
- Note that the image used was a random 2048x2048x64 array that was a uint8
99
- for average pooling and a uint64 for mode pooling to represent real use cases more fairly. In the table, read it as 2D or 3D downsamples, generating a single or multiple mip levels, with sparse mode enabled or disabled. The speed values are in megavoxels per a second and are the mean of ten runs.
100
-
101
-
102
- | dwnsmpl | mips | sparse | AVG (MVx/sec) | MODE (MVx/sec) |
103
- |----------|---------|-----------|------------------|-------------------|
104
- | 2x2 | 1 | N | 3856.07 | 1057.87 |
105
- | 2x2 | 2 | N | 2685.80 | 1062.69 |
106
- | 2x2 | 1 | Y | N/A | 129.64 |
107
- | 2x2 | 2 | Y | N/A | 81.62 |
108
- | 2x2x2 | 1 | N | 4468.55 | 336.85 |
109
- | 2x2x2 | 2 | N | 2867.80 | 298.45 |
110
- | 2x2x2 | 1 | Y | 1389.47 | 337.87 |
111
- | 2x2x2 | 2 | Y | 1259.58 | 293.84 |
112
-
113
- As the downsampling code's performance is data dependent due to branching, I also used [`connectomics.npy`](https://github.com/seung-lab/connected-components-3d/blob/master/benchmarks/connectomics.npy.gz) (512<sup>3</sup> uint32 extended to uint64) to see how that affected performance. This data comes from mouse visual cortex and has many equal adjacent voxels. In this volume, the 2x2x2 non-sparse mode is much faster as the "instant" majority detection can skip examining half the voxels in many cases.
114
-
115
- | dwnsmpl | mips | sparse | MODE (MVx/sec) |
116
- |----------|---------|-----------|-------------------|
117
- | 2x2 | 1 | N | 1078.09 |
118
- | 2x2 | 2 | N | 1030.90 |
119
- | 2x2 | 1 | Y | 146.15 |
120
- | 2x2 | 2 | Y | 69.25 |
121
- | 2x2x2 | 1 | N | 1966.74 |
122
- | 2x2x2 | 2 | N | 1790.60 |
123
- | 2x2x2 | 1 | Y | 2041.96 |
124
- | 2x2x2 | 2 | Y | 1758.42 |
125
-
126
-
127
- ## Considerations: downsample_segmentation
128
-
129
- The `downsample_segmentation` function performs mode pooling operations provided the downsample factor is a power of two, including in three dimensions. If the factor is a non-power of two, striding is used. The mode pooling, which is usually what you want, is computed recursively. Mode pooling is superior to striding, but the recursive calculation can introduce defects at mip levels higher than 1. This may be improved in the future.
130
-
131
- The way the calculation is actually done uses an ensemble of several different methods. For (2,2,1,1) and (2,2,2,1) downsamples, a Cython fast, low memory path is selected. (2,2,1,1) implements [*countless if*](https://towardsdatascience.com/countless-high-performance-2x-downsampling-of-labeled-images-using-python-and-numpy-e70ad3275589). (2,2,2,1) uses a combination of counting and "instant" majority detection. For (4,4,1) or other 2D powers of two, the [*countless 2d*](https://towardsdatascience.com/countless-high-performance-2x-downsampling-of-labeled-images-using-python-and-numpy-e70ad3275589) algorithm is used. For (4,4,4), (8,8,8) etc, the [*dynamic countless 3d*](https://towardsdatascience.com/countless-3d-vectorized-2x-downsampling-of-labeled-volume-images-using-python-and-numpy-59d686c2f75) algorithm is used. For 2D powers of two, [*stippled countless 2d*](https://medium.com/@willsilversmith/countless-2d-inflated-2x-downsampling-of-labeled-images-holding-zero-values-as-background-4d13a7675f2d) is used if the sparse flag is enabled. For all other configurations, striding is used.
132
-
133
- Countless 2d paths are also fast, but use slightly more memory and time. Countless 3D is okay for (2,2,2) and (4,4,4) but will use time and memory exponential in the product of dimensions. This state of affairs could be improved by implementing a counting based algorithm in Cython/C++ for arbitrary factors that doesn't compute recursively. The countless algorithms were developed before I knew how to write Cython and package libraries. However, C++ implementations of countless are much faster than counting for computing the first 2x2x1 mip level. In particular, an AVX2 SIMD implementation can saturate memory bandwidth.
134
-
135
- Documentation for the countless algorithm family is located here: https://github.com/william-silversmith/countless
136
-
137
-
138
- Platform: UNKNOWN
139
- Classifier: Intended Audience :: Developers
140
- Classifier: Development Status :: 5 - Production/Stable
141
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
142
- Classifier: Programming Language :: Python :: 3
143
- Classifier: Programming Language :: Python :: 3.8
144
- Classifier: Programming Language :: Python :: 3.9
145
- Classifier: Programming Language :: Python :: 3.10
146
- Classifier: Programming Language :: Python :: 3.11
147
- Classifier: Programming Language :: Python :: 3.12
148
- Classifier: Topic :: Scientific/Engineering
149
- Requires-Python: >=3.7
150
- Description-Content-Type: text/markdown
@@ -1 +0,0 @@
1
- {"git_version": "e879a1f", "is_release": true}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes