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.
- {numpyimage-3.7.0 → numpyimage-3.8.0}/PKG-INFO +26 -6
- {numpyimage-3.7.0 → numpyimage-3.8.0}/README.md +14 -4
- {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/align.py +15 -6
- {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/graphics.py +22 -12
- {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/imageio.py +26 -0
- {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/vidio.py +110 -8
- {numpyimage-3.7.0 → numpyimage-3.8.0}/numpyimage.egg-info/PKG-INFO +26 -6
- numpyimage-3.8.0/numpyimage.egg-info/requires.txt +24 -0
- {numpyimage-3.7.0 → numpyimage-3.8.0}/pyproject.toml +22 -3
- numpyimage-3.8.0/tests/test_heic.py +58 -0
- numpyimage-3.8.0/tests/test_pbm.py +37 -0
- numpyimage-3.8.0/tests/test_video_streamer.py +322 -0
- numpyimage-3.8.0/tests/test_video_writers.py +37 -0
- numpyimage-3.7.0/numpyimage.egg-info/requires.txt +0 -11
- numpyimage-3.7.0/tests/test_heic.py +0 -61
- numpyimage-3.7.0/tests/test_pbm.py +0 -61
- numpyimage-3.7.0/tests/test_video_streamer.py +0 -92
- numpyimage-3.7.0/tests/test_video_writers.py +0 -170
- {numpyimage-3.7.0 → numpyimage-3.8.0}/LICENSE +0 -0
- {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/__init__.py +0 -0
- {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/nrrd_utils.py +0 -0
- {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/operations.py +0 -0
- {numpyimage-3.7.0 → numpyimage-3.8.0}/npimage/utils.py +0 -0
- {numpyimage-3.7.0 → numpyimage-3.8.0}/numpyimage.egg-info/SOURCES.txt +0 -0
- {numpyimage-3.7.0 → numpyimage-3.8.0}/numpyimage.egg-info/dependency_links.txt +0 -0
- {numpyimage-3.7.0 → numpyimage-3.8.0}/numpyimage.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
|
|
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
|
+
[](https://github.com/jasper-tms/npimage/actions/workflows/tests.yml)
|
|
63
|
+
[](https://codecov.io/gh/jasper-tms/npimage)
|
|
64
|
+
[](https://pypi.org/project/numpyimage/)
|
|
65
|
+
[](https://pypi.org/project/numpyimage/)
|
|
66
|
+
[](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
|
|
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
|
|
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
|
+
[](https://github.com/jasper-tms/npimage/actions/workflows/tests.yml)
|
|
4
|
+
[](https://codecov.io/gh/jasper-tms/npimage)
|
|
5
|
+
[](https://pypi.org/project/numpyimage/)
|
|
6
|
+
[](https://pypi.org/project/numpyimage/)
|
|
7
|
+
[](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
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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
|
|
447
|
-
|
|
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.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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 =
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
[](https://github.com/jasper-tms/npimage/actions/workflows/tests.yml)
|
|
63
|
+
[](https://codecov.io/gh/jasper-tms/npimage)
|
|
64
|
+
[](https://pypi.org/project/numpyimage/)
|
|
65
|
+
[](https://pypi.org/project/numpyimage/)
|
|
66
|
+
[](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
|
|
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
|
|
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
|
+
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
|