numpyimage 3.6.3__tar.gz → 3.7.1__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.6.3 → numpyimage-3.7.1}/PKG-INFO +18 -2
  2. {numpyimage-3.6.3 → numpyimage-3.7.1}/README.md +6 -0
  3. {numpyimage-3.6.3 → numpyimage-3.7.1}/npimage/align.py +15 -6
  4. {numpyimage-3.6.3 → numpyimage-3.7.1}/npimage/imageio.py +26 -0
  5. {numpyimage-3.6.3 → numpyimage-3.7.1}/npimage/vidio.py +13 -3
  6. {numpyimage-3.6.3 → numpyimage-3.7.1}/numpyimage.egg-info/PKG-INFO +18 -2
  7. numpyimage-3.7.1/numpyimage.egg-info/requires.txt +24 -0
  8. {numpyimage-3.6.3 → numpyimage-3.7.1}/pyproject.toml +22 -3
  9. numpyimage-3.7.1/tests/test_heic.py +58 -0
  10. numpyimage-3.7.1/tests/test_pbm.py +37 -0
  11. numpyimage-3.7.1/tests/test_video_streamer.py +71 -0
  12. numpyimage-3.7.1/tests/test_video_writers.py +37 -0
  13. numpyimage-3.6.3/numpyimage.egg-info/requires.txt +0 -11
  14. numpyimage-3.6.3/tests/test_heic.py +0 -61
  15. numpyimage-3.6.3/tests/test_pbm.py +0 -61
  16. numpyimage-3.6.3/tests/test_video_streamer.py +0 -92
  17. numpyimage-3.6.3/tests/test_video_writers.py +0 -170
  18. {numpyimage-3.6.3 → numpyimage-3.7.1}/LICENSE +0 -0
  19. {numpyimage-3.6.3 → numpyimage-3.7.1}/npimage/__init__.py +0 -0
  20. {numpyimage-3.6.3 → numpyimage-3.7.1}/npimage/graphics.py +0 -0
  21. {numpyimage-3.6.3 → numpyimage-3.7.1}/npimage/nrrd_utils.py +0 -0
  22. {numpyimage-3.6.3 → numpyimage-3.7.1}/npimage/operations.py +0 -0
  23. {numpyimage-3.6.3 → numpyimage-3.7.1}/npimage/utils.py +0 -0
  24. {numpyimage-3.6.3 → numpyimage-3.7.1}/numpyimage.egg-info/SOURCES.txt +0 -0
  25. {numpyimage-3.6.3 → numpyimage-3.7.1}/numpyimage.egg-info/dependency_links.txt +0 -0
  26. {numpyimage-3.6.3 → numpyimage-3.7.1}/numpyimage.egg-info/top_level.txt +0 -0
  27. {numpyimage-3.6.3 → numpyimage-3.7.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numpyimage
3
- Version: 3.6.3
3
+ Version: 3.7.1
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,12 +42,28 @@ 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
60
  # npimage
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)
51
67
  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
68
 
53
69
  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!)
@@ -1,4 +1,10 @@
1
1
  # npimage
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)
2
8
  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
9
 
4
10
  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!)
@@ -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]]
@@ -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
+ )
@@ -249,7 +249,7 @@ class VideoStreamer:
249
249
  self._build_index(cache_index=cache_index)
250
250
 
251
251
  def _build_index(self, cache_index='auto'):
252
- if self.index_filename.exists():
252
+ if cache_index and self.index_filename.exists():
253
253
  return self._load_index()
254
254
  if self.verbose:
255
255
  print('Building frame timestamp index for fast random frame access...')
@@ -301,9 +301,8 @@ class VideoStreamer:
301
301
  if len(frames_pts) == 0:
302
302
  raise RuntimeError('No timestamps found in frame metadata')
303
303
 
304
- self.frames_pts = frames_pts
305
304
  self.n_frames = len(frames_pts)
306
- self.pts0 = self.frames_pts[0]
305
+ self.pts0 = frames_pts[0]
307
306
  self.rotation = _get_rotation_from_metadata(self.filename)
308
307
  index = {}
309
308
 
@@ -319,10 +318,14 @@ class VideoStreamer:
319
318
  else:
320
319
  index['framerate'] = {'numerator': self._framerate.numerator,
321
320
  'denominator': self._framerate.denominator}
321
+ self.frames_pts = range(self.pts0,
322
+ self.pts0 + self.pts_delta * self.n_frames,
323
+ self.pts_delta)
322
324
  else:
323
325
  # The video is variable framerate
324
326
  self._framerate = 'variable'
325
327
  index['framerate'] = 'variable'
328
+ self.frames_pts = frames_pts
326
329
 
327
330
  index['n_frames'] = self.n_frames
328
331
  index['rotation'] = self.rotation
@@ -368,6 +371,9 @@ class VideoStreamer:
368
371
  raise ValueError('pts_delta does not appear to be an integer. This is'
369
372
  ' unexpected and may indicate a malformed index.')
370
373
  self.pts_delta = self.pts_delta.numerator
374
+ self.frames_pts = range(self.pts0,
375
+ self.pts0 + self.pts_delta * self.n_frames,
376
+ self.pts_delta)
371
377
 
372
378
  @property
373
379
  def framerate(self) -> Union[float, Literal['variable']]:
@@ -394,6 +400,10 @@ class VideoStreamer:
394
400
  return 'variable'
395
401
  return float(1.0 / self._framerate)
396
402
 
403
+ @property
404
+ def duration(self) -> float:
405
+ return float((self.frames_pts[-1] - self.frames_pts[0]) * self.time_base)
406
+
397
407
  def frame_number_to_pts(self, frame_number: int) -> int:
398
408
  if hasattr(frame_number, '__iter__'):
399
409
  return [self.frame_number_to_pts(n) for n in frame_number]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numpyimage
3
- Version: 3.6.3
3
+ Version: 3.7.1
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,12 +42,28 @@ 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
60
  # npimage
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)
51
67
  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
68
 
53
69
  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!)
@@ -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.6.3'
7
+ version = '3.7.1'
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
@@ -0,0 +1,37 @@
1
+ """Tests for PBM read/write roundtripping."""
2
+
3
+ import numpy as np
4
+
5
+ import npimage
6
+
7
+
8
+ def _roundtrip(data, tmp_path, filename):
9
+ path = tmp_path / filename
10
+ npimage.save(data, str(path))
11
+
12
+ expected_size = npimage.filetypes.pbm.predict_file_size(data)
13
+ assert path.stat().st_size == expected_size
14
+
15
+ loaded = npimage.load(str(path))
16
+ assert np.array_equal(data, loaded)
17
+
18
+
19
+ def test_pbm_arbitrary_width(tmp_path):
20
+ """PBM roundtrip with a width that is not a multiple of 8."""
21
+ data = np.array([
22
+ [1, 0, 1, 0, 1],
23
+ [0, 1, 0, 1, 0],
24
+ [1, 1, 1, 0, 0],
25
+ ], dtype=bool)
26
+ _roundtrip(data, tmp_path, 'arbitrary_width.pbm')
27
+
28
+
29
+ def test_pbm_width_multiple_of_8(tmp_path):
30
+ """PBM roundtrip with a width that is a multiple of 8."""
31
+ data = np.array([
32
+ [1, 0, 1, 0, 1, 0, 1, 0],
33
+ [0, 1, 0, 1, 0, 1, 0, 1],
34
+ [1, 1, 1, 1, 0, 0, 0, 0],
35
+ [0, 0, 0, 0, 1, 1, 1, 1],
36
+ ], dtype=bool)
37
+ _roundtrip(data, tmp_path, 'multiple_of_8.pbm')
@@ -0,0 +1,71 @@
1
+ """
2
+ Tests for VideoStreamer, particularly handling of HEVC videos with
3
+ negative-PTS priming packets (common in iPhone recordings).
4
+ """
5
+
6
+ import subprocess
7
+
8
+ import numpy as np
9
+ import pytest
10
+
11
+ import npimage
12
+
13
+
14
+ @pytest.mark.slow
15
+ def test_negative_pts_priming_packet(tmp_path):
16
+ """
17
+ VideoStreamer correctly handles HEVC videos where the first packet has a
18
+ negative PTS (a "priming" packet used for B-frame prediction that doesn't
19
+ produce a decoded frame).
20
+
21
+ This is common in iPhone HEVC recordings. The bug was that the index was
22
+ built from demuxed packets, which included the priming packet's negative
23
+ PTS. When trying to access frame 0, it would seek to that negative PTS,
24
+ which no decoded frame ever has, causing a VideoSeekError.
25
+ """
26
+ import av
27
+
28
+ video_path = tmp_path / 'test_negative_pts.mp4'
29
+
30
+ # Create a small HEVC video with a negative-PTS priming packet.
31
+ # -output_ts_offset shifts timestamps so the first keyframe packet gets a
32
+ # negative PTS, mimicking iPhone HEVC behavior.
33
+ result = subprocess.run([
34
+ 'ffmpeg', '-y',
35
+ '-f', 'lavfi', '-i', 'color=c=red:size=320x240:rate=30:d=1',
36
+ '-c:v', 'libx265', '-preset', 'ultrafast',
37
+ '-bf', '2', '-x265-params', 'bframes=2',
38
+ '-tag:v', 'hvc1',
39
+ '-output_ts_offset', '-0.033',
40
+ str(video_path),
41
+ ], capture_output=True, text=True)
42
+ assert result.returncode == 0, f'ffmpeg failed: {result.stderr}'
43
+
44
+ # Verify the test video actually has the problematic structure: more
45
+ # packets than decoded frames due to the priming packet.
46
+ with av.open(str(video_path)) as container:
47
+ stream = container.streams.video[0]
48
+ pkt_pts = sorted(
49
+ p.pts for p in container.demux(stream) if p.pts is not None
50
+ )
51
+ with av.open(str(video_path)) as container:
52
+ stream = container.streams.video[0]
53
+ frame_pts = [f.pts for f in container.decode(stream)]
54
+
55
+ assert len(pkt_pts) > len(frame_pts), (
56
+ f'Test video should have more packets ({len(pkt_pts)}) than decoded '
57
+ f'frames ({len(frame_pts)}). The test setup may need updating if '
58
+ f'ffmpeg behavior has changed.'
59
+ )
60
+ assert pkt_pts[0] < 0, f'First packet PTS should be negative, got {pkt_pts[0]}'
61
+
62
+ vid = npimage.VideoStreamer(str(video_path))
63
+
64
+ assert vid.n_frames == len(frame_pts)
65
+
66
+ frame = vid[0]
67
+ assert isinstance(frame, np.ndarray)
68
+ assert frame.shape == (240, 320, 3)
69
+
70
+ last_frame = vid[vid.n_frames - 1]
71
+ assert isinstance(last_frame, np.ndarray)
@@ -0,0 +1,37 @@
1
+ """Tests for FFmpegVideoWriter and AVVideoWriter."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ import npimage
7
+ from npimage.vidio import FFmpegVideoWriter, AVVideoWriter
8
+
9
+
10
+ def _animated_frames(base_image, num_frames=30, movement_range=30):
11
+ """Generate frames that move `base_image` in a small circle."""
12
+ frames = []
13
+ for i in range(num_frames):
14
+ angle = 2 * np.pi * i / num_frames
15
+ dx = int(movement_range * np.cos(angle))
16
+ dy = int(movement_range * np.sin(angle))
17
+ frames.append(npimage.operations.offset(base_image, (dy, dx)))
18
+ return frames
19
+
20
+
21
+ @pytest.mark.slow
22
+ @pytest.mark.parametrize('writer_class', [FFmpegVideoWriter, AVVideoWriter])
23
+ def test_video_writer_roundtrip(writer_class, table_tennis_emoji, tmp_path):
24
+ """Writing frames with each writer produces a readable, non-empty file."""
25
+ frames = _animated_frames(table_tennis_emoji, num_frames=30)
26
+ output = tmp_path / f'{writer_class.__name__}.mp4'
27
+
28
+ with writer_class(str(output), framerate=30, overwrite=True) as writer:
29
+ for frame in frames:
30
+ writer.write(frame)
31
+
32
+ assert output.exists()
33
+ assert output.stat().st_size > 0
34
+
35
+ vid = npimage.VideoStreamer(str(output))
36
+ assert vid.n_frames == len(frames)
37
+ assert vid[0].shape[:2] == table_tennis_emoji.shape[:2]
@@ -1,11 +0,0 @@
1
- numpy
2
- tqdm
3
- pillow
4
- pillow-heif
5
- tifffile
6
- pynrrd
7
- matplotlib
8
- opencv-python-headless
9
-
10
- [all]
11
- av
@@ -1,61 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- import os
4
- import tempfile
5
- import numpy as np
6
- import npimage
7
-
8
-
9
- def test_heic_load_save():
10
- """Test loading and saving HEIC images."""
11
- # Create a simple test image
12
- test_data = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
13
-
14
- with tempfile.TemporaryDirectory() as tmpdir:
15
- # Test saving as HEIC
16
- heic_filename = os.path.join(tmpdir, "test_image.heic")
17
-
18
- try:
19
- # Save the test data as HEIC
20
- npimage.save(test_data, heic_filename)
21
-
22
- # Verify the file was created
23
- assert os.path.exists(heic_filename), "HEIC file was not created"
24
-
25
- # Load the HEIC file back
26
- loaded_data = npimage.load(heic_filename)
27
-
28
- # Check that the loaded data has the same shape
29
- assert loaded_data.shape == test_data.shape, f"Shape mismatch: expected {test_data.shape}, got {loaded_data.shape}"
30
-
31
- # Check that the data type is correct
32
- assert loaded_data.dtype == np.uint8, f"Data type mismatch: expected uint8, got {loaded_data.dtype}"
33
-
34
- print("Test passed: HEIC load/save functionality works correctly.")
35
-
36
- except Exception as e:
37
- print(f"Test failed with error: {e}")
38
- raise
39
-
40
-
41
- def test_heic_extension_support():
42
- """Test that HEIC extension is properly recognized."""
43
- # Test that HEIC is in the supported extensions
44
- assert 'heic' in npimage.core.supported_extensions, "HEIC extension not in supported extensions"
45
-
46
- # Test that the extension is properly parsed
47
- filename = "test_image.heic"
48
- extension = filename.split('.')[-1].lower()
49
- assert extension == 'heic', f"Extension parsing failed: expected 'heic', got '{extension}'"
50
-
51
- print("Test passed: HEIC extension is properly supported.")
52
-
53
-
54
- if __name__ == '__main__':
55
- try:
56
- test_heic_extension_support()
57
- test_heic_load_save()
58
- print("All HEIC tests passed!")
59
- except Exception as e:
60
- print(f"HEIC tests failed: {e}")
61
- raise
@@ -1,61 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- import os
4
-
5
- import numpy as np
6
-
7
- import npimage
8
-
9
-
10
- def test_pbm_load_save():
11
- test_cases = [
12
- {
13
- "data": np.array([
14
- [1, 0, 1, 0, 1],
15
- [0, 1, 0, 1, 0],
16
- [1, 1, 1, 0, 0]
17
- ], dtype=bool),
18
- "filename": "test_arbitrary_width.pbm",
19
- "description": "PBM read/write, arbitrary width"
20
- },
21
- {
22
- "data": np.array([
23
- [1, 0, 1, 0, 1, 0, 1, 0],
24
- [0, 1, 0, 1, 0, 1, 0, 1],
25
- [1, 1, 1, 1, 0, 0, 0, 0],
26
- [0, 0, 0, 0, 1, 1, 1, 1]
27
- ], dtype=bool),
28
- "filename": "test_multiple_of_8.pbm",
29
- "description": "PBM read/write, width a multiple of 8"
30
- }
31
- ]
32
-
33
- for case in test_cases:
34
- test_data = case["data"]
35
- test_filename = case["filename"]
36
- description = case["description"]
37
-
38
- try:
39
- # Save the test data to a PBM file
40
- npimage.save(test_data, test_filename)
41
-
42
- # Verify the file size
43
- expected_file_size = npimage.filetypes.pbm.predict_file_size(test_data)
44
- actual_file_size = os.path.getsize(test_filename)
45
- assert actual_file_size == expected_file_size, f"File size mismatch: expected {expected_file_size}, got {actual_file_size}"
46
-
47
- # Load the data back from the PBM file
48
- loaded_data = npimage.load(test_filename)
49
-
50
- # Assert that the loaded data matches the original data
51
- assert np.array_equal(test_data, loaded_data), f"Loaded data does not match original data for {description}"
52
-
53
- print(f"Test passed: {description} works correctly.")
54
- finally:
55
- # Clean up the test file
56
- if os.path.exists(test_filename):
57
- os.remove(test_filename)
58
-
59
-
60
- if __name__ == '__main__':
61
- test_pbm_load_save()
@@ -1,92 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Tests for VideoStreamer, particularly handling of HEVC videos with
4
- negative-PTS priming packets (common in iPhone recordings).
5
- """
6
-
7
- import os
8
- import subprocess
9
- import tempfile
10
-
11
- import numpy as np
12
-
13
- import npimage
14
-
15
-
16
- def test_negative_pts_priming_packet():
17
- """
18
- Test that VideoStreamer correctly handles HEVC videos where the first
19
- packet has a negative PTS (a "priming" packet used for B-frame prediction
20
- that doesn't produce a decoded frame).
21
-
22
- This is common in iPhone HEVC recordings. The bug was that the index was
23
- built from demuxed packets, which included the priming packet's negative
24
- PTS. When trying to access frame 0, it would seek to that negative PTS,
25
- which no decoded frame ever has, causing a VideoSeekError.
26
- """
27
- with tempfile.TemporaryDirectory() as tmpdir:
28
- video_path = os.path.join(tmpdir, 'test_negative_pts.mp4')
29
-
30
- # Create a small HEVC video with a negative-PTS priming packet.
31
- # The -output_ts_offset shifts timestamps so the first keyframe packet
32
- # gets a negative PTS, mimicking iPhone HEVC behavior.
33
- result = subprocess.run([
34
- 'ffmpeg', '-y',
35
- '-f', 'lavfi', '-i', 'color=c=red:size=320x240:rate=30:d=1',
36
- '-c:v', 'libx265', '-preset', 'ultrafast',
37
- '-bf', '2', '-x265-params', 'bframes=2',
38
- '-tag:v', 'hvc1',
39
- '-output_ts_offset', '-0.033',
40
- video_path
41
- ], capture_output=True, text=True)
42
- assert result.returncode == 0, f'ffmpeg failed: {result.stderr}'
43
-
44
- # Verify the test video actually has the problematic structure:
45
- # more packets than decoded frames due to the priming packet
46
- import av
47
- with av.open(video_path) as container:
48
- stream = container.streams.video[0]
49
- pkt_pts = sorted(
50
- p.pts for p in container.demux(stream) if p.pts is not None
51
- )
52
- with av.open(video_path) as container:
53
- stream = container.streams.video[0]
54
- frame_pts = [f.pts for f in container.decode(stream)]
55
-
56
- assert len(pkt_pts) > len(frame_pts), (
57
- f'Test video should have more packets ({len(pkt_pts)}) than '
58
- f'decoded frames ({len(frame_pts)}). The test setup may need '
59
- f'updating if ffmpeg behavior has changed.'
60
- )
61
- assert pkt_pts[0] < 0, (
62
- f'First packet PTS should be negative, got {pkt_pts[0]}'
63
- )
64
-
65
- # Now test that VideoStreamer handles this correctly
66
- vid = npimage.VideoStreamer(video_path)
67
-
68
- # The number of frames should match decoded frames, not packets
69
- assert vid.n_frames == len(frame_pts), (
70
- f'Expected {len(frame_pts)} frames, got {vid.n_frames}'
71
- )
72
-
73
- # Accessing frame 0 should work without raising VideoSeekError
74
- frame = vid[0]
75
- assert isinstance(frame, np.ndarray)
76
- assert frame.shape == (240, 320, 3)
77
-
78
- # Accessing the last frame should also work
79
- last_frame = vid[vid.n_frames - 1]
80
- assert isinstance(last_frame, np.ndarray)
81
-
82
- print(f'PASSED: Loaded {vid.n_frames} frames from video with '
83
- f'negative-PTS priming packet')
84
-
85
-
86
- def main():
87
- test_negative_pts_priming_packet()
88
- print('\nAll VideoStreamer tests passed.')
89
-
90
-
91
- if __name__ == '__main__':
92
- main()
@@ -1,170 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Test script to compare FFmpegVideoWriter and AVVideoWriter implementations.
4
- Uses the table tennis emoji image with animated movement to create test videos.
5
- """
6
-
7
- import os
8
- import time
9
- import tempfile
10
- import psutil
11
- import gc
12
- from pathlib import Path
13
-
14
- import numpy as np
15
-
16
- import npimage
17
- from npimage.vidio import FFmpegVideoWriter, AVVideoWriter
18
-
19
- script_dir = Path(__file__).parent
20
-
21
- # Memory monitoring
22
- process = psutil.Process(os.getpid())
23
-
24
-
25
- def print_memory(label):
26
- """Print current memory usage"""
27
- gc.collect()
28
- mem = process.memory_info().rss / 1024 / 1024
29
- print(f"{label}: {mem:.1f} MB")
30
-
31
-
32
- def create_animated_frames(base_image, num_frames=60, movement_range=128):
33
- """Create animated frames by moving the image around"""
34
- frames = []
35
- height, width = base_image.shape[:2]
36
-
37
- for i in range(num_frames):
38
- # Create circular motion
39
- angle = 2 * np.pi * i / num_frames
40
- dx = int(movement_range * np.cos(angle))
41
- dy = int(movement_range * np.sin(angle))
42
-
43
- # Apply offset to create movement
44
- frame = npimage.operations.offset(base_image, (dy, dx))
45
- frames.append(frame)
46
-
47
- return frames
48
-
49
-
50
- def test_video_writer(writer_class, filename, frames, writer_name):
51
- """Test a specific video writer class"""
52
- print(f"\n=== Testing {writer_name} ===")
53
- print_memory(f"Before {writer_name}")
54
-
55
- start_time = time.time()
56
-
57
- try:
58
- with writer_class(filename, framerate=30, overwrite=True) as writer:
59
- for i, frame in enumerate(frames):
60
- if i % 20 == 0: # Print progress every 20 frames
61
- print(f" Writing frame {i}/{len(frames)}")
62
- writer.write(frame)
63
-
64
- elapsed = time.time() - start_time
65
- file_size = os.path.getsize(filename) / 1024 # KB
66
-
67
- print_memory(f"After {writer_name}")
68
- print(f"✓ {writer_name} completed:")
69
- print(f" Time: {elapsed:.2f} seconds")
70
- print(f" File size: {file_size:.1f} KB")
71
- print(f" Frames per second: {len(frames)/elapsed:.1f}")
72
-
73
- return True
74
-
75
- except Exception as e:
76
- print(f"✗ {writer_name} failed: {e}")
77
- return False
78
-
79
-
80
- def test_memory_leak(writer_class, frames, writer_name, num_iterations=5):
81
- """Test for memory leaks by creating multiple videos"""
82
- print(f"\n=== Memory Leak Test for {writer_name} ===")
83
- print_memory("Initial memory")
84
-
85
- for i in range(num_iterations):
86
- filename = script_dir / "videos" / f"memory_test_{writer_name.lower()}_{i+1}.mp4"
87
-
88
- print_memory(f"Before iteration {i+1}")
89
-
90
- with writer_class(filename, framerate=30, overwrite=True) as writer:
91
- for frame in frames:
92
- writer.write(frame)
93
-
94
- print_memory(f"After iteration {i+1}")
95
-
96
- print_memory(f"Final memory after {num_iterations} iterations")
97
-
98
-
99
- def main():
100
- print("Video Writer Comparison Test")
101
- print("=" * 60)
102
-
103
- # Check FFmpeg version
104
- print("\nChecking FFmpeg version...")
105
- import subprocess
106
- try:
107
- result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True)
108
- ffmpeg_version = result.stdout.split('\n')[0]
109
- print(f"FFmpeg version: {ffmpeg_version}")
110
- except Exception as e:
111
- print(f"Error checking FFmpeg version: {e}")
112
-
113
- # Load the table tennis emoji image
114
- print("\nLoading table tennis emoji image...")
115
- image_path = script_dir / "table-tennis-emoji.png"
116
- if not os.path.exists(image_path):
117
- print(f"Error: Image not found at {image_path}")
118
- return
119
-
120
- base_image = npimage.load(image_path)
121
- print(f"Loaded image: {base_image.shape}, dtype: {base_image.dtype}")
122
-
123
- # Create animated frames
124
- print("\nCreating animated frames...")
125
- frames = create_animated_frames(base_image, num_frames=60, movement_range=30)
126
- print(f"Created {len(frames)} animated frames")
127
-
128
- # Test both video writers
129
- results = {}
130
-
131
- # Test FFmpegVideoWriter
132
- ffmpeg_filename = script_dir / "videos" / "table_tennis_ffmpeg.mp4"
133
- results['FFmpegVideoWriter'] = test_video_writer(
134
- FFmpegVideoWriter, ffmpeg_filename, frames, "FFmpegVideoWriter"
135
- )
136
-
137
- # Test AVVideoWriter
138
- av_filename = script_dir / "videos" / "table_tennis_av.mp4"
139
- results['AVVideoWriter'] = test_video_writer(
140
- AVVideoWriter, av_filename, frames, "AVVideoWriter"
141
- )
142
-
143
- # Memory leak tests
144
- print("\n" + "=" * 60)
145
- print("MEMORY LEAK TESTS")
146
- print("=" * 60)
147
-
148
- # Use a smaller set of frames for memory leak testing
149
- test_frames = frames[:30] # 30 frames for faster testing
150
-
151
- if results.get('FFmpegVideoWriter', False):
152
- test_memory_leak(FFmpegVideoWriter, test_frames, "FFmpegVideoWriter")
153
-
154
- if results.get('AVVideoWriter', False):
155
- test_memory_leak(AVVideoWriter, test_frames, "AVVideoWriter")
156
-
157
- # Summary
158
- print("\n" + "=" * 60)
159
- print("SUMMARY")
160
- print("=" * 60)
161
-
162
- for writer_name, success in results.items():
163
- status = "✓ PASSED" if success else "✗ FAILED"
164
- print(f"{writer_name}: {status}")
165
-
166
- print("\n🎉 Test completed!")
167
-
168
-
169
- if __name__ == "__main__":
170
- main()
File without changes
File without changes
File without changes