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.
@@ -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
- while filename.endswith('/'):
128
- filename = filename[:-1]
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
- print('Reading headers of all nrrd files in this directory.\n')
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
- print(fn)
48
- print(nrrd.read_header(fn), '\n')
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(int)
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(int)
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(int)
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
@@ -3,3 +3,4 @@ pillow
3
3
  tifffile
4
4
  pynrrd
5
5
  matplotlib
6
+ opencv-python
@@ -3,3 +3,4 @@ pillow
3
3
  tifffile
4
4
  pynrrd
5
5
  matplotlib
6
+ opencv-python
@@ -9,7 +9,7 @@ with open("requirements.txt", "r") as f:
9
9
 
10
10
  setuptools.setup(
11
11
  name="numpyimage",
12
- version="1.1.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