numpyimage 1.1.1__tar.gz → 1.2.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.
- numpyimage-1.2.0/PKG-INFO +52 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/npimage/core.py +48 -3
- {numpyimage-1.1.1 → numpyimage-1.2.0}/npimage/nrrd_utils.py +10 -5
- numpyimage-1.2.0/npimage/operations.py +469 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/npimage/utils.py +6 -6
- numpyimage-1.2.0/numpyimage.egg-info/PKG-INFO +52 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/numpyimage.egg-info/requires.txt +1 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/requirements.txt +1 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/setup.py +1 -2
- numpyimage-1.1.1/PKG-INFO +0 -52
- numpyimage-1.1.1/npimage/operations.py +0 -196
- numpyimage-1.1.1/numpyimage.egg-info/PKG-INFO +0 -52
- {numpyimage-1.1.1 → numpyimage-1.2.0}/LICENSE +0 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/MANIFEST.in +0 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/README.md +0 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/npimage/__init__.py +0 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/npimage/filetypes/__init__.py +0 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/npimage/filetypes/pbm.py +0 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/npimage/graphics.py +0 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/numpyimage.egg-info/SOURCES.txt +0 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/numpyimage.egg-info/dependency_links.txt +0 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/numpyimage.egg-info/top_level.txt +0 -0
- {numpyimage-1.1.1 → numpyimage-1.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: numpyimage
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Load, save, and manipulate image files as numpy arrays
|
|
5
|
+
Home-page: https://github.com/jasper-tms/npimage
|
|
6
|
+
Author: Jasper Phelps
|
|
7
|
+
Author-email: jasper.s.phelps@gmail.com
|
|
8
|
+
License: GNU GPL v3
|
|
9
|
+
Description: # npimage
|
|
10
|
+
Need to load pixel values from image files as numpy arrays, and hate having to remember whether you should use PIL, tifffile, matplotlib, or something else? Hate having to deal with the fact that those libraries all use different function names and syntaxes? Wish you could just provide a filename and get back a numpy array? This library's `core.py` does that, with `array = load(filename)`, `save(array, filename)`, and `show(array)` functions that let you easily handle a number of common image file formats without having to remember library-specific syntax. (Another choice of library to consider for accomplishing similar goals is [imageio](https://pypi.org/project/imageio/), which also supports loading videos through the FFmpeg wrapper library [pyav](https://pypi.org/project/av/).)
|
|
11
|
+
|
|
12
|
+
Want to draw simple shapes like lines, triangles, and circles into 3D numpy arrays? Frustrated that the python libraries you can find online like `opencv` and `skimage.draw` work on 2D arrays but not 3D? I wrote some functions in `graphics.py` that do the trick in 3D. (If you know of another library that can do this, please let me know!)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
- `core.py`: load, save, or show images.
|
|
17
|
+
- `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
|
|
18
|
+
- `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
|
|
19
|
+
- `operations.py`: perform operations on images.
|
|
20
|
+
|
|
21
|
+
For now, check each function's docstring for more. A jupyter notebook demonstrating this package's functions will come later.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Installation
|
|
25
|
+
|
|
26
|
+
As is always the case in python, consider making a virtual environment (using your preference of conda, virtualenv, or virtualenvwrapper) before installing.
|
|
27
|
+
|
|
28
|
+
**Option 1:** `pip install` from PyPI:
|
|
29
|
+
|
|
30
|
+
pip install numpyimage
|
|
31
|
+
|
|
32
|
+
(Unfortunately the name `npimage` was already taken on PyPI, so `pip install npimage` will get you a different package.)
|
|
33
|
+
|
|
34
|
+
**Option 2:** `pip install` directly from GitHub:
|
|
35
|
+
|
|
36
|
+
pip install git+https://github.com/jasper-tms/npimage.git
|
|
37
|
+
|
|
38
|
+
**Option 3:** First `git clone` this repo and then `pip install` it from your clone:
|
|
39
|
+
|
|
40
|
+
cd ~/repos # Or wherever on your computer you want to download this code to
|
|
41
|
+
git clone https://github.com/jasper-tms/npimage.git
|
|
42
|
+
cd npimage
|
|
43
|
+
pip install .
|
|
44
|
+
|
|
45
|
+
**After installing,** you can import this package in python using `import npimage` (not `import numpyimage`!)
|
|
46
|
+
|
|
47
|
+
Platform: UNKNOWN
|
|
48
|
+
Classifier: Programming Language :: Python :: 3
|
|
49
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
50
|
+
Classifier: Operating System :: OS Independent
|
|
51
|
+
Requires-Python: >=3.6
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
@@ -110,7 +110,7 @@ read = load # Function name alias
|
|
|
110
110
|
imread = load # Function name alias
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
def save(data, filename, dim_order='zyx', metadata=None, compress=False):
|
|
113
|
+
def save(data, filename, overwrite=False, dim_order='zyx', metadata=None, compress=False):
|
|
114
114
|
"""
|
|
115
115
|
Save a numpy array to file with a file type specified by the
|
|
116
116
|
filename extension.
|
|
@@ -124,8 +124,10 @@ def save(data, filename, dim_order='zyx', metadata=None, compress=False):
|
|
|
124
124
|
|
|
125
125
|
`compress` only matters when saving in `.nrrd` format
|
|
126
126
|
"""
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
filename = filename.rstrip('/')
|
|
128
|
+
if os.path.exists(filename) and not overwrite:
|
|
129
|
+
raise Exception(f'File {filename} already exists. '
|
|
130
|
+
'Set overwrite=True to overwrite.')
|
|
129
131
|
extension = filename.split('.')[-1]
|
|
130
132
|
assert extension in supported_extensions, f'Filetype {extension} not supported'
|
|
131
133
|
|
|
@@ -176,10 +178,53 @@ def save(data, filename, dim_order='zyx', metadata=None, compress=False):
|
|
|
176
178
|
if extension == 'zarr':
|
|
177
179
|
raise NotImplementedError
|
|
178
180
|
|
|
181
|
+
|
|
179
182
|
write = save # Function name alias
|
|
180
183
|
to_file = save # Function name alias
|
|
181
184
|
|
|
182
185
|
|
|
186
|
+
def save_video(data, filename, overwrite=False, dim_order='yx', time_axis=0,
|
|
187
|
+
framerate=30, crf=23, compression_speed='medium'):
|
|
188
|
+
"""
|
|
189
|
+
Save a 3D numpy array as a video, with a specified axis as the time axis.
|
|
190
|
+
|
|
191
|
+
Follows the PyAV cookbook section on generating video from numpy arrays:
|
|
192
|
+
https://pyav.basswood-io.com/docs/develop/cookbook/numpy.html#generating-video
|
|
193
|
+
"""
|
|
194
|
+
if not data.ndim == 3:
|
|
195
|
+
raise ValueError('Input data must be a 3D numpy array.')
|
|
196
|
+
try:
|
|
197
|
+
import av
|
|
198
|
+
except ImportError:
|
|
199
|
+
raise ImportError('To save videos, you must have PyAV installed. '
|
|
200
|
+
'You can install it with "pip install av".')
|
|
201
|
+
data = np.moveaxis(data, time_axis, 0)
|
|
202
|
+
if 'xy' in dim_order:
|
|
203
|
+
data = data.swapaxes(1, 2)
|
|
204
|
+
n_frames = data.shape[0]
|
|
205
|
+
|
|
206
|
+
if os.path.exists(filename) and not overwrite:
|
|
207
|
+
raise Exception(f'File {filename} already exists. '
|
|
208
|
+
'Set overwrite=True to overwrite.')
|
|
209
|
+
container = av.open(filename, mode='w')
|
|
210
|
+
|
|
211
|
+
stream = container.add_stream('libx264', rate=framerate)
|
|
212
|
+
stream.pix_fmt = 'yuv420p'
|
|
213
|
+
stream.options = {'crf': str(crf), 'preset': compression_speed}
|
|
214
|
+
stream.height = data.shape[1]
|
|
215
|
+
stream.width = data.shape[2]
|
|
216
|
+
|
|
217
|
+
for frame_i in range(n_frames):
|
|
218
|
+
frame = av.VideoFrame.from_ndarray(data[frame_i], format='gray')
|
|
219
|
+
for packet in stream.encode(frame):
|
|
220
|
+
container.mux(packet)
|
|
221
|
+
|
|
222
|
+
for packet in stream.encode():
|
|
223
|
+
container.mux(packet)
|
|
224
|
+
|
|
225
|
+
container.close()
|
|
226
|
+
|
|
227
|
+
|
|
183
228
|
def show(data, dim_order='yx', mode='PIL', **kwargs):
|
|
184
229
|
"""
|
|
185
230
|
Display a numpy array of pixel values as an image. Supported types:
|
|
@@ -38,14 +38,19 @@ def compress(*fn_patterns, keep_uncompressed=False):
|
|
|
38
38
|
nrrd.write(fn, data, header=header)
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def read_headers(*fn_patterns):
|
|
41
|
+
def read_headers(*fn_patterns, verbose=True):
|
|
42
|
+
headers = dict()
|
|
42
43
|
if len(fn_patterns) == 0:
|
|
43
44
|
fn_patterns = ['*.nrrd']
|
|
44
|
-
|
|
45
|
+
if verbose:
|
|
46
|
+
print('Reading headers of all nrrd files in this directory.')
|
|
45
47
|
for fn_pattern in fn_patterns:
|
|
46
|
-
for fn in glob.glob(fn_pattern):
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
for fn in sorted(glob.glob(fn_pattern)):
|
|
49
|
+
if verbose:
|
|
50
|
+
print(f'Reading header of {fn}')
|
|
51
|
+
headers[fn] = nrrd.read_header(fn)
|
|
52
|
+
|
|
53
|
+
return headers
|
|
49
54
|
|
|
50
55
|
|
|
51
56
|
if __name__ == '__main__':
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Literal
|
|
5
|
+
import gc
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
#import cv2
|
|
9
|
+
|
|
10
|
+
from .utils import iround, eq, isint
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def to_8bit(image: np.ndarray,
|
|
14
|
+
bottom_percentile=0.05,
|
|
15
|
+
top_percentile=99.95,
|
|
16
|
+
bottom_value=None,
|
|
17
|
+
top_value=None) -> np.ndarray:
|
|
18
|
+
"""
|
|
19
|
+
Convert an image to 8-bit (uint8) by scaling the image's pixel values so
|
|
20
|
+
that values <= bottom_value are mapped to 0 and values >= top_value are
|
|
21
|
+
mapped to 255. Values within the range (bottom_value, top_value) will be
|
|
22
|
+
mapped linearly into the range [1, 254]. By default, bottom_value and
|
|
23
|
+
top_value are set to percentiles of the source image's pixel values.
|
|
24
|
+
Some examples:
|
|
25
|
+
- Set bottom_percentile=0 to map only the minimum value in the source image
|
|
26
|
+
to 0 and and top_percentile=100 to map only the maximum value in the
|
|
27
|
+
source image to 255, and all other values to the range [1, 254].
|
|
28
|
+
- Set bottom_percentile=25 to map all of the the lowest 25% of the source
|
|
29
|
+
image's pixel values to 0 and set top_percentile=75 to map all of the
|
|
30
|
+
highest 25% of the source image's pixel values to 255.
|
|
31
|
+
The defaults are:
|
|
32
|
+
- bottom_percentile=0.05
|
|
33
|
+
- top_percentile=99.95
|
|
34
|
+
which are ~3.3 standard deviations below and above the mean, respectively,
|
|
35
|
+
if the pixel values are approximately normally distributed. (Compared to
|
|
36
|
+
using percentiles of 0 and 100, using these percentiles lessens the impact
|
|
37
|
+
of a few extreme outlier pixel values in the image.)
|
|
38
|
+
|
|
39
|
+
You may however specify bottom_value and/or top_value explicitly yourself,
|
|
40
|
+
in which case bottom_percentile and/or top_percentile will be ignored.
|
|
41
|
+
|
|
42
|
+
Algorithm:
|
|
43
|
+
- Clip values < bottom_value or > top_value
|
|
44
|
+
(i.e. replace them with bottom_value or top_value)
|
|
45
|
+
- Map the range [bottom_value, top_value] to
|
|
46
|
+
[just less than 1, just greater than 255]
|
|
47
|
+
- Cast to int (which rounds down)
|
|
48
|
+
From these steps, values will be transformed as follows:
|
|
49
|
+
- bottom_value or less -> just less than 1 -> 0
|
|
50
|
+
- a value just greater than bottom_value -> just greater than 1 -> 1
|
|
51
|
+
- top_value or larger -> just greater than 255 -> 255
|
|
52
|
+
- a value just less than top_value -> just less than 255 -> 254
|
|
53
|
+
- values between bottom_value and top_value -> linearly mapped to
|
|
54
|
+
values between 1 and 254
|
|
55
|
+
"""
|
|
56
|
+
assert isinstance(image, np.ndarray)
|
|
57
|
+
|
|
58
|
+
if bottom_value is None or top_value is None:
|
|
59
|
+
percentiles = np.percentile(image, [bottom_percentile, top_percentile])
|
|
60
|
+
if bottom_value is None:
|
|
61
|
+
bottom_value = percentiles[0]
|
|
62
|
+
if top_value is None:
|
|
63
|
+
top_value = percentiles[1]
|
|
64
|
+
|
|
65
|
+
if bottom_value == top_value:
|
|
66
|
+
raise ZeroDivisionError('top_value and bottom_value are the same: '
|
|
67
|
+
'{}'.format(top_value))
|
|
68
|
+
|
|
69
|
+
image = np.clip(image, bottom_value, top_value)
|
|
70
|
+
|
|
71
|
+
bottom_target = 1 - 1e-12
|
|
72
|
+
top_target = 255 + 1e-12
|
|
73
|
+
# TODO some more clever implementation that doesn't require casting to
|
|
74
|
+
# float64, which can take up a lot of memory.
|
|
75
|
+
return ((image.astype('float64') - bottom_value)
|
|
76
|
+
/ (top_value - bottom_value)
|
|
77
|
+
* (top_target - bottom_target)
|
|
78
|
+
+ bottom_target).astype('uint8')
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def downsample(image: np.ndarray,
|
|
82
|
+
factor: [int, Iterable] = 2,
|
|
83
|
+
keep_input_dtype=True) -> np.ndarray:
|
|
84
|
+
"""
|
|
85
|
+
Downsample an image by a given factor along each axis.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
image : np.ndarray
|
|
90
|
+
The image to downsample.
|
|
91
|
+
|
|
92
|
+
factor : int or Iterable
|
|
93
|
+
An iterable with length matching the number of axes in the image,
|
|
94
|
+
specifying a downsampling factor along each axis.
|
|
95
|
+
If factor is provided as an int, that int will be used for each axis.
|
|
96
|
+
If the image is rgb or rgba (that is, the final axis has length 3 or
|
|
97
|
+
4), it is not necessary to specify a factor for that axis and so the
|
|
98
|
+
'factor' iterable can be one element shorter than the number of axes in
|
|
99
|
+
the image.
|
|
100
|
+
|
|
101
|
+
keep_input_dtype : bool
|
|
102
|
+
If True, the output image will have the same dtype as the input image.
|
|
103
|
+
If False, the output image will have dtype float64 to keep full precision.
|
|
104
|
+
"""
|
|
105
|
+
if isinstance(factor, int):
|
|
106
|
+
if image.shape[-1] in [3, 4]:
|
|
107
|
+
# If RGB/RGBA image, don't downsample the colors axis
|
|
108
|
+
factor = (factor,) * (len(image.shape) - 1)
|
|
109
|
+
else:
|
|
110
|
+
factor = (factor,) * len(image.shape)
|
|
111
|
+
if len(factor) == len(image.shape) - 1 and image.shape[-1] in [3, 4]:
|
|
112
|
+
print('RGB/RGBA image detected - not downsampling last axis.')
|
|
113
|
+
factor = (*factor, 1)
|
|
114
|
+
if any([f > l > 1 for f, l in zip(factor, image.shape)]):
|
|
115
|
+
raise ValueError('Downsampling factor must be <= image size along each axis')
|
|
116
|
+
|
|
117
|
+
# In the code below, 'l' is used for the elements of image.shape and 'f' is
|
|
118
|
+
# used for the elements of factor.
|
|
119
|
+
padding = [(0, 0) if l % f == 0
|
|
120
|
+
else (0, f - l % f)
|
|
121
|
+
for l, f in zip(image.shape, factor)]
|
|
122
|
+
# Pad the image so that its axes are a multiple of the downsampling factor
|
|
123
|
+
# This is necessary because the image will be split into blocks of size
|
|
124
|
+
# 'factor' and then the mean of each block will be taken, so the image
|
|
125
|
+
# needs to be a multiple of 'factor' in size so that the blocks are all
|
|
126
|
+
# the same size.
|
|
127
|
+
image = np.pad(image, padding, mode='edge')
|
|
128
|
+
if not all([(l == 1) or (l % f == 0) for l, f in zip(image.shape, factor)]):
|
|
129
|
+
raise RuntimeError('Padding failed: shape should be a multiple of factor')
|
|
130
|
+
|
|
131
|
+
# For any dimension with length > 1, split it into blocks of size f, then
|
|
132
|
+
# average over each block.
|
|
133
|
+
temp_shape = []
|
|
134
|
+
for l, f in zip(image.shape, factor):
|
|
135
|
+
if l == 1:
|
|
136
|
+
if f != 1:
|
|
137
|
+
raise ValueError("You can't downsample along"
|
|
138
|
+
" an axis of length 1.")
|
|
139
|
+
temp_shape.extend([1, 1])
|
|
140
|
+
else:
|
|
141
|
+
temp_shape.extend([l // f, f])
|
|
142
|
+
axes_to_avg = tuple(range(1, len(temp_shape), 2))
|
|
143
|
+
image_downsampled = image.reshape(temp_shape).mean(axis=axes_to_avg)
|
|
144
|
+
|
|
145
|
+
if keep_input_dtype:
|
|
146
|
+
if np.issubdtype(image.dtype, np.integer):
|
|
147
|
+
image_downsampled = iround(image_downsampled, output_dtype=image.dtype)
|
|
148
|
+
else:
|
|
149
|
+
image_downsampled = image_downsampled.astype(image.dtype)
|
|
150
|
+
|
|
151
|
+
return image_downsampled
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def offset(image: np.ndarray,
|
|
155
|
+
distance: float or Iterable[float],
|
|
156
|
+
axis: int = None,
|
|
157
|
+
expand_bounds: bool = False,
|
|
158
|
+
edge_mode: Literal['extend', 'wrap',
|
|
159
|
+
'reflect', 'constant'] = 'extend',
|
|
160
|
+
edge_fill_value=0,
|
|
161
|
+
fill_transparent=False) -> np.ndarray:
|
|
162
|
+
"""
|
|
163
|
+
Offset an image by a given distance.
|
|
164
|
+
|
|
165
|
+
'distance' must be an iterable with length matching the number of axes in
|
|
166
|
+
the image, to specify an number of pixels to offset along each axis. If the
|
|
167
|
+
image is rgb or rgba (that is, the final axis has length 3 or 4), it is not
|
|
168
|
+
necessary to specify an offset for that axis and so the 'distance' iterable
|
|
169
|
+
can be one element shorter than the number of axes in the image.
|
|
170
|
+
|
|
171
|
+
If edge_mode is set to 'constant', the pixels no longer occupied by the
|
|
172
|
+
original image as a result of the offset will be filled in with
|
|
173
|
+
'edge_fill_value'.
|
|
174
|
+
|
|
175
|
+
See also scipy.ndimage.shift, which performs a very similar operation
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
iter(distance)
|
|
179
|
+
if axis is not None:
|
|
180
|
+
raise ValueError('Either give distance as one number and specify'
|
|
181
|
+
' axis, or give distance as an iterable. You'
|
|
182
|
+
' did a mix of those two.')
|
|
183
|
+
except TypeError:
|
|
184
|
+
distance_iter = [0] * len(image.shape)
|
|
185
|
+
if axis is None:
|
|
186
|
+
raise ValueError('Must specify axis when giving distance as a'
|
|
187
|
+
' single number.')
|
|
188
|
+
distance_iter[axis] = distance
|
|
189
|
+
distance = distance_iter
|
|
190
|
+
if edge_mode not in ['extend', 'wrap', 'reflect', 'constant']:
|
|
191
|
+
raise ValueError("edge_mode must be one of 'extend', 'wrap', 'reflect', or 'constant'")
|
|
192
|
+
if edge_mode in ['wrap', 'reflect']:
|
|
193
|
+
raise NotImplementedError("edge_mode '{}' not yet implemented".format(edge_mode))
|
|
194
|
+
|
|
195
|
+
if len(image.shape) == len(distance) + 1 and image.shape[-1] in [1, 3, 4]:
|
|
196
|
+
# Specify no offset along the channels axis, if not specified by user
|
|
197
|
+
distance = (*distance, 0)
|
|
198
|
+
|
|
199
|
+
if len(image.shape) != len(distance):
|
|
200
|
+
m = (f'distance must have length {len(image.shape)} to specify an'
|
|
201
|
+
' offset along each axis of the image, but instead had length'
|
|
202
|
+
f' {len(distance)}')
|
|
203
|
+
raise ValueError(m)
|
|
204
|
+
|
|
205
|
+
if not expand_bounds:
|
|
206
|
+
new_image = np.full_like(image, edge_fill_value)
|
|
207
|
+
else:
|
|
208
|
+
new_shape = np.array(image.shape) + np.array([int(max(0, d)) for d in distance])
|
|
209
|
+
new_image = np.full(new_shape, edge_fill_value, dtype=image.dtype)
|
|
210
|
+
|
|
211
|
+
if image.shape[-1] == 4 and not fill_transparent:
|
|
212
|
+
# If rgba, set alpha channel value to max
|
|
213
|
+
# The line below means new_image[:, :, :, ..., :, -1] = 255
|
|
214
|
+
new_image[tuple([slice(None, None)] * (len(image.shape)-1) + [-1])] = 255
|
|
215
|
+
|
|
216
|
+
distance_int = [int(x) for x in distance]
|
|
217
|
+
|
|
218
|
+
source_range = [slice(max(0, -d), min(s, s-d)) for d, s in zip(distance_int, new_image.shape)]
|
|
219
|
+
target_range = [slice(max(0, d), min(s, s+d)) for d, s in zip(distance_int, new_image.shape)]
|
|
220
|
+
|
|
221
|
+
new_image[tuple(target_range)] = image[tuple(source_range)]
|
|
222
|
+
|
|
223
|
+
for i, d in enumerate(distance):
|
|
224
|
+
if not eq(d, int(d)):
|
|
225
|
+
_offset_subpixel(new_image, d - int(d), i,
|
|
226
|
+
edge_fill_value=edge_fill_value,
|
|
227
|
+
fill_transparent=fill_transparent, inplace=True)
|
|
228
|
+
|
|
229
|
+
return new_image
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _offset_subpixel(image: np.ndarray,
|
|
233
|
+
distance: float,
|
|
234
|
+
axis: int,
|
|
235
|
+
edge_mode: Literal['extend', 'wrap',
|
|
236
|
+
'reflect', 'constant'] = 'extend',
|
|
237
|
+
edge_fill_value=0,
|
|
238
|
+
fill_transparent=False,
|
|
239
|
+
inplace=False):
|
|
240
|
+
"""
|
|
241
|
+
Offset an image by a fraction of a pixel along a single specified axis.
|
|
242
|
+
|
|
243
|
+
If an offset of 0.1 is requested, the output will be 10% of the image
|
|
244
|
+
shifted one pixel upward plus 90% of the original image.
|
|
245
|
+
If an offset of -0.1 is requested, the output will be 10% of the image
|
|
246
|
+
shifted one pixel downward plus 90% of the original image.
|
|
247
|
+
If an offset of 0.5 is requested, the output will be 50% of the image
|
|
248
|
+
shifted one pixel upward plus 50% of the original image.
|
|
249
|
+
etc.
|
|
250
|
+
|
|
251
|
+
The pixels no longer occupied by the original image as a result of the
|
|
252
|
+
offset will be filled in with 'edge_fill_value'.
|
|
253
|
+
"""
|
|
254
|
+
assert -1 < distance and distance < 1
|
|
255
|
+
|
|
256
|
+
one_pix_offset = [0] * len(image.shape)
|
|
257
|
+
one_pix_offset[axis] = 1 if distance >= 0 else -1
|
|
258
|
+
image_1pix_shifted = offset(image, one_pix_offset,
|
|
259
|
+
edge_fill_value=edge_fill_value,
|
|
260
|
+
fill_transparent=fill_transparent)
|
|
261
|
+
distance = abs(distance)
|
|
262
|
+
|
|
263
|
+
if np.issubdtype(image.dtype, np.integer):
|
|
264
|
+
# If the input array is an integer type, we should avoid creating a
|
|
265
|
+
# float version of the array during calculations, because float arrays
|
|
266
|
+
# can take up an unacceptable amount of memory. (e.g. If I write lazy
|
|
267
|
+
# code that ends up multiplying a uint8 array by a fractional value
|
|
268
|
+
# like 0.5, a float64 array is created which takes up 8x the amount of
|
|
269
|
+
# memory as the source array. And we need to make two of these!).
|
|
270
|
+
# Instead, we will do a trick of increasing the bit-depth of the source
|
|
271
|
+
# array by 1 byte (or a few bytes, since numpy only works with bit
|
|
272
|
+
# depths that are a power of 2, e.g. uint24 isn't a thing), use that
|
|
273
|
+
# additional range to keep some accuracy during the weighted average
|
|
274
|
+
# calculation, then cast back to the original dtype.
|
|
275
|
+
|
|
276
|
+
upcast_dtype = np.dtype(f'{image.dtype.kind}{image.dtype.itemsize * 2}')
|
|
277
|
+
image_upcast = image.astype(upcast_dtype)
|
|
278
|
+
image_1pix_shifted_upcast = image_1pix_shifted.astype(upcast_dtype)
|
|
279
|
+
|
|
280
|
+
# We'll use the extra bit of precision to enable us to use integer
|
|
281
|
+
# weights from 0 to 255 instead of float weights from 0.0 to 1.0
|
|
282
|
+
image_weight = int(256 * (1 - distance))
|
|
283
|
+
image_1pix_shifted_weight = int(256 * distance)
|
|
284
|
+
# Adding 127 before dividing by 256 means we round to the nearest
|
|
285
|
+
# integer instead of truncating (which we'd get without the 127)
|
|
286
|
+
image_subpix_shifted = (
|
|
287
|
+
(image_upcast * image_weight) +
|
|
288
|
+
(image_1pix_shifted_upcast * image_1pix_shifted_weight) +
|
|
289
|
+
127
|
|
290
|
+
) // 256
|
|
291
|
+
del image_upcast, image_1pix_shifted_upcast, image_1pix_shifted
|
|
292
|
+
gc.collect()
|
|
293
|
+
|
|
294
|
+
# Now cast back to the original dtype
|
|
295
|
+
image_subpix_shifted = image_subpix_shifted.astype(image.dtype)
|
|
296
|
+
|
|
297
|
+
elif np.issubdtype(image.dtype, np.floating):
|
|
298
|
+
image_subpix_shifted = image * (1 - distance) + image_1pix_shifted * distance
|
|
299
|
+
|
|
300
|
+
if inplace:
|
|
301
|
+
image[:] = image_subpix_shifted
|
|
302
|
+
else:
|
|
303
|
+
return image_subpix_shifted
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def paste(image: np.ndarray,
|
|
307
|
+
target: np.ndarray,
|
|
308
|
+
offset: Iterable[float]):
|
|
309
|
+
"""
|
|
310
|
+
Paste an image onto another image at a given offset.
|
|
311
|
+
|
|
312
|
+
`target` is modified in place. Regions of `image` that would be
|
|
313
|
+
pasted outside the bounds of `target` are ignored.
|
|
314
|
+
"""
|
|
315
|
+
image = image.copy()
|
|
316
|
+
offset_int = [int(x) for x in offset]
|
|
317
|
+
offset_subpixel = [x - int(x) for x in offset]
|
|
318
|
+
for i, offset in enumerate(offset_subpixel):
|
|
319
|
+
if not eq(offset, 0):
|
|
320
|
+
_offset_subpixel(image, offset, i, inplace=True)
|
|
321
|
+
|
|
322
|
+
source_range = [slice(max(0, -o), min(s, t-o))
|
|
323
|
+
for o, s, t in zip(offset_int, image.shape, target.shape)]
|
|
324
|
+
target_range = [slice(max(0, o), min(t, max(0, o) + min(s, t-o) - max(0, -o)))
|
|
325
|
+
for o, s, t in zip(offset_int, image.shape, target.shape)]
|
|
326
|
+
|
|
327
|
+
target[tuple(target_range)] = image[tuple(source_range)]
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def overlay(ims: list[np.ndarray],
|
|
331
|
+
offsets: list[tuple[float]],
|
|
332
|
+
later_images_on_top=True,
|
|
333
|
+
expand_bounds=False,
|
|
334
|
+
fill_value=0):
|
|
335
|
+
"""
|
|
336
|
+
Overlay multiple images / image volumes onto each other, with each
|
|
337
|
+
image offset by a given amount.
|
|
338
|
+
|
|
339
|
+
Parameters
|
|
340
|
+
----------
|
|
341
|
+
ims : list of np.ndarray
|
|
342
|
+
The images or image volumes to overlay.
|
|
343
|
+
|
|
344
|
+
offsets : list of tuple of float
|
|
345
|
+
The offsets to apply to each image volume. Each tuple should
|
|
346
|
+
have the same length as the number of dimensions in the
|
|
347
|
+
corresponding images.
|
|
348
|
+
|
|
349
|
+
later_images_on_top : bool
|
|
350
|
+
If True, the later images in the list will be drawn on top of
|
|
351
|
+
the earlier images. If False, the earlier images will be drawn
|
|
352
|
+
on top of the later images.
|
|
353
|
+
|
|
354
|
+
expand_bounds : bool
|
|
355
|
+
If True, the output image will be large enough to contain all
|
|
356
|
+
of the input images. If False, the output image will be the same
|
|
357
|
+
size as the first input image.
|
|
358
|
+
"""
|
|
359
|
+
if not len(ims) == len(offsets):
|
|
360
|
+
raise ValueError('The number of images must match the number of offsets.')
|
|
361
|
+
ndims = ims[0].ndim
|
|
362
|
+
if not all([im.ndim == ndims for im in ims[1:]]):
|
|
363
|
+
raise ValueError('All images must have the same number of dimensions.')
|
|
364
|
+
if not all([len(offset) == ndims for offset in offsets]):
|
|
365
|
+
raise ValueError('All offsets must have the same length as the number'
|
|
366
|
+
' of dimensions in the images.')
|
|
367
|
+
|
|
368
|
+
if expand_bounds:
|
|
369
|
+
origin = [min([0] + [offset[axis] for offset in offsets])
|
|
370
|
+
for axis in range(ndims)]
|
|
371
|
+
max_coord = [max([im.shape[axis] + offset[axis]
|
|
372
|
+
for im, offset in zip(ims, offsets)])
|
|
373
|
+
for axis in range(ndims)]
|
|
374
|
+
canvas_shape = [max_coord[axis] - origin[axis] for axis in range(ndims)]
|
|
375
|
+
offsets = [tuple(offset[axis] - origin[axis] for axis in range(ndims))
|
|
376
|
+
for offset in offsets]
|
|
377
|
+
canvas = np.full(canvas_shape, fill_value, dtype=ims[0].dtype)
|
|
378
|
+
else:
|
|
379
|
+
canvas = np.full_like(ims[0], fill_value)
|
|
380
|
+
|
|
381
|
+
if not later_images_on_top:
|
|
382
|
+
ims = reversed(ims)
|
|
383
|
+
offsets = reversed(offsets)
|
|
384
|
+
|
|
385
|
+
for im, offset in zip(ims, offsets):
|
|
386
|
+
paste(im, canvas, offset)
|
|
387
|
+
|
|
388
|
+
return canvas
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def overlay_two_images(im1: np.ndarray,
|
|
392
|
+
im2: np.ndarray,
|
|
393
|
+
offset: Iterable[float] = 0,
|
|
394
|
+
later_images_on_top=True,
|
|
395
|
+
expand_bounds=False,
|
|
396
|
+
fill_value=0):
|
|
397
|
+
"""
|
|
398
|
+
Overlay two images, with the second image offset by the given amount.
|
|
399
|
+
"""
|
|
400
|
+
if isint(offset):
|
|
401
|
+
offset = [offset] * im1.ndim
|
|
402
|
+
if expand_bounds:
|
|
403
|
+
offsets = ([abs(max(0, -o)) for o in offset], [max(0, o) for o in offset])
|
|
404
|
+
else:
|
|
405
|
+
offsets = ([0] * len(offset), offset)
|
|
406
|
+
return overlay([im1, im2],
|
|
407
|
+
offsets,
|
|
408
|
+
later_images_on_top=later_images_on_top,
|
|
409
|
+
expand_bounds=expand_bounds,
|
|
410
|
+
fill_value=fill_value)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def remove_bleedthrough(im, contaminated_slice, source_slice,
|
|
414
|
+
leave_saturated_pixels_alone=True):
|
|
415
|
+
"""
|
|
416
|
+
Given two channels of multi-channel image, determine how strong the
|
|
417
|
+
bleedthrough was from the source channel to the contaminated channel
|
|
418
|
+
and remove the contamination.
|
|
419
|
+
Saturated pixels are by default not changed, which is reasonable
|
|
420
|
+
when bleedthrough is weak. If bleedthrough is strong, you may want
|
|
421
|
+
to try leave_saturated_pixels_alone=False, though this may
|
|
422
|
+
adjust those pixels more than desired.
|
|
423
|
+
"""
|
|
424
|
+
#TODO implement ICA and use it to separate the two independent sources
|
|
425
|
+
raise NotImplementedError
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def find_landmark(image: np.ndarray,
|
|
429
|
+
landmark: np.ndarray,
|
|
430
|
+
search_bbox: type = None,
|
|
431
|
+
rotation_angles: Iterable = [0]):
|
|
432
|
+
"""
|
|
433
|
+
Given a region in a source image, find the region in the target
|
|
434
|
+
image that most resembles the source feature.
|
|
435
|
+
Specify search_bbox to restrict the search to a particular region
|
|
436
|
+
within the target image. Otherwise the whole image is searched.
|
|
437
|
+
By default rotation is not allowed, but specific rotation angles can
|
|
438
|
+
be searched by passing an iterable specifying rotations (in degrees)
|
|
439
|
+
to search, e.g.:
|
|
440
|
+
rotation_angles=[-90, 0, 90]
|
|
441
|
+
rotation_angles=np.arange(-10, 10.5, 0.5)
|
|
442
|
+
rotation_angles=np.linspace(-10, 10, 5)
|
|
443
|
+
|
|
444
|
+
Implementation leverages cv2.matchTemplate, following tutorial at
|
|
445
|
+
https://docs.opencv.org/4.5.2/d4/dc6/tutorial_py_template_matching.html
|
|
446
|
+
"""
|
|
447
|
+
#TODO implement
|
|
448
|
+
raise NotImplementedError
|
|
449
|
+
|
|
450
|
+
if search_bbox is not None:
|
|
451
|
+
if isinstance(search_bbox[0], slice):
|
|
452
|
+
img_to_search = image[search_bbox[0], search_bbox[1]]
|
|
453
|
+
elif isinstance(search_bbox[0], [list, tuple]):
|
|
454
|
+
img_to_search = image[search_bbox[0][0]:search_bbox[0][1],
|
|
455
|
+
search_bbox[1][0]:search_bbox[1][1]]
|
|
456
|
+
else:
|
|
457
|
+
img_to_search = image
|
|
458
|
+
|
|
459
|
+
scores = cv2.matchTemplate(img_to_search, landmark, cv2.TM_CCOEFF_NORMED)
|
|
460
|
+
# TODO TODO TODO implement subpixel max_loc finding. Perhaps fitting a gaussian peak?
|
|
461
|
+
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(scores)
|
|
462
|
+
top_left = max_loc # This is x,y ordered,
|
|
463
|
+
top_left = top_left[::-1] # so flip it to y,x
|
|
464
|
+
|
|
465
|
+
def align(im1, im2, channel=0):
|
|
466
|
+
"""
|
|
467
|
+
todo
|
|
468
|
+
"""
|
|
469
|
+
pass
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def iround(n):
|
|
6
|
+
def iround(n, output_dtype=int):
|
|
7
7
|
# This rounds to the nearest integer, rounding values ending in .5 up
|
|
8
8
|
# (toward positive infinity)
|
|
9
|
-
return np.floor(n + 0.5).astype(
|
|
9
|
+
return np.floor(n + 0.5).astype(output_dtype)
|
|
10
10
|
|
|
11
11
|
# "For values exactly halfway between rounded decimal values, NumPy.round
|
|
12
12
|
# rounds to the nearest even value. Thus 1.5 and 2.5 round to 2.0, -0.5 and
|
|
@@ -17,12 +17,12 @@ def iround(n):
|
|
|
17
17
|
#return np.round(n).astype(int)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def ifloor(n):
|
|
21
|
-
return np.floor(n).astype(
|
|
20
|
+
def ifloor(n, output_dtype=int):
|
|
21
|
+
return np.floor(n).astype(output_dtype)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def iceil(n):
|
|
25
|
-
return np.ceil(n).astype(
|
|
24
|
+
def iceil(n, output_dtype=int):
|
|
25
|
+
return np.ceil(n).astype(output_dtype)
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def eq(a, b, tolerance=1e-8):
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: numpyimage
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Load, save, and manipulate image files as numpy arrays
|
|
5
|
+
Home-page: https://github.com/jasper-tms/npimage
|
|
6
|
+
Author: Jasper Phelps
|
|
7
|
+
Author-email: jasper.s.phelps@gmail.com
|
|
8
|
+
License: GNU GPL v3
|
|
9
|
+
Description: # npimage
|
|
10
|
+
Need to load pixel values from image files as numpy arrays, and hate having to remember whether you should use PIL, tifffile, matplotlib, or something else? Hate having to deal with the fact that those libraries all use different function names and syntaxes? Wish you could just provide a filename and get back a numpy array? This library's `core.py` does that, with `array = load(filename)`, `save(array, filename)`, and `show(array)` functions that let you easily handle a number of common image file formats without having to remember library-specific syntax. (Another choice of library to consider for accomplishing similar goals is [imageio](https://pypi.org/project/imageio/), which also supports loading videos through the FFmpeg wrapper library [pyav](https://pypi.org/project/av/).)
|
|
11
|
+
|
|
12
|
+
Want to draw simple shapes like lines, triangles, and circles into 3D numpy arrays? Frustrated that the python libraries you can find online like `opencv` and `skimage.draw` work on 2D arrays but not 3D? I wrote some functions in `graphics.py` that do the trick in 3D. (If you know of another library that can do this, please let me know!)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
- `core.py`: load, save, or show images.
|
|
17
|
+
- `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
|
|
18
|
+
- `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
|
|
19
|
+
- `operations.py`: perform operations on images.
|
|
20
|
+
|
|
21
|
+
For now, check each function's docstring for more. A jupyter notebook demonstrating this package's functions will come later.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Installation
|
|
25
|
+
|
|
26
|
+
As is always the case in python, consider making a virtual environment (using your preference of conda, virtualenv, or virtualenvwrapper) before installing.
|
|
27
|
+
|
|
28
|
+
**Option 1:** `pip install` from PyPI:
|
|
29
|
+
|
|
30
|
+
pip install numpyimage
|
|
31
|
+
|
|
32
|
+
(Unfortunately the name `npimage` was already taken on PyPI, so `pip install npimage` will get you a different package.)
|
|
33
|
+
|
|
34
|
+
**Option 2:** `pip install` directly from GitHub:
|
|
35
|
+
|
|
36
|
+
pip install git+https://github.com/jasper-tms/npimage.git
|
|
37
|
+
|
|
38
|
+
**Option 3:** First `git clone` this repo and then `pip install` it from your clone:
|
|
39
|
+
|
|
40
|
+
cd ~/repos # Or wherever on your computer you want to download this code to
|
|
41
|
+
git clone https://github.com/jasper-tms/npimage.git
|
|
42
|
+
cd npimage
|
|
43
|
+
pip install .
|
|
44
|
+
|
|
45
|
+
**After installing,** you can import this package in python using `import npimage` (not `import numpyimage`!)
|
|
46
|
+
|
|
47
|
+
Platform: UNKNOWN
|
|
48
|
+
Classifier: Programming Language :: Python :: 3
|
|
49
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
50
|
+
Classifier: Operating System :: OS Independent
|
|
51
|
+
Requires-Python: >=3.6
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
@@ -9,7 +9,7 @@ with open("requirements.txt", "r") as f:
|
|
|
9
9
|
|
|
10
10
|
setuptools.setup(
|
|
11
11
|
name="numpyimage",
|
|
12
|
-
version="1.
|
|
12
|
+
version="1.2.0",
|
|
13
13
|
author="Jasper Phelps",
|
|
14
14
|
author_email="jasper.s.phelps@gmail.com",
|
|
15
15
|
description="Load, save, and manipulate image files as numpy arrays",
|
|
@@ -18,7 +18,6 @@ setuptools.setup(
|
|
|
18
18
|
url="https://github.com/jasper-tms/npimage",
|
|
19
19
|
license='GNU GPL v3',
|
|
20
20
|
packages=setuptools.find_packages(),
|
|
21
|
-
#package_data={'npimage': ['requirements.txt']},
|
|
22
21
|
classifiers=[
|
|
23
22
|
"Programming Language :: Python :: 3",
|
|
24
23
|
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
|
numpyimage-1.1.1/PKG-INFO
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: numpyimage
|
|
3
|
-
Version: 1.1.1
|
|
4
|
-
Summary: Load, save, and manipulate image files as numpy arrays
|
|
5
|
-
Home-page: https://github.com/jasper-tms/npimage
|
|
6
|
-
Author: Jasper Phelps
|
|
7
|
-
Author-email: jasper.s.phelps@gmail.com
|
|
8
|
-
License: GNU GPL v3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.6
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
|
-
|
|
16
|
-
# npimage
|
|
17
|
-
Need to load pixel values from image files as numpy arrays, and hate having to remember whether you should use PIL, tifffile, matplotlib, or something else? Hate having to deal with the fact that those libraries all use different function names and syntaxes? Wish you could just provide a filename and get back a numpy array? This library's `core.py` does that, with `array = load(filename)`, `save(array, filename)`, and `show(array)` functions that let you easily handle a number of common image file formats without having to remember library-specific syntax. (Another choice of library to consider for accomplishing similar goals is [imageio](https://pypi.org/project/imageio/), which also supports loading videos through the FFmpeg wrapper library [pyav](https://pypi.org/project/av/).)
|
|
18
|
-
|
|
19
|
-
Want to draw simple shapes like lines, triangles, and circles into 3D numpy arrays? Frustrated that the python libraries you can find online like `opencv` and `skimage.draw` work on 2D arrays but not 3D? I wrote some functions in `graphics.py` that do the trick in 3D. (If you know of another library that can do this, please let me know!)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
### Documentation
|
|
23
|
-
- `core.py`: load, save, or show images.
|
|
24
|
-
- `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
|
|
25
|
-
- `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
|
|
26
|
-
- `operations.py`: perform operations on images.
|
|
27
|
-
|
|
28
|
-
For now, check each function's docstring for more. A jupyter notebook demonstrating this package's functions will come later.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
### Installation
|
|
32
|
-
|
|
33
|
-
As is always the case in python, consider making a virtual environment (using your preference of conda, virtualenv, or virtualenvwrapper) before installing.
|
|
34
|
-
|
|
35
|
-
**Option 1:** `pip install` from PyPI:
|
|
36
|
-
|
|
37
|
-
pip install numpyimage
|
|
38
|
-
|
|
39
|
-
(Unfortunately the name `npimage` was already taken on PyPI, so `pip install npimage` will get you a different package.)
|
|
40
|
-
|
|
41
|
-
**Option 2:** `pip install` directly from GitHub:
|
|
42
|
-
|
|
43
|
-
pip install git+https://github.com/jasper-tms/npimage.git
|
|
44
|
-
|
|
45
|
-
**Option 3:** First `git clone` this repo and then `pip install` it from your clone:
|
|
46
|
-
|
|
47
|
-
cd ~/repos # Or wherever on your computer you want to download this code to
|
|
48
|
-
git clone https://github.com/jasper-tms/npimage.git
|
|
49
|
-
cd npimage
|
|
50
|
-
pip install .
|
|
51
|
-
|
|
52
|
-
**After installing,** you can import this package in python using `import npimage` (not `import numpyimage`!)
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
from collections.abc import Iterable
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
|
|
7
|
-
from .utils import iround, eq, isint
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def to_8bit(image: np.ndarray,
|
|
11
|
-
bottom_percentile=0.4,
|
|
12
|
-
top_percentile=99.6,
|
|
13
|
-
bottom_value=None,
|
|
14
|
-
top_value=None) -> np.ndarray:
|
|
15
|
-
"""
|
|
16
|
-
Convert an image to 8-bit (uint8) by scaling the image's pixel values so
|
|
17
|
-
that values <= bottom_value are mapped to 0 and values >= top_value are
|
|
18
|
-
mapped to 255. Values within the range (bottom_value, top_value) will be
|
|
19
|
-
mapped linearly into the range [1, 254]. By default, bottom_value and
|
|
20
|
-
top_value are set to percentiles of the source image's pixel values.
|
|
21
|
-
Some examples:
|
|
22
|
-
- Set bottom_percentile=0 to map only the minimum value in the source image
|
|
23
|
-
to 0 and and top_percentile=100 to map only the maximum value in the
|
|
24
|
-
source image to 255, and all other values to the range [1, 254].
|
|
25
|
-
- Set bottom_percentile=25 to map all of the the lowest 25% of the source
|
|
26
|
-
image's pixel values to 0 and set top_percentile=75 to map all of the
|
|
27
|
-
highest 25% of the source image's pixel values to 255.
|
|
28
|
-
The defaults are:
|
|
29
|
-
- bottom_percentile=0.4 (~= 100*1/256)
|
|
30
|
-
- top_percentile=99.6 (~= 100*255/256)
|
|
31
|
-
to map the bottom 1/256th of the pixel values to 0 and the top 1/256th of
|
|
32
|
-
the pixel values to 255. (Compared to using percentiles of 0 and 100, this
|
|
33
|
-
approach lessens the impact of a few extreme outlier pixel values in the
|
|
34
|
-
image.)
|
|
35
|
-
|
|
36
|
-
You may however specify bottom_value and/or top_value explicitly yourself,
|
|
37
|
-
in which case bottom_percentile and/or top_percentile will be ignored.
|
|
38
|
-
|
|
39
|
-
Algorithm:
|
|
40
|
-
- Clip values < bottom_value or > top_value
|
|
41
|
-
(i.e. replace them with bottom_value or top_value)
|
|
42
|
-
- Map the range [bottom_value, top_value] to
|
|
43
|
-
[just less than 1, just greater than 255]
|
|
44
|
-
- Cast to int (which rounds down)
|
|
45
|
-
From these two steps, values will be transformed as follows:
|
|
46
|
-
- bottom_value or less -> just less than 1 -> 0
|
|
47
|
-
- a value just greater than bottom_value -> just greater than 1 -> 1
|
|
48
|
-
- top_value or larger -> just greater than 255 -> 255
|
|
49
|
-
- a value just less than top_value -> just less than 255 -> 254
|
|
50
|
-
- values between bottom_value and top_value -> linearly mapped to
|
|
51
|
-
values between 1 and 254
|
|
52
|
-
"""
|
|
53
|
-
assert isinstance(image, np.ndarray)
|
|
54
|
-
|
|
55
|
-
if bottom_value is None or top_value is None:
|
|
56
|
-
percentiles = np.percentile(image, [bottom_percentile, top_percentile])
|
|
57
|
-
if bottom_value is None:
|
|
58
|
-
bottom_value = percentiles[0]
|
|
59
|
-
if top_value is None:
|
|
60
|
-
top_value = percentiles[1]
|
|
61
|
-
|
|
62
|
-
if bottom_value == top_value:
|
|
63
|
-
raise ZeroDivisionError('top_value and bottom_value are the same: '
|
|
64
|
-
'{}'.format(top_value))
|
|
65
|
-
|
|
66
|
-
image = np.clip(image, bottom_value, top_value)
|
|
67
|
-
|
|
68
|
-
bottom_target = 1 - 1e-12
|
|
69
|
-
top_target = 255 + 1e-12
|
|
70
|
-
return ((image.astype('float64') - bottom_value)
|
|
71
|
-
/ (top_value - bottom_value)
|
|
72
|
-
* (top_target - bottom_target)
|
|
73
|
-
+ bottom_target).astype('uint8')
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def offset(image: np.ndarray,
|
|
77
|
-
distance: Iterable,
|
|
78
|
-
fill_value=0,
|
|
79
|
-
fill_transparent=False) -> np.ndarray:
|
|
80
|
-
"""
|
|
81
|
-
Offset an image by a given distance.
|
|
82
|
-
|
|
83
|
-
'distance' must be an iterable with length matching the number of axes in
|
|
84
|
-
the image, to specify an number of pixels to offset along each axis. If the
|
|
85
|
-
image is rgb or rgba (that is, the final axis has length 3 or 4), it is not
|
|
86
|
-
necessary to specify an offset for that axis and so the 'distance' iterable
|
|
87
|
-
can be one element shorter than the number of axes in the image.
|
|
88
|
-
|
|
89
|
-
The pixels no longer occupied by the original image as a result of the
|
|
90
|
-
offset will be filled in with 'fill_value'.
|
|
91
|
-
|
|
92
|
-
See also scipy.ndimage.shift, which performs a very similar operation
|
|
93
|
-
"""
|
|
94
|
-
if len(image.shape) == len(distance) + 1 and image.shape[-1] in [3, 4]:
|
|
95
|
-
# Specify no offset along the channels axis, if not specified by user
|
|
96
|
-
distance = (*distance, 0)
|
|
97
|
-
|
|
98
|
-
if len(image.shape) != len(distance):
|
|
99
|
-
m = (f'distance must have length {len(image.shape)} to specify an'
|
|
100
|
-
' offset along each axis of the image, but instead had length'
|
|
101
|
-
f' {len(distance)}')
|
|
102
|
-
raise ValueError(m)
|
|
103
|
-
|
|
104
|
-
new_image = np.full_like(image, fill_value) # Blank image filled with fill_value
|
|
105
|
-
if image.shape[-1] == 4 and not fill_transparent:
|
|
106
|
-
# If rgba, set alpha channel value to max
|
|
107
|
-
# The line below means new_image[:, :, :, ..., :, -1] = 255
|
|
108
|
-
new_image[tuple([slice(None, None)] * (len(image.shape)-1) + [-1])] = 255
|
|
109
|
-
|
|
110
|
-
distance_int = [int(x) for x in distance]
|
|
111
|
-
|
|
112
|
-
source_range = [slice(max(0, -d), min(s, s-d)) for d, s in zip(distance_int, image.shape)]
|
|
113
|
-
target_range = [slice(max(0, d), min(s, s+d)) for d, s in zip(distance_int, image.shape)]
|
|
114
|
-
|
|
115
|
-
new_image[tuple(target_range)] = image[tuple(source_range)]
|
|
116
|
-
|
|
117
|
-
for i, d in enumerate(distance):
|
|
118
|
-
if not eq(d, int(d)):
|
|
119
|
-
_offset_subpixel(new_image, d - int(d), i, fill_value=fill_value,
|
|
120
|
-
fill_transparent=fill_transparent, inplace=True)
|
|
121
|
-
|
|
122
|
-
return new_image
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _offset_subpixel(image: np.ndarray,
|
|
126
|
-
distance: float,
|
|
127
|
-
axis: int,
|
|
128
|
-
fill_value=0,
|
|
129
|
-
fill_transparent=False,
|
|
130
|
-
inplace=False):
|
|
131
|
-
"""
|
|
132
|
-
Offset an image by a fraction of a pixel along a single specified axis.
|
|
133
|
-
|
|
134
|
-
If an offset of 0.1 is requested, the output will be 10% of the image
|
|
135
|
-
shifted one pixel upward plus 90% of the original image.
|
|
136
|
-
If an offset of -0.1 is requested, the output will be 10% of the image
|
|
137
|
-
shifted one pixel downward plus 90% of the original image.
|
|
138
|
-
If an offset of 0.5 is requested, the output will be 50% of the image
|
|
139
|
-
shifted one pixel upward plus 50% of the original image.
|
|
140
|
-
etc.
|
|
141
|
-
|
|
142
|
-
The pixels no longer occupied by the original image as a result of the
|
|
143
|
-
offset will be filled in with 'fill_value'.
|
|
144
|
-
"""
|
|
145
|
-
assert -1 < distance and distance < 1
|
|
146
|
-
|
|
147
|
-
one_pix_offset = [0] * len(image.shape)
|
|
148
|
-
one_pix_offset[axis] = 1 if distance >= 0 else -1
|
|
149
|
-
image_1pix_shifted = offset(image, one_pix_offset, fill_value=fill_value,
|
|
150
|
-
fill_transparent=fill_transparent)
|
|
151
|
-
distance = abs(distance)
|
|
152
|
-
|
|
153
|
-
image_subpix_shifted = image * (1 - distance) + image_1pix_shifted * distance
|
|
154
|
-
if np.issubdtype(image.dtype, np.integer):
|
|
155
|
-
image_subpix_shifted = iround(image_subpix_shifted)
|
|
156
|
-
|
|
157
|
-
if inplace:
|
|
158
|
-
image[:] = image_subpix_shifted
|
|
159
|
-
else:
|
|
160
|
-
return image_subpix_shifted
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def remove_bleedthrough(im, contaminated_slice, source_slice,
|
|
164
|
-
leave_saturated_pixels_alone=True):
|
|
165
|
-
"""
|
|
166
|
-
Given two channels of multi-channel image, determine how strong the
|
|
167
|
-
bleedthrough was from the source channel to the contaminated channel
|
|
168
|
-
and remove the contamination.
|
|
169
|
-
Saturated pixels are by default not changed, which is reasonable
|
|
170
|
-
when bleedthrough is weak. If bleedthrough is strong, you may want
|
|
171
|
-
to try leave_saturated_pixels_alone=False, though this may
|
|
172
|
-
adjust those pixels more than desired.
|
|
173
|
-
"""
|
|
174
|
-
#TODO implement ICA and use it to separate the two independent sources
|
|
175
|
-
raise NotImplementedError
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def find_landmark(im1, landmark_bbox,
|
|
179
|
-
im2, search_bbox=None,
|
|
180
|
-
rotation_angles=[0]):
|
|
181
|
-
"""
|
|
182
|
-
Given a region in a source image, find the region in the target
|
|
183
|
-
image that most resembles the source feature.
|
|
184
|
-
Specify search_bbox to restrict the search to a particular region
|
|
185
|
-
within the target image. Otherwise the whole image is searched.
|
|
186
|
-
By default rotation is not allowed, but specific rotation angles can
|
|
187
|
-
be searched by passing an iterable specifying rotations (in degrees)
|
|
188
|
-
to search, e.g.:
|
|
189
|
-
rotation_range=[-90, 0, 90]
|
|
190
|
-
rotation_range=np.arange(-10, 10.5, 0.5)
|
|
191
|
-
rotation_range=np.linspace(-10, 10, 5)
|
|
192
|
-
|
|
193
|
-
See also cv2.matchTemplate, e.g. https://docs.opencv.org/4.5.2/d4/dc6/tutorial_py_template_matching.html
|
|
194
|
-
"""
|
|
195
|
-
#TODO implement
|
|
196
|
-
raise NotImplementedError
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: numpyimage
|
|
3
|
-
Version: 1.1.1
|
|
4
|
-
Summary: Load, save, and manipulate image files as numpy arrays
|
|
5
|
-
Home-page: https://github.com/jasper-tms/npimage
|
|
6
|
-
Author: Jasper Phelps
|
|
7
|
-
Author-email: jasper.s.phelps@gmail.com
|
|
8
|
-
License: GNU GPL v3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.6
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
|
-
|
|
16
|
-
# npimage
|
|
17
|
-
Need to load pixel values from image files as numpy arrays, and hate having to remember whether you should use PIL, tifffile, matplotlib, or something else? Hate having to deal with the fact that those libraries all use different function names and syntaxes? Wish you could just provide a filename and get back a numpy array? This library's `core.py` does that, with `array = load(filename)`, `save(array, filename)`, and `show(array)` functions that let you easily handle a number of common image file formats without having to remember library-specific syntax. (Another choice of library to consider for accomplishing similar goals is [imageio](https://pypi.org/project/imageio/), which also supports loading videos through the FFmpeg wrapper library [pyav](https://pypi.org/project/av/).)
|
|
18
|
-
|
|
19
|
-
Want to draw simple shapes like lines, triangles, and circles into 3D numpy arrays? Frustrated that the python libraries you can find online like `opencv` and `skimage.draw` work on 2D arrays but not 3D? I wrote some functions in `graphics.py` that do the trick in 3D. (If you know of another library that can do this, please let me know!)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
### Documentation
|
|
23
|
-
- `core.py`: load, save, or show images.
|
|
24
|
-
- `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
|
|
25
|
-
- `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
|
|
26
|
-
- `operations.py`: perform operations on images.
|
|
27
|
-
|
|
28
|
-
For now, check each function's docstring for more. A jupyter notebook demonstrating this package's functions will come later.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
### Installation
|
|
32
|
-
|
|
33
|
-
As is always the case in python, consider making a virtual environment (using your preference of conda, virtualenv, or virtualenvwrapper) before installing.
|
|
34
|
-
|
|
35
|
-
**Option 1:** `pip install` from PyPI:
|
|
36
|
-
|
|
37
|
-
pip install numpyimage
|
|
38
|
-
|
|
39
|
-
(Unfortunately the name `npimage` was already taken on PyPI, so `pip install npimage` will get you a different package.)
|
|
40
|
-
|
|
41
|
-
**Option 2:** `pip install` directly from GitHub:
|
|
42
|
-
|
|
43
|
-
pip install git+https://github.com/jasper-tms/npimage.git
|
|
44
|
-
|
|
45
|
-
**Option 3:** First `git clone` this repo and then `pip install` it from your clone:
|
|
46
|
-
|
|
47
|
-
cd ~/repos # Or wherever on your computer you want to download this code to
|
|
48
|
-
git clone https://github.com/jasper-tms/npimage.git
|
|
49
|
-
cd npimage
|
|
50
|
-
pip install .
|
|
51
|
-
|
|
52
|
-
**After installing,** you can import this package in python using `import npimage` (not `import numpyimage`!)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|