numpyimage 1.2.0__tar.gz → 1.3.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.
Files changed (25) hide show
  1. numpyimage-1.3.0/PKG-INFO +58 -0
  2. numpyimage-1.3.0/npimage/__init__.py +4 -0
  3. numpyimage-1.3.0/npimage/align.py +84 -0
  4. {numpyimage-1.2.0 → numpyimage-1.3.0}/npimage/core.py +150 -24
  5. {numpyimage-1.2.0 → numpyimage-1.3.0}/npimage/filetypes/pbm.py +0 -1
  6. {numpyimage-1.2.0 → numpyimage-1.3.0}/npimage/graphics.py +72 -74
  7. {numpyimage-1.2.0 → numpyimage-1.3.0}/npimage/nrrd_utils.py +7 -2
  8. {numpyimage-1.2.0 → numpyimage-1.3.0}/npimage/operations.py +167 -101
  9. {numpyimage-1.2.0 → numpyimage-1.3.0}/npimage/utils.py +32 -0
  10. numpyimage-1.3.0/numpyimage.egg-info/PKG-INFO +58 -0
  11. {numpyimage-1.2.0 → numpyimage-1.3.0}/numpyimage.egg-info/SOURCES.txt +3 -1
  12. {numpyimage-1.2.0 → numpyimage-1.3.0}/numpyimage.egg-info/requires.txt +1 -1
  13. {numpyimage-1.2.0 → numpyimage-1.3.0}/requirements.txt +1 -1
  14. {numpyimage-1.2.0 → numpyimage-1.3.0}/setup.py +1 -1
  15. numpyimage-1.3.0/tests/tests.py +39 -0
  16. numpyimage-1.2.0/PKG-INFO +0 -52
  17. numpyimage-1.2.0/npimage/__init__.py +0 -1
  18. numpyimage-1.2.0/numpyimage.egg-info/PKG-INFO +0 -52
  19. {numpyimage-1.2.0 → numpyimage-1.3.0}/LICENSE +0 -0
  20. {numpyimage-1.2.0 → numpyimage-1.3.0}/MANIFEST.in +0 -0
  21. {numpyimage-1.2.0 → numpyimage-1.3.0}/README.md +0 -0
  22. {numpyimage-1.2.0 → numpyimage-1.3.0}/npimage/filetypes/__init__.py +0 -0
  23. {numpyimage-1.2.0 → numpyimage-1.3.0}/numpyimage.egg-info/dependency_links.txt +0 -0
  24. {numpyimage-1.2.0 → numpyimage-1.3.0}/numpyimage.egg-info/top_level.txt +0 -0
  25. {numpyimage-1.2.0 → numpyimage-1.3.0}/setup.cfg +0 -0
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.1
2
+ Name: numpyimage
3
+ Version: 1.3.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
+ 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
+ Requires-Dist: numpy
16
+ Requires-Dist: pillow
17
+ Requires-Dist: tifffile
18
+ Requires-Dist: pynrrd
19
+ Requires-Dist: matplotlib
20
+ Requires-Dist: opencv-python-headless
21
+
22
+ # npimage
23
+ 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/).)
24
+
25
+ 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!)
26
+
27
+
28
+ ### Documentation
29
+ - `core.py`: load, save, or show images.
30
+ - `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
31
+ - `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
32
+ - `operations.py`: perform operations on images.
33
+
34
+ For now, check each function's docstring for more. A jupyter notebook demonstrating this package's functions will come later.
35
+
36
+
37
+ ### Installation
38
+
39
+ As is always the case in python, consider making a virtual environment (using your preference of conda, virtualenv, or virtualenvwrapper) before installing.
40
+
41
+ **Option 1:** `pip install` from PyPI:
42
+
43
+ pip install numpyimage
44
+
45
+ (Unfortunately the name `npimage` was already taken on PyPI, so `pip install npimage` will get you a different package.)
46
+
47
+ **Option 2:** `pip install` directly from GitHub:
48
+
49
+ pip install git+https://github.com/jasper-tms/npimage.git
50
+
51
+ **Option 3:** First `git clone` this repo and then `pip install` it from your clone:
52
+
53
+ cd ~/repos # Or wherever on your computer you want to download this code to
54
+ git clone https://github.com/jasper-tms/npimage.git
55
+ cd npimage
56
+ pip install .
57
+
58
+ **After installing,** you can import this package in python using `import npimage` (not `import numpyimage`!)
@@ -0,0 +1,4 @@
1
+ from .core import *
2
+ from .operations import *
3
+ from .align import *
4
+ from .graphics import *
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Functions for aligning images.
4
+ """
5
+
6
+ import numpy as np
7
+ import cv2 as cv
8
+
9
+
10
+ def find_landmark(image: np.ndarray,
11
+ landmark: np.ndarray,
12
+ search_bbox=None,
13
+ metric=cv.TM_CCOEFF_NORMED):
14
+ """
15
+ Find the region in an image that most resembles a particular landmark.
16
+
17
+ Implementation leverages OpenCV functions, following tutorial at
18
+ https://docs.opencv.org/4.x/d4/dc6/tutorial_py_template_matching.html
19
+
20
+ Parameters
21
+ ----------
22
+ image : np.ndarray
23
+ The image in which to search for the landmark.
24
+
25
+ landmark : np.ndarray
26
+ The landmark to search for in the image.
27
+
28
+ search_bbox : list/tuple, optional
29
+ The bounding box in which to search for the landmark. If you know
30
+ the landmark you're searching for is in a subset of the image then
31
+ specifying a search region will save time.
32
+ If None, the entire image will be searched. If not None, search_bbox
33
+ should be a list/tuple with two elements. Each element should be either
34
+ a slice object or a list/tuple of two integers that specify the min and
35
+ max values of the range.
36
+ For example, search_bbox=(slice(0, 100), slice(0, 200)) or
37
+ search_box=([0, 100], [0, 200]) would search the top-left
38
+ 100x200 region of the image.
39
+
40
+ metric : cv.TemplateMatchModes, optional
41
+ The metric to use to compare the landmark to the image. Options are
42
+ cv.TM_SQDIFF, cv.TM_SQDIFF_NORMED, cv.TM_CCORR, cv.TM_CCORR_NORMED,
43
+ cv.TM_CCOEFF, cv.TM_CCOEFF_NORMED. Default is cv.TM_CCOEFF_NORMED.
44
+
45
+ Returns
46
+ -------
47
+ (top_left, match_score) : tuple
48
+ top_left : tuple
49
+ The top-left corner of the bounding box in the image that
50
+ is most similar to the landmark.
51
+ score : float
52
+ The score of the match, from 0 to 1. A score of 1 indicates
53
+ a perfect match.
54
+ """
55
+ if search_bbox is not None:
56
+ if all(isinstance(el, slice) for el in search_bbox):
57
+ img_to_search = image[search_bbox[0], search_bbox[1]]
58
+ offset = (search_bbox[0].start, search_bbox[1].start)
59
+ elif all(isinstance(el, (list, tuple)) and len(el) == 2
60
+ for el in search_bbox):
61
+ img_to_search = image[search_bbox[0][0]:search_bbox[0][1],
62
+ search_bbox[1][0]:search_bbox[1][1]]
63
+ offset = (search_bbox[0][0], search_bbox[1][0])
64
+ else:
65
+ raise ValueError(f"Invalid search_bbox: {search_bbox}. Must "
66
+ "be two slices or two (min, max) pairs.")
67
+ else:
68
+ img_to_search = image
69
+ offset = (0, 0)
70
+
71
+ # Perform template matching
72
+ scores = cv.matchTemplate(img_to_search, landmark, metric)
73
+
74
+ # TODO implement subpixel max_loc finding.
75
+ # Perhaps fitting a gaussian peak?
76
+ min_val, max_val, min_loc, max_loc = cv.minMaxLoc(scores)
77
+ if metric in [cv.TM_SQDIFF, cv.TM_SQDIFF_NORMED]:
78
+ top_left = min_loc[::-1] # Includes a flip from (x, y) to (y, x)
79
+ match_score = 1 - min_val
80
+ else:
81
+ top_left = max_loc[::-1] # Includes a flip from (x, y) to (y, x)
82
+ match_score = max_val
83
+ top_left = (top_left[0] + offset[0], top_left[1] + offset[1])
84
+ return top_left, match_score
@@ -1,15 +1,28 @@
1
1
  #!/usr/bin/env python3
2
+ """
3
+ Functions for reading, writing, and showing images.
4
+
5
+ Function list:
6
+ - load(filename) -> numpy.ndarray
7
+ - save(data, filename) -> Saves a numpy array as an nD image file
8
+ - save_video(data, filename) -> Saves a 3D numpy array as a video
9
+ - show(np_array) -> Displays a numpy array of pixel values as an image
10
+ """
2
11
 
3
12
  import os
4
13
  import glob
14
+ from builtins import open as builtin_open
5
15
 
6
16
  import numpy as np
7
17
 
18
+ from . import utils
19
+
8
20
  supported_extensions = [
9
21
  'tif', 'tiff', 'jpg', 'jpeg', 'png', 'pbm',
10
- 'nrrd', 'zarr', 'raw', 'vol'
22
+ 'nrrd', 'zarr', 'raw', 'vol', 'ng'
11
23
  ]
12
24
 
25
+
13
26
  def load(filename, dim_order='zyx', **kwargs):
14
27
  """
15
28
  Open a 2D or 3D image file and return its pixel values as a numpy array.
@@ -21,11 +34,19 @@ def load(filename, dim_order='zyx', **kwargs):
21
34
  xyz for 3D image volumes, xy for 1-channel 2D images, or
22
35
  xyc for multi-channel 2D images.
23
36
  """
24
-
37
+ filename = str(filename)
25
38
  while filename.endswith('/'):
26
39
  filename = filename[:-1]
27
- extension = filename.split('.')[-1]
28
- assert extension in supported_extensions, f'Filetype {extension} not supported'
40
+ if 'format' in kwargs:
41
+ extension = kwargs['format'].lower()
42
+ elif '.' in filename:
43
+ extension = filename.split('.')[-1].lower()
44
+ else:
45
+ raise ValueError('Could not determine file format from filename'
46
+ f' "{filename}". Please specify the file type via'
47
+ ' the `format` argument, e.g. format="tif"')
48
+ if extension not in supported_extensions:
49
+ raise ValueError(f'File format "{extension}" not supported/recognized.')
29
50
 
30
51
  data = None
31
52
 
@@ -39,21 +60,22 @@ def load(filename, dim_order='zyx', **kwargs):
39
60
 
40
61
  if extension == 'pbm':
41
62
  from .filetypes import pbm
42
- data = pbm.load(filename) #pbm.load returns zyx
63
+ data = pbm.load(filename) # pbm.load returns zyx
43
64
 
44
65
  if extension == 'nrrd':
45
66
  import nrrd # pip install pynrrd
46
67
  # Specify C-style ordering to get zyx ordering from nrrd.read
47
- # https://pynrrd.readthedocs.io/en/latest/user-guide.html#index-ordering
68
+ # https://pynrrd.readthedocs.io/en/stable/background/index-ordering.html
48
69
  data, metadata = nrrd.read(filename, index_order='C')
70
+ # Metadata is in Fortran order, so flip it to C order
71
+ utils.transpose_metadata(metadata, inplace=True)
49
72
 
50
73
  if extension in ['raw', 'vol']:
51
74
  dtype = kwargs.get('dtype', np.uint8)
52
- with open(filename, 'rb') as f:
53
- if shape in kwargs:
54
- im = np.fromfile(f, dtype=dtype).reshape(*shape)
55
- else:
56
- im = np.fromfile(f, dtype=dtype)
75
+ with builtin_open(filename, 'rb') as f:
76
+ data = np.fromfile(f, dtype=dtype)
77
+ if 'shape' in kwargs:
78
+ data = data.reshape(*kwargs['shape'])
57
79
 
58
80
  if extension == 'zarr':
59
81
  if 'dataset' not in kwargs:
@@ -94,6 +116,9 @@ def load(filename, dim_order='zyx', **kwargs):
94
116
  else:
95
117
  data = data.T
96
118
 
119
+ if extension == 'nrrd': # TODO check if other formats need this
120
+ utils.transpose_metadata(metadata, inplace=True)
121
+
97
122
  if any([kwargs.get(key, False) for key in
98
123
  ['metadata', 'get_metadata', 'return_metadata']]):
99
124
  try:
@@ -110,7 +135,14 @@ read = load # Function name alias
110
135
  imread = load # Function name alias
111
136
 
112
137
 
113
- def save(data, filename, overwrite=False, dim_order='zyx', metadata=None, compress=False):
138
+ def save(data,
139
+ filename,
140
+ overwrite=False,
141
+ dim_order='zyx',
142
+ pixel_size=None,
143
+ units=None,
144
+ compress=None,
145
+ metadata=None):
114
146
  """
115
147
  Save a numpy array to file with a file type specified by the
116
148
  filename extension.
@@ -122,16 +154,18 @@ def save(data, filename, overwrite=False, dim_order='zyx', metadata=None, compre
122
154
  a 1-channel 2D image in xy order, or a multi-channel 2D image
123
155
  in xyc order.
124
156
 
125
- `compress` only matters when saving in `.nrrd` format
157
+ Currently `pixel_size`, `units`, and `compress` are only recognized
158
+ when saving to .nrrd and .ng files, and ignored otherwise.
126
159
  """
160
+ filename = str(filename)
127
161
  filename = filename.rstrip('/')
128
162
  if os.path.exists(filename) and not overwrite:
129
- raise Exception(f'File {filename} already exists. '
130
- 'Set overwrite=True to overwrite.')
163
+ raise FileExistsError(f'File {filename} already exists. '
164
+ 'Set overwrite=True to overwrite.')
131
165
  extension = filename.split('.')[-1]
132
166
  assert extension in supported_extensions, f'Filetype {extension} not supported'
133
167
 
134
- if compress and extension != 'nrrd':
168
+ if compress and extension not in ['nrrd', 'ng']:
135
169
  print('WARNING: compress argument is ignored because not saving as '
136
170
  '.nrrd. Whether or not compression occurs now will depend on '
137
171
  'the format you are saving to.')
@@ -163,13 +197,50 @@ def save(data, filename, overwrite=False, dim_order='zyx', metadata=None, compre
163
197
  import nrrd # pip install pynrrd
164
198
  if metadata is None:
165
199
  metadata = {}
166
- if 'encoding' not in metadata:
167
- if compress:
168
- metadata.update({'encoding': 'gzip'})
200
+ else:
201
+ metadata = metadata.copy()
202
+ if compress is True:
203
+ metadata.update({'encoding': 'gzip'})
204
+ if compress is False or 'encoding' not in metadata:
205
+ metadata.update({'encoding': 'raw'})
206
+ if pixel_size is not None:
207
+ try:
208
+ iter(pixel_size)
209
+ except TypeError:
210
+ pixel_size = [pixel_size] * data.ndim
211
+
212
+ if 'xy' in dim_order:
213
+ pixel_size = np.flip(pixel_size)
214
+ metadata.update({'space directions': np.flip(np.diag(pixel_size),
215
+ axis=-1)})
216
+ if 'space dimension' not in metadata and 'space' not in metadata:
217
+ # If the number of spatial dimensions is not specified, assume
218
+ # it's the number of dimensions in the data array, minus the
219
+ # channel axis if it's present.
220
+ if is_rgb_or_rgba(data):
221
+ metadata.update({'space dimension': data.ndim - 1})
169
222
  else:
170
- metadata.update({'encoding': 'raw'})
171
- # Specify C-style ordering when writing zyx-ordered array using nrrd.write
172
- # https://pynrrd.readthedocs.io/en/latest/user-guide.html#index-ordering
223
+ metadata.update({'space dimension': data.ndim})
224
+ if units is not None:
225
+ metadata.update({'space units': [units] * data.ndim})
226
+
227
+ # From https://pynrrd.readthedocs.io/en/stable/background/index-ordering.html
228
+ # "C-order is the index order used in Python and many Python libraries
229
+ # (e.g. NumPy, scikit-image, PIL, OpenCV). pynrrd recommends using
230
+ # C-order indexing to be consistent with the Python community. However,
231
+ # as of this time, the default indexing [in the nrrd.write command] is
232
+ # Fortran-order to maintain backwards compatibility."
233
+ # "All header fields are specified in Fortran order, per the NRRD
234
+ # specification, regardless of the index order. For example, a
235
+ # C-ordered array with shape (60, 800, 600) would have a sizes field
236
+ # of (600, 800, 60)."
237
+ #
238
+ # We expect users of this save function to pass in metadata that has
239
+ # header fields ordered in the same order as their data, so in addition
240
+ # to effectively flipping the order of the data's axes by specifying
241
+ # index_order='C' to the nrrd.write command below, we also need to flip
242
+ # the order of any per-axis metadata fields.
243
+ utils.transpose_metadata(metadata, inplace=True)
173
244
  nrrd.write(filename, data, header=metadata, index_order='C')
174
245
 
175
246
  if extension in ['raw', 'vol']:
@@ -178,6 +249,54 @@ def save(data, filename, overwrite=False, dim_order='zyx', metadata=None, compre
178
249
  if extension == 'zarr':
179
250
  raise NotImplementedError
180
251
 
252
+ if extension == 'ng':
253
+ from cloudvolume import CloudVolume
254
+
255
+ # CloudVolume expects data in Fortran order
256
+ if is_rgb_or_rgba(data):
257
+ data = data.swapaxes(0, 1)
258
+ else:
259
+ data = data.T
260
+
261
+ resolution = 1
262
+ if pixel_size is not None:
263
+ resolution = pixel_size
264
+ try:
265
+ iter(resolution)
266
+ except TypeError:
267
+ resolution = [resolution] * data.ndim
268
+ if compress is True or compress == 'lossy':
269
+ encoding = 'jpeg'
270
+ gzip = True
271
+ elif compress == 'lossless':
272
+ encoding = 'raw',
273
+ gzip = True
274
+ elif compress is False or compress is None:
275
+ encoding = 'raw'
276
+ gzip = False
277
+ else:
278
+ raise ValueError('For .ng format, compress must be True, False,'
279
+ f' "lossy", or "lossless" but was {compress}')
280
+
281
+ info = CloudVolume.create_new_info(
282
+ num_channels=1,
283
+ layer_type='image',
284
+ data_type=data.dtype,
285
+ encoding=encoding,
286
+ resolution=resolution,
287
+ voxel_offset=[0, 0, 0],
288
+ chunk_size=[64, 64, 64],
289
+ volume_size=data.shape
290
+ )
291
+
292
+ if not any(filename.startswith(prefix)
293
+ for prefix in ['file://', 'gs://', 's3://']):
294
+ filename = 'file://' + filename
295
+ vol = CloudVolume(filename, info=info, compress=gzip)
296
+ vol.commit_info()
297
+
298
+ vol[:] = data
299
+
181
300
 
182
301
  write = save # Function name alias
183
302
  to_file = save # Function name alias
@@ -190,6 +309,11 @@ def save_video(data, filename, overwrite=False, dim_order='yx', time_axis=0,
190
309
 
191
310
  Follows the PyAV cookbook section on generating video from numpy arrays:
192
311
  https://pyav.basswood-io.com/docs/develop/cookbook/numpy.html#generating-video
312
+
313
+ Parameters
314
+ ----------
315
+ data : numpy.ndarray or list of filenames
316
+ A 3D numpy array of pixel values, with the time axis as the first axis.
193
317
  """
194
318
  if not data.ndim == 3:
195
319
  raise ValueError('Input data must be a 3D numpy array.')
@@ -203,9 +327,11 @@ def save_video(data, filename, overwrite=False, dim_order='yx', time_axis=0,
203
327
  data = data.swapaxes(1, 2)
204
328
  n_frames = data.shape[0]
205
329
 
330
+ if filename.split('.')[-1] not in ['mp4', 'mkv', 'avi', 'mov']:
331
+ filename += '.mp4'
206
332
  if os.path.exists(filename) and not overwrite:
207
- raise Exception(f'File {filename} already exists. '
208
- 'Set overwrite=True to overwrite.')
333
+ raise FileExistsError(f'File {filename} already exists. '
334
+ 'Set overwrite=True to overwrite.')
209
335
  container = av.open(filename, mode='w')
210
336
 
211
337
  stream = container.add_stream('libx264', rate=framerate)
@@ -27,7 +27,6 @@ def load(filename):
27
27
  data = data.reshape((h, w)).view(bool)
28
28
 
29
29
  return data
30
-
31
30
 
32
31
 
33
32
  def save(data, filename, comments=None):