numpyimage 3.7.0__tar.gz → 3.8.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 (27) hide show
  1. {numpyimage-3.7.0 → numpyimage-3.8.0}/PKG-INFO +26 -6
  2. {numpyimage-3.7.0 → numpyimage-3.8.0}/README.md +14 -4
  3. {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/align.py +15 -6
  4. {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/graphics.py +22 -12
  5. {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/imageio.py +26 -0
  6. {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/vidio.py +110 -8
  7. {numpyimage-3.7.0 → numpyimage-3.8.0}/numpyimage.egg-info/PKG-INFO +26 -6
  8. numpyimage-3.8.0/numpyimage.egg-info/requires.txt +24 -0
  9. {numpyimage-3.7.0 → numpyimage-3.8.0}/pyproject.toml +22 -3
  10. numpyimage-3.8.0/tests/test_heic.py +58 -0
  11. numpyimage-3.8.0/tests/test_pbm.py +37 -0
  12. numpyimage-3.8.0/tests/test_video_streamer.py +322 -0
  13. numpyimage-3.8.0/tests/test_video_writers.py +37 -0
  14. numpyimage-3.7.0/numpyimage.egg-info/requires.txt +0 -11
  15. numpyimage-3.7.0/tests/test_heic.py +0 -61
  16. numpyimage-3.7.0/tests/test_pbm.py +0 -61
  17. numpyimage-3.7.0/tests/test_video_streamer.py +0 -92
  18. numpyimage-3.7.0/tests/test_video_writers.py +0 -170
  19. {numpyimage-3.7.0 → numpyimage-3.8.0}/LICENSE +0 -0
  20. {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/__init__.py +0 -0
  21. {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/nrrd_utils.py +0 -0
  22. {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/operations.py +0 -0
  23. {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/utils.py +0 -0
  24. {numpyimage-3.7.0 → numpyimage-3.8.0}/numpyimage.egg-info/SOURCES.txt +0 -0
  25. {numpyimage-3.7.0 → numpyimage-3.8.0}/numpyimage.egg-info/dependency_links.txt +0 -0
  26. {numpyimage-3.7.0 → numpyimage-3.8.0}/numpyimage.egg-info/top_level.txt +0 -0
  27. {numpyimage-3.7.0 → numpyimage-3.8.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numpyimage
3
- Version: 3.7.0
3
+ Version: 3.8.0
4
4
  Summary: Load, save, & manipulate image files as numpy arrays
5
5
  Author-email: Jasper Phelps <jasper.s.phelps@gmail.com>
6
6
  License: MIT License
@@ -42,21 +42,38 @@ Requires-Dist: pillow-heif
42
42
  Requires-Dist: tifffile
43
43
  Requires-Dist: pynrrd
44
44
  Requires-Dist: matplotlib
45
- Requires-Dist: opencv-python-headless
45
+ Provides-Extra: vid
46
+ Requires-Dist: av; extra == "vid"
47
+ Provides-Extra: align
48
+ Requires-Dist: opencv-python-headless; extra == "align"
46
49
  Provides-Extra: all
47
50
  Requires-Dist: av; extra == "all"
51
+ Requires-Dist: opencv-python-headless; extra == "all"
52
+ Provides-Extra: dev
53
+ Requires-Dist: av; extra == "dev"
54
+ Requires-Dist: opencv-python-headless; extra == "dev"
55
+ Requires-Dist: pytest; extra == "dev"
56
+ Requires-Dist: pytest-cov; extra == "dev"
57
+ Requires-Dist: psutil; extra == "dev"
48
58
  Dynamic: license-file
49
59
 
50
- # npimage
60
+ # npimage: Load, save & manipulate numpy arrays representing images & videos
61
+
62
+ [![Tests](https://github.com/jasper-tms/npimage/actions/workflows/tests.yml/badge.svg)](https://github.com/jasper-tms/npimage/actions/workflows/tests.yml)
63
+ [![codecov](https://codecov.io/gh/jasper-tms/npimage/branch/main/graph/badge.svg)](https://codecov.io/gh/jasper-tms/npimage)
64
+ [![PyPI version](https://img.shields.io/pypi/v/numpyimage)](https://pypi.org/project/numpyimage/)
65
+ [![PyPI downloads](https://img.shields.io/pypi/dm/numpyimage)](https://pypi.org/project/numpyimage/)
66
+ [![License](https://img.shields.io/github/license/jasper-tms/npimage)](https://github.com/jasper-tms/npimage/blob/main/LICENSE)
67
+
51
68
  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 `imageio.py` does that, with `array = npimage.load(filename)`, `npimage.save(array, filename)`, and `npimage.show(array)` functions that let you easily handle a number of common image file formats without having to remember library-specific syntax. Additionally, `vidio.py` provides `array = npimage.load_video(filename)` and `npimage.save_video(array, filename)` for videos as well. (Another similar library to consider using is [imageio](https://pypi.org/project/imageio/).)
52
69
 
53
- 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!)
70
+ 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/4D/+? I wrote some functions in `graphics.py` that do the trick.
54
71
 
55
72
 
56
73
  ### Documentation
57
74
  - `imageio.py`: load, save, or show images.
58
75
  - `vidio.py`: load or save videos.
59
- - `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
76
+ - `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D/3D/+D numpy arrays representing image data.
60
77
  - `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
61
78
  - `operations.py`: perform operations on images.
62
79
 
@@ -70,6 +87,9 @@ As is always the case in python, consider making a virtual environment (using yo
70
87
  **Option 1:** `pip install` from PyPI:
71
88
 
72
89
  pip install numpyimage
90
+ pip install 'numpyimage[vid]' # If you also want to install the video loading/saving dependencies
91
+ pip install 'numpyimage[align]' # If you also want to install the image alignment dependencies
92
+ pip install 'numpyimage[all]' # If you want to install both video and alignment dependencies
73
93
 
74
94
  (Unfortunately the name `npimage` was already taken on PyPI, so `pip install npimage` will get you a different package.)
75
95
 
@@ -82,6 +102,6 @@ As is always the case in python, consider making a virtual environment (using yo
82
102
  cd ~/repos # Or wherever on your computer you want to download this code to
83
103
  git clone https://github.com/jasper-tms/npimage.git
84
104
  cd npimage
85
- pip install .
105
+ pip install '.[all]'
86
106
 
87
107
  **After installing,** you can import this package in python using `import npimage` (not `import numpyimage`!)
@@ -1,13 +1,20 @@
1
- # npimage
1
+ # npimage: Load, save & manipulate numpy arrays representing images & videos
2
+
3
+ [![Tests](https://github.com/jasper-tms/npimage/actions/workflows/tests.yml/badge.svg)](https://github.com/jasper-tms/npimage/actions/workflows/tests.yml)
4
+ [![codecov](https://codecov.io/gh/jasper-tms/npimage/branch/main/graph/badge.svg)](https://codecov.io/gh/jasper-tms/npimage)
5
+ [![PyPI version](https://img.shields.io/pypi/v/numpyimage)](https://pypi.org/project/numpyimage/)
6
+ [![PyPI downloads](https://img.shields.io/pypi/dm/numpyimage)](https://pypi.org/project/numpyimage/)
7
+ [![License](https://img.shields.io/github/license/jasper-tms/npimage)](https://github.com/jasper-tms/npimage/blob/main/LICENSE)
8
+
2
9
  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 `imageio.py` does that, with `array = npimage.load(filename)`, `npimage.save(array, filename)`, and `npimage.show(array)` functions that let you easily handle a number of common image file formats without having to remember library-specific syntax. Additionally, `vidio.py` provides `array = npimage.load_video(filename)` and `npimage.save_video(array, filename)` for videos as well. (Another similar library to consider using is [imageio](https://pypi.org/project/imageio/).)
3
10
 
4
- 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!)
11
+ 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/4D/+? I wrote some functions in `graphics.py` that do the trick.
5
12
 
6
13
 
7
14
  ### Documentation
8
15
  - `imageio.py`: load, save, or show images.
9
16
  - `vidio.py`: load or save videos.
10
- - `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
17
+ - `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D/3D/+D numpy arrays representing image data.
11
18
  - `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
12
19
  - `operations.py`: perform operations on images.
13
20
 
@@ -21,6 +28,9 @@ As is always the case in python, consider making a virtual environment (using yo
21
28
  **Option 1:** `pip install` from PyPI:
22
29
 
23
30
  pip install numpyimage
31
+ pip install 'numpyimage[vid]' # If you also want to install the video loading/saving dependencies
32
+ pip install 'numpyimage[align]' # If you also want to install the image alignment dependencies
33
+ pip install 'numpyimage[all]' # If you want to install both video and alignment dependencies
24
34
 
25
35
  (Unfortunately the name `npimage` was already taken on PyPI, so `pip install npimage` will get you a different package.)
26
36
 
@@ -33,6 +43,6 @@ As is always the case in python, consider making a virtual environment (using yo
33
43
  cd ~/repos # Or wherever on your computer you want to download this code to
34
44
  git clone https://github.com/jasper-tms/npimage.git
35
45
  cd npimage
36
- pip install .
46
+ pip install '.[all]'
37
47
 
38
48
  **After installing,** you can import this package in python using `import npimage` (not `import numpyimage`!)
@@ -2,18 +2,15 @@
2
2
  """
3
3
  Functions for aligning images.
4
4
  """
5
- from typing import Optional, Tuple, Literal
5
+ from typing import Optional, Tuple
6
6
 
7
7
  import numpy as np
8
- import cv2 as cv
9
8
 
10
9
 
11
10
  def find_landmark(image: np.ndarray,
12
11
  landmark: np.ndarray,
13
12
  search_bbox: Optional[Tuple[Tuple[int, int], Tuple[int, int]]] = None,
14
- metric: Literal[cv.TM_SQDIFF, cv.TM_SQDIFF_NORMED,
15
- cv.TM_CCORR, cv.TM_CCORR_NORMED,
16
- cv.TM_CCOEFF, cv.TM_CCOEFF_NORMED] = cv.TM_CCOEFF_NORMED,
13
+ metric: Optional[int] = None,
17
14
  subpixel_accuracy: bool = True
18
15
  ) -> Tuple[Tuple[int, int], float]:
19
16
  """
@@ -45,7 +42,8 @@ def find_landmark(image: np.ndarray,
45
42
  metric : cv.TemplateMatchModes, optional
46
43
  The metric to use to compare the landmark to the image. Options are
47
44
  cv.TM_SQDIFF, cv.TM_SQDIFF_NORMED, cv.TM_CCORR, cv.TM_CCORR_NORMED,
48
- cv.TM_CCOEFF, cv.TM_CCOEFF_NORMED. Default is cv.TM_CCOEFF_NORMED.
45
+ cv.TM_CCOEFF, cv.TM_CCOEFF_NORMED.
46
+ If left as None, defaults to cv.TM_CCOEFF_NORMED.
49
47
 
50
48
  Returns
51
49
  -------
@@ -57,6 +55,17 @@ def find_landmark(image: np.ndarray,
57
55
  The score of the match, from 0 to 1. A score of 1 indicates
58
56
  a perfect match.
59
57
  """
58
+ try:
59
+ import cv2 as cv
60
+ except ImportError as e:
61
+ raise ImportError(
62
+ 'find_landmark requires opencv. Install it with '
63
+ '`pip install numpyimage[align]` or `pip install opencv-python-headless`.'
64
+ ) from e
65
+
66
+ if metric is None:
67
+ metric = cv.TM_CCOEFF_NORMED
68
+
60
69
  if search_bbox is not None:
61
70
  if all(isinstance(el, slice) for el in search_bbox):
62
71
  img_to_search = image[search_bbox[0], search_bbox[1]]
@@ -399,10 +399,14 @@ def imset(image, coords, value,
399
399
  Parameters
400
400
  ----------
401
401
  image : numpy.ndarray
402
- The image to set pixel value(s) in.
402
+ The N-dimensional image to set pixel value(s) in.
403
403
 
404
404
  coords : numpy.ndarray or array-like
405
- The coordinate locations to set values of.
405
+ The coordinate locations to set values of. Must be a 2D array
406
+ of shape (C, M) where C is however many coordinates you want to
407
+ set, and M must equal N (number of dimensions in `image`) if
408
+ `add_missing_dims` is None, or M can be less than N if
409
+ `add_missing_dims` is 'start' or 'end'.
406
410
 
407
411
  value : int, float, or array-like
408
412
  The value to set the pixel(s) at the given coordinates to.
@@ -443,20 +447,26 @@ def imset(image, coords, value,
443
447
  Refusing to wrap makes more sense in most graphics applications.
444
448
 
445
449
  add_missing_dims : {None, 'start', 'end'}, default None
446
- If the `coords` argument has fewer dimensions than the `image`
447
- argument, this parameter determines how to handle that mismatch.
448
-
450
+ If `coords` has fewer columns (M) than `image` has dimensions (N),
451
+ this parameter determines whether to add columns of slice(None) to
452
+ the 'start' or 'end' of the coordinate list (or to raise an error
453
+ when set to None).
449
454
  This is useful when you want to set all the rows, columns, or
450
- channels of an image to a particular value. For example:
451
- - You have a (360, 640, 3) RGB pixel array `image`
452
- - You have 100 pixels specified in a (100, 2) array `coords`
453
- - You want to make those pixels cyan, i.e. set the values along
454
- the last axis to (0, 255, 255)
455
- then since the color axis is the last axis, you can call:
456
- >>> imset(image, coords, (0, 255, 255), add_missing_dims='end')
455
+ channels of an image to a particular value. See `Examples` below
456
+
457
+ Examples
458
+ --------
459
+ Using `add_missing_dims` to set RGB pixels in a color image. Let's say:
460
+ - You have a (360, 640, 3) RGB pixel array `image`
461
+ - You have 100 pixels specified in a (100, 2) array `coords`
462
+ - You want to make those pixels cyan, i.e. set the values along
463
+ the last axis to (0, 255, 255)
464
+ then since the color axis is the last axis, you can call:
465
+ >>> imset(image, coords, (0, 255, 255), add_missing_dims='end')
457
466
  """
458
467
  if not isinstance(coords, np.ndarray):
459
468
  coords = np.array(coords)
469
+ coords = coords[~np.isnan(coords).any(axis=1)]
460
470
 
461
471
  if out_of_bounds == 'wrap':
462
472
  allow_negative_wrapping = True
@@ -452,6 +452,32 @@ def _ensure_heif_opener_registered() -> None:
452
452
  """Register the HEIF opener if not already registered."""
453
453
  global _heif_opener_registered
454
454
  if not _heif_opener_registered:
455
+ _check_pyav_not_loaded_before_heif()
455
456
  from pillow_heif import register_heif_opener
456
457
  register_heif_opener()
457
458
  _heif_opener_registered = True
459
+
460
+
461
+ def _check_pyav_not_loaded_before_heif() -> None:
462
+ """Raise a clear error if PyAV has been imported before HEIF.
463
+
464
+ On macOS, loading PyAV's bundled native libraries before libheif causes a
465
+ segfault inside pillow_heif when it first initializes. Loading libheif
466
+ first (or not loading PyAV at all) avoids the crash. Once libheif is
467
+ loaded, further HEIF operations and PyAV use coexist fine in the same
468
+ process.
469
+ """
470
+ import sys
471
+ if sys.platform != 'darwin' or 'av' not in sys.modules:
472
+ return
473
+ raise RuntimeError(
474
+ 'Cannot open a HEIC file: PyAV (`av`) has already been imported in '
475
+ 'this process, which on macOS causes pillow_heif to segfault when it '
476
+ 'first loads libheif.\n\n'
477
+ 'Workarounds:\n'
478
+ ' 1. Open/save your HEIC files before any code path that imports '
479
+ '`av` runs (e.g. before using npimage.VideoStreamer, AVVideoWriter, '
480
+ 'or any other code that uses PyAV).\n'
481
+ ' 2. Do HEIC I/O in a separate Python process from your video I/O.\n'
482
+ ' 3. Use a different image format (PNG, JPEG, TIFF).'
483
+ )
@@ -11,7 +11,8 @@ Function list:
11
11
 
12
12
  Class list:
13
13
  - VideoStreamer: Provides fast random access to frames in a video file
14
- via VideoStreamer[frame_number].
14
+ via VideoStreamer[frame_number], or by time in seconds via
15
+ VideoStreamer.t[time_in_seconds].
15
16
  - VideoWriter: Allows writing frames one-by-one to a video file via
16
17
  VideoWriter.write(image). This can be advantageous compared to save_video
17
18
  because you don't ever have to have all the frames in memory at once.
@@ -22,6 +23,8 @@ from pathlib import Path
22
23
  import subprocess
23
24
  import threading
24
25
  import json
26
+ import math
27
+ import bisect
25
28
  from fractions import Fraction
26
29
 
27
30
  import numpy as np
@@ -247,9 +250,10 @@ class VideoStreamer:
247
250
 
248
251
  self.index_filename = self.filename.with_suffix(self.filename.suffix + '.index')
249
252
  self._build_index(cache_index=cache_index)
253
+ self.t = _VideoStreamerTimeIndexer(self)
250
254
 
251
255
  def _build_index(self, cache_index='auto'):
252
- if self.index_filename.exists():
256
+ if cache_index and self.index_filename.exists():
253
257
  return self._load_index()
254
258
  if self.verbose:
255
259
  print('Building frame timestamp index for fast random frame access...')
@@ -301,9 +305,8 @@ class VideoStreamer:
301
305
  if len(frames_pts) == 0:
302
306
  raise RuntimeError('No timestamps found in frame metadata')
303
307
 
304
- self.frames_pts = frames_pts
305
308
  self.n_frames = len(frames_pts)
306
- self.pts0 = self.frames_pts[0]
309
+ self.pts0 = frames_pts[0]
307
310
  self.rotation = _get_rotation_from_metadata(self.filename)
308
311
  index = {}
309
312
 
@@ -319,10 +322,14 @@ class VideoStreamer:
319
322
  else:
320
323
  index['framerate'] = {'numerator': self._framerate.numerator,
321
324
  'denominator': self._framerate.denominator}
325
+ self.frames_pts = range(self.pts0,
326
+ self.pts0 + self.pts_delta * self.n_frames,
327
+ self.pts_delta)
322
328
  else:
323
329
  # The video is variable framerate
324
330
  self._framerate = 'variable'
325
331
  index['framerate'] = 'variable'
332
+ self.frames_pts = frames_pts
326
333
 
327
334
  index['n_frames'] = self.n_frames
328
335
  index['rotation'] = self.rotation
@@ -368,6 +375,9 @@ class VideoStreamer:
368
375
  raise ValueError('pts_delta does not appear to be an integer. This is'
369
376
  ' unexpected and may indicate a malformed index.')
370
377
  self.pts_delta = self.pts_delta.numerator
378
+ self.frames_pts = range(self.pts0,
379
+ self.pts0 + self.pts_delta * self.n_frames,
380
+ self.pts_delta)
371
381
 
372
382
  @property
373
383
  def framerate(self) -> Union[float, Literal['variable']]:
@@ -378,9 +388,11 @@ class VideoStreamer:
378
388
  @property
379
389
  def fps(self) -> float:
380
390
  """
381
- Note that fps always returns a float even if the framerate is 'variable'.
382
- (If framerate is 'variable', fps returns exactly what its name, "frames per second",
383
- implies: the number of frame intervals divided by the video duration in seconds.)
391
+ Frames per second, returned as a float even when the framerate is
392
+ 'variable'. For variable-framerate videos this is the average rate,
393
+ equal to `n_frames / duration`, equivalently the number of frame
394
+ intervals divided by the time span between the first and last frames'
395
+ timestamps.
384
396
  """
385
397
  if self.framerate == 'variable':
386
398
  return float((self.n_frames - 1) / self.time_base
@@ -396,7 +408,21 @@ class VideoStreamer:
396
408
 
397
409
  @property
398
410
  def duration(self) -> float:
399
- return float((self.frames_pts[-1] - self.frames_pts[0]) * self.time_base)
411
+ """
412
+ Total playback duration of the video in seconds, equal to
413
+ `n_frames / fps`.
414
+
415
+ Each frame is treated as occupying one inter-frame interval on
416
+ screen, so the duration extends from the first frame's timestamp to
417
+ one mean inter-frame interval past the last frame's timestamp. For
418
+ constant-framerate videos this is exactly `n_frames * timestep`.
419
+ For variable-framerate videos the last frame's display interval is
420
+ assumed to equal the mean inter-frame interval, which may differ
421
+ slightly from ffprobe's reported duration (ffprobe trusts the
422
+ encoder-written last-frame duration metadata, which is encoder-
423
+ dependent).
424
+ """
425
+ return float(self.n_frames / self.fps)
400
426
 
401
427
  def frame_number_to_pts(self, frame_number: int) -> int:
402
428
  if hasattr(frame_number, '__iter__'):
@@ -612,6 +638,76 @@ class VideoStreamer:
612
638
  self.close()
613
639
 
614
640
 
641
+ class _VideoStreamerTimeIndexer:
642
+ """
643
+ Helper exposing time-based indexing on a VideoStreamer via
644
+ ``streamer.t[time_in_seconds]``.
645
+
646
+ Returns a 3-tuple ``(image, timestamp, frame_index)`` for the frame
647
+ that is "on screen" at the requested time:
648
+ - ``image`` (np.ndarray): the frame's pixel data
649
+ - ``timestamp`` (float): the frame's actual timestamp in seconds
650
+ - ``frame_index`` (int): the frame's integer index in the video
651
+
652
+ Each frame N is treated as occupying the half-open interval
653
+ ``[frame_N_timestamp, frame_{N+1}_timestamp)``, and the last frame
654
+ occupies one mean inter-frame interval past its timestamp. The lookup
655
+ is therefore "largest frame timestamp less than or equal to the
656
+ requested time", with a half-tick eps tolerance so that a request for
657
+ the exact timestamp of frame N reliably returns frame N even when
658
+ floating-point arithmetic has nudged the value slightly below.
659
+
660
+ Valid times span ``[first_frame_timestamp, end_of_playback)``, where
661
+ ``end_of_playback = first_frame_timestamp + duration``.
662
+
663
+ Negative times wrap around relative to ``end_of_playback``, mirroring
664
+ how negative integer indices wrap on ``VideoStreamer``: a time of
665
+ ``-x`` resolves to ``end_of_playback - x``. Times more negative than
666
+ ``-duration`` raise ``IndexError``.
667
+ """
668
+ def __init__(self, streamer: 'VideoStreamer'):
669
+ self._streamer = streamer
670
+
671
+ def __getitem__(self, time) -> Tuple[np.ndarray, float, int]:
672
+ if not np.issubdtype(type(time), np.number):
673
+ raise TypeError(f'Time index must be a number, got {type(time).__name__}')
674
+ s = self._streamer
675
+ time = float(time)
676
+ first_frame_time = s.frame_number_to_time(0)
677
+ end_of_playback = first_frame_time + s.duration
678
+ if time < 0:
679
+ if time < -s.duration:
680
+ raise IndexError(f'Negative time {time:g} s is more than one'
681
+ f' video duration ({s.duration:g} s) before'
682
+ f' the end of the video.')
683
+ time = time + end_of_playback
684
+ if time >= end_of_playback:
685
+ raise IndexError(f'No frame at time {time:g} s (playback'
686
+ f' ends at {end_of_playback:g} s).')
687
+ if time < first_frame_time:
688
+ raise IndexError(f'Time {time:g} s is before the first frame'
689
+ f' (timestamp {first_frame_time:g} s).')
690
+ # Convert the requested time to a fractional PTS, then add half a
691
+ # time_base tick. The eps shift means a request for the exact
692
+ # timestamp of frame N returns frame N (rather than frame N-1) even
693
+ # if float arithmetic has nudged the converted value below the
694
+ # stored integer PTS.
695
+ target_pts = time / float(s.time_base) + 0.5
696
+ if s._framerate == 'variable':
697
+ # Largest index i with s.frames_pts[i] <= target_pts.
698
+ frame_number = bisect.bisect_right(s.frames_pts, target_pts) - 1
699
+ else:
700
+ offset = target_pts - s.pts0
701
+ frame_number = math.floor(offset / s.pts_delta)
702
+ # The bound checks above guarantee frame_number lands in
703
+ # [0, n_frames - 1] under exact arithmetic. Clamp defensively for
704
+ # the float-roundoff edge at exactly time == end_of_playback - eps.
705
+ frame_number = max(0, min(frame_number, s.n_frames - 1))
706
+ image = s._get_frame(frame_number)
707
+ timestamp = s.frame_number_to_time(frame_number)
708
+ return (image, timestamp, frame_number)
709
+
710
+
615
711
  class AVVideoWriter:
616
712
  """
617
713
  Create a video writer object for saving frames to a video file.
@@ -655,6 +751,9 @@ class AVVideoWriter:
655
751
  raise FileExistsError(f'File {filename} already exists. '
656
752
  'Set overwrite=True to overwrite.')
657
753
  self.filename = filename
754
+ if not np.issubdtype(type(framerate), np.number):
755
+ raise TypeError('framerate must be a number but got'
756
+ f' type {type(framerate)} instead')
658
757
  self._framerate = utils.limit_fraction(framerate)
659
758
  self.crf = crf
660
759
  self.compression_speed = compression_speed
@@ -770,6 +869,9 @@ class FFmpegVideoWriter:
770
869
  raise FileExistsError(f'File {filename} already exists. '
771
870
  'Set overwrite=True to overwrite.')
772
871
  self.filename = filename
872
+ if not np.issubdtype(type(framerate), np.number):
873
+ raise TypeError('framerate must be a number but got'
874
+ f' type {type(framerate)} instead')
773
875
  self._framerate = utils.limit_fraction(framerate)
774
876
  self.crf = crf
775
877
  self.compression_speed = compression_speed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numpyimage
3
- Version: 3.7.0
3
+ Version: 3.8.0
4
4
  Summary: Load, save, & manipulate image files as numpy arrays
5
5
  Author-email: Jasper Phelps <jasper.s.phelps@gmail.com>
6
6
  License: MIT License
@@ -42,21 +42,38 @@ Requires-Dist: pillow-heif
42
42
  Requires-Dist: tifffile
43
43
  Requires-Dist: pynrrd
44
44
  Requires-Dist: matplotlib
45
- Requires-Dist: opencv-python-headless
45
+ Provides-Extra: vid
46
+ Requires-Dist: av; extra == "vid"
47
+ Provides-Extra: align
48
+ Requires-Dist: opencv-python-headless; extra == "align"
46
49
  Provides-Extra: all
47
50
  Requires-Dist: av; extra == "all"
51
+ Requires-Dist: opencv-python-headless; extra == "all"
52
+ Provides-Extra: dev
53
+ Requires-Dist: av; extra == "dev"
54
+ Requires-Dist: opencv-python-headless; extra == "dev"
55
+ Requires-Dist: pytest; extra == "dev"
56
+ Requires-Dist: pytest-cov; extra == "dev"
57
+ Requires-Dist: psutil; extra == "dev"
48
58
  Dynamic: license-file
49
59
 
50
- # npimage
60
+ # npimage: Load, save & manipulate numpy arrays representing images & videos
61
+
62
+ [![Tests](https://github.com/jasper-tms/npimage/actions/workflows/tests.yml/badge.svg)](https://github.com/jasper-tms/npimage/actions/workflows/tests.yml)
63
+ [![codecov](https://codecov.io/gh/jasper-tms/npimage/branch/main/graph/badge.svg)](https://codecov.io/gh/jasper-tms/npimage)
64
+ [![PyPI version](https://img.shields.io/pypi/v/numpyimage)](https://pypi.org/project/numpyimage/)
65
+ [![PyPI downloads](https://img.shields.io/pypi/dm/numpyimage)](https://pypi.org/project/numpyimage/)
66
+ [![License](https://img.shields.io/github/license/jasper-tms/npimage)](https://github.com/jasper-tms/npimage/blob/main/LICENSE)
67
+
51
68
  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 `imageio.py` does that, with `array = npimage.load(filename)`, `npimage.save(array, filename)`, and `npimage.show(array)` functions that let you easily handle a number of common image file formats without having to remember library-specific syntax. Additionally, `vidio.py` provides `array = npimage.load_video(filename)` and `npimage.save_video(array, filename)` for videos as well. (Another similar library to consider using is [imageio](https://pypi.org/project/imageio/).)
52
69
 
53
- 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!)
70
+ 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/4D/+? I wrote some functions in `graphics.py` that do the trick.
54
71
 
55
72
 
56
73
  ### Documentation
57
74
  - `imageio.py`: load, save, or show images.
58
75
  - `vidio.py`: load or save videos.
59
- - `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
76
+ - `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D/3D/+D numpy arrays representing image data.
60
77
  - `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
61
78
  - `operations.py`: perform operations on images.
62
79
 
@@ -70,6 +87,9 @@ As is always the case in python, consider making a virtual environment (using yo
70
87
  **Option 1:** `pip install` from PyPI:
71
88
 
72
89
  pip install numpyimage
90
+ pip install 'numpyimage[vid]' # If you also want to install the video loading/saving dependencies
91
+ pip install 'numpyimage[align]' # If you also want to install the image alignment dependencies
92
+ pip install 'numpyimage[all]' # If you want to install both video and alignment dependencies
73
93
 
74
94
  (Unfortunately the name `npimage` was already taken on PyPI, so `pip install npimage` will get you a different package.)
75
95
 
@@ -82,6 +102,6 @@ As is always the case in python, consider making a virtual environment (using yo
82
102
  cd ~/repos # Or wherever on your computer you want to download this code to
83
103
  git clone https://github.com/jasper-tms/npimage.git
84
104
  cd npimage
85
- pip install .
105
+ pip install '.[all]'
86
106
 
87
107
  **After installing,** you can import this package in python using `import npimage` (not `import numpyimage`!)
@@ -0,0 +1,24 @@
1
+ numpy
2
+ tqdm
3
+ pillow
4
+ pillow-heif
5
+ tifffile
6
+ pynrrd
7
+ matplotlib
8
+
9
+ [align]
10
+ opencv-python-headless
11
+
12
+ [all]
13
+ av
14
+ opencv-python-headless
15
+
16
+ [dev]
17
+ av
18
+ opencv-python-headless
19
+ pytest
20
+ pytest-cov
21
+ psutil
22
+
23
+ [vid]
24
+ av
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
4
4
 
5
5
  [project]
6
6
  name = 'numpyimage'
7
- version = '3.7.0'
7
+ version = '3.8.0'
8
8
  description = 'Load, save, & manipulate image files as numpy arrays'
9
9
  readme.file = 'README.md'
10
10
  readme.content-type = 'text/markdown'
@@ -28,12 +28,31 @@ dependencies = [
28
28
  'tifffile',
29
29
  'pynrrd',
30
30
  'matplotlib',
31
- 'opencv-python-headless',
32
31
  ]
33
32
 
34
33
  [project.optional-dependencies]
34
+ vid = [
35
+ 'av',
36
+ ]
37
+ align = [
38
+ 'opencv-python-headless',
39
+ ]
35
40
  all = [
36
- 'av'
41
+ 'av',
42
+ 'opencv-python-headless',
43
+ ]
44
+ dev = [
45
+ 'av',
46
+ 'opencv-python-headless',
47
+ 'pytest',
48
+ 'pytest-cov',
49
+ 'psutil',
50
+ ]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ['tests']
54
+ markers = [
55
+ 'slow: marks tests that shell out to ffmpeg or write real video files (deselect with -m "not slow")',
37
56
  ]
38
57
 
39
58
  [tool.setuptools.packages.find]
@@ -0,0 +1,58 @@
1
+ """Tests for HEIC read/write support."""
2
+
3
+ import subprocess
4
+ import sys
5
+ import textwrap
6
+
7
+ import numpy as np
8
+ import pytest
9
+
10
+ import npimage
11
+
12
+
13
+ def test_heic_extension_registered():
14
+ """HEIC is in the set of supported file extensions."""
15
+ assert 'heic' in npimage.imageio.supported_extensions
16
+
17
+
18
+ def test_heic_load_save_roundtrip(tmp_path):
19
+ """A uint8 RGB image roundtripped through HEIC keeps its shape and dtype."""
20
+ data = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
21
+ path = tmp_path / 'test_image.heic'
22
+
23
+ npimage.save(data, str(path))
24
+ assert path.exists()
25
+
26
+ loaded = npimage.load(str(path))
27
+ assert loaded.shape == data.shape
28
+ assert loaded.dtype == np.uint8
29
+
30
+
31
+ @pytest.mark.skipif(sys.platform != 'darwin', reason='guard only fires on macOS')
32
+ def test_heic_guards_against_pyav_already_imported(tmp_path):
33
+ """
34
+ On macOS, importing PyAV before libheif causes a segfault inside
35
+ pillow_heif. npimage guards against this by raising a RuntimeError with a
36
+ clear message instead of letting the segfault happen.
37
+ """
38
+ script = textwrap.dedent(f"""
39
+ import av # noqa: F401
40
+ import numpy as np
41
+ import npimage
42
+ data = np.zeros((10, 10, 3), dtype=np.uint8)
43
+ try:
44
+ npimage.save(data, r'{tmp_path / "x.heic"}')
45
+ except RuntimeError as e:
46
+ print('GUARD_FIRED:', e)
47
+ raise SystemExit(0)
48
+ raise SystemExit('guard did not fire')
49
+ """)
50
+ result = subprocess.run(
51
+ [sys.executable, '-c', script], capture_output=True, text=True
52
+ )
53
+ assert result.returncode == 0, (
54
+ f'subprocess exited {result.returncode}. stdout={result.stdout!r} '
55
+ f'stderr={result.stderr!r}'
56
+ )
57
+ assert 'GUARD_FIRED:' in result.stdout
58
+ assert 'PyAV' in result.stdout