numpyimage 2.4.3__tar.gz → 2.5.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-2.4.3 → numpyimage-2.5.0}/PKG-INFO +4 -1
- {numpyimage-2.4.3 → numpyimage-2.5.0}/npimage/core.py +193 -86
- {numpyimage-2.4.3 → numpyimage-2.5.0}/numpyimage.egg-info/PKG-INFO +4 -1
- {numpyimage-2.4.3 → numpyimage-2.5.0}/numpyimage.egg-info/SOURCES.txt +2 -1
- {numpyimage-2.4.3 → numpyimage-2.5.0}/numpyimage.egg-info/requires.txt +4 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/pyproject.toml +7 -1
- numpyimage-2.5.0/tests/test_video.py +31 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/LICENSE +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/README.md +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/npimage/__init__.py +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/npimage/align.py +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/npimage/graphics.py +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/npimage/nrrd_utils.py +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/npimage/operations.py +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/npimage/utils.py +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/numpyimage.egg-info/dependency_links.txt +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/numpyimage.egg-info/top_level.txt +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/setup.cfg +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/tests/test_heic.py +0 -0
- {numpyimage-2.4.3 → numpyimage-2.5.0}/tests/test_pbm.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: numpyimage
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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: GNU GENERAL PUBLIC LICENSE
|
|
@@ -695,6 +695,9 @@ Requires-Dist: tifffile
|
|
|
695
695
|
Requires-Dist: pynrrd
|
|
696
696
|
Requires-Dist: matplotlib
|
|
697
697
|
Requires-Dist: opencv-python-headless
|
|
698
|
+
Provides-Extra: all
|
|
699
|
+
Requires-Dist: tqdm; extra == "all"
|
|
700
|
+
Requires-Dist: av; extra == "all"
|
|
698
701
|
Dynamic: license-file
|
|
699
702
|
|
|
700
703
|
# npimage
|
|
@@ -9,7 +9,7 @@ Function list:
|
|
|
9
9
|
- show(np_array) -> Displays a numpy array of pixel values as an image
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from typing import Literal
|
|
12
|
+
from typing import Literal, Union, Tuple, Iterator
|
|
13
13
|
import os
|
|
14
14
|
import glob
|
|
15
15
|
from builtins import open as builtin_open
|
|
@@ -24,7 +24,7 @@ supported_extensions = [
|
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def load(filename, dim_order='zyx', **kwargs):
|
|
27
|
+
def load(filename, dim_order='zyx', **kwargs) -> Union[np.ndarray, Tuple[np.ndarray, dict]]:
|
|
28
28
|
"""
|
|
29
29
|
Open a 2D or 3D image file and return its pixel values as a numpy array.
|
|
30
30
|
|
|
@@ -52,16 +52,16 @@ def load(filename, dim_order='zyx', **kwargs):
|
|
|
52
52
|
|
|
53
53
|
if extension in ['jpg', 'jpeg', 'png']:
|
|
54
54
|
from PIL import Image
|
|
55
|
-
data = np.array(Image.open(filename))
|
|
55
|
+
data = np.array(Image.open(filename)) # PIL.Image.open returns zyx order
|
|
56
56
|
|
|
57
57
|
if extension == 'heic':
|
|
58
58
|
_ensure_heif_opener_registered()
|
|
59
59
|
from PIL import Image
|
|
60
|
-
data = np.array(Image.open(filename))
|
|
60
|
+
data = np.array(Image.open(filename)) # PIL.Image.open returns zyx order
|
|
61
61
|
|
|
62
62
|
if extension in ['tif', 'tiff']:
|
|
63
63
|
import tifffile
|
|
64
|
-
data = tifffile.imread(filename)
|
|
64
|
+
data = tifffile.imread(filename) # tifffile.imread returns zyx order
|
|
65
65
|
|
|
66
66
|
if extension == 'pbm':
|
|
67
67
|
from .filetypes import pbm
|
|
@@ -151,7 +151,7 @@ def save(data,
|
|
|
151
151
|
units=None,
|
|
152
152
|
compress=None,
|
|
153
153
|
compression_level=3,
|
|
154
|
-
metadata=None):
|
|
154
|
+
metadata=None) -> None:
|
|
155
155
|
"""
|
|
156
156
|
Save a numpy array to file with a file type specified by the
|
|
157
157
|
filename extension.
|
|
@@ -357,12 +357,13 @@ def save(data,
|
|
|
357
357
|
|
|
358
358
|
vol[:] = data
|
|
359
359
|
|
|
360
|
-
|
|
361
360
|
write = save # Function name alias
|
|
362
361
|
to_file = save # Function name alias
|
|
363
362
|
|
|
364
363
|
|
|
365
|
-
def load_video(filename,
|
|
364
|
+
def load_video(filename,
|
|
365
|
+
return_framerate=False,
|
|
366
|
+
progress_bar=True) -> Union[np.ndarray, Tuple[np.ndarray, float]]:
|
|
366
367
|
"""
|
|
367
368
|
Load all images in a video file as a numpy array.
|
|
368
369
|
|
|
@@ -370,53 +371,55 @@ def load_video(filename, force_rgb=False, progress_bar=True):
|
|
|
370
371
|
----------
|
|
371
372
|
filename : str
|
|
372
373
|
Path to the video file
|
|
373
|
-
|
|
374
|
-
If True,
|
|
375
|
-
If False, return frames in their native format (e.g., grayscale if possible)
|
|
374
|
+
return_framerate : bool, default False
|
|
375
|
+
If True, return the frame rate of the video
|
|
376
376
|
progress_bar : bool, default True
|
|
377
377
|
If True, display a progress bar
|
|
378
378
|
|
|
379
379
|
Returns
|
|
380
380
|
-------
|
|
381
381
|
data : numpy.ndarray
|
|
382
|
-
The video frames as a numpy array, shape (num_frames, height, width
|
|
382
|
+
The video frames as a numpy array, shape (num_frames, height, width, colors)
|
|
383
383
|
"""
|
|
384
384
|
try:
|
|
385
385
|
import av
|
|
386
|
+
from tqdm import tqdm
|
|
386
387
|
except ImportError:
|
|
387
|
-
raise ImportError('
|
|
388
|
-
|
|
388
|
+
raise ImportError('Missing optional dependency for video processing,'
|
|
389
|
+
' run `pip install av tqdm`')
|
|
390
|
+
|
|
389
391
|
container = av.open(filename)
|
|
390
392
|
stream = container.streams.video[0]
|
|
391
393
|
num_frames = stream.frames
|
|
392
394
|
if not num_frames or num_frames == 0:
|
|
393
395
|
# If we don't know the number of frames, we can't preallocate, so
|
|
394
|
-
# it's hard to do better than the following approach
|
|
395
|
-
# uses double the amount of RAM
|
|
396
|
-
|
|
396
|
+
# it's hard to do better than the following approach which temporarily
|
|
397
|
+
# uses double the amount of RAM compared to the preallocated approach.
|
|
398
|
+
data = np.array(list(lazy_load_video(filename)))
|
|
399
|
+
if return_framerate:
|
|
400
|
+
return data, float(stream.average_rate)
|
|
401
|
+
else:
|
|
402
|
+
return data
|
|
397
403
|
else:
|
|
398
404
|
# Load first image to get shape and dtype
|
|
399
405
|
frame_iter = container.decode(stream)
|
|
400
406
|
first_frame = next(frame_iter)
|
|
401
|
-
|
|
402
|
-
first_img = first_frame.to_ndarray(format='rgb24')
|
|
403
|
-
else:
|
|
404
|
-
first_img = first_frame.to_ndarray()
|
|
407
|
+
first_img = first_frame.to_ndarray(format='rgb24')
|
|
405
408
|
# Preallocate memory for the entire array
|
|
406
409
|
data = np.empty((num_frames, *first_img.shape), dtype=first_img.dtype)
|
|
407
410
|
# Then fill it up frame by frame
|
|
408
411
|
data[0] = first_img
|
|
409
412
|
for i, frame in tqdm(enumerate(frame_iter, start=1), total=num_frames,
|
|
410
413
|
desc='Loading video', disable=not progress_bar):
|
|
411
|
-
|
|
412
|
-
img = frame.to_ndarray(format='rgb24')
|
|
413
|
-
else:
|
|
414
|
-
img = frame.to_ndarray()
|
|
414
|
+
img = frame.to_ndarray(format='rgb24')
|
|
415
415
|
data[i] = img
|
|
416
|
-
|
|
416
|
+
if return_framerate:
|
|
417
|
+
return data, float(stream.average_rate)
|
|
418
|
+
else:
|
|
419
|
+
return data
|
|
417
420
|
|
|
418
421
|
|
|
419
|
-
def lazy_load_video(filename
|
|
422
|
+
def lazy_load_video(filename) -> Iterator[np.ndarray]:
|
|
420
423
|
"""
|
|
421
424
|
Lazily load video frames as numpy arrays using PyAV.
|
|
422
425
|
|
|
@@ -424,31 +427,115 @@ def lazy_load_video(filename, force_rgb=False):
|
|
|
424
427
|
----------
|
|
425
428
|
filename : str
|
|
426
429
|
Path to the video file.
|
|
427
|
-
force_rgb : bool, default False
|
|
428
|
-
If True, always return frames as RGB arrays (shape=(H, W, 3)).
|
|
429
|
-
If False, return frames in their native format (e.g., grayscale if possible).
|
|
430
430
|
|
|
431
431
|
Yields
|
|
432
432
|
------
|
|
433
433
|
frame : np.ndarray
|
|
434
|
-
Video frame as a numpy array, shape (height, width
|
|
434
|
+
Video frame as a numpy array, shape (height, width, colors).
|
|
435
435
|
"""
|
|
436
436
|
try:
|
|
437
437
|
import av
|
|
438
438
|
except ImportError:
|
|
439
|
-
raise ImportError('
|
|
439
|
+
raise ImportError('Missing optional dependency for video processing,'
|
|
440
|
+
' run `pip install av tqdm`')
|
|
440
441
|
container = av.open(filename)
|
|
441
442
|
stream = container.streams.video[0]
|
|
442
443
|
for frame in container.decode(stream):
|
|
443
|
-
|
|
444
|
-
img = frame.to_ndarray(format='rgb24')
|
|
445
|
-
else:
|
|
446
|
-
img = frame.to_ndarray()
|
|
444
|
+
img = frame.to_ndarray(format='rgb24')
|
|
447
445
|
yield img
|
|
448
446
|
|
|
449
447
|
|
|
448
|
+
codec_aliases = {
|
|
449
|
+
"libx264": "libx264",
|
|
450
|
+
"avc1": "libx264",
|
|
451
|
+
"h264": "libx264",
|
|
452
|
+
"H.264": "libx264",
|
|
453
|
+
"libx265": "libx265",
|
|
454
|
+
"hevc": "libx265",
|
|
455
|
+
"hvc1": "libx265",
|
|
456
|
+
"hev1": "libx265",
|
|
457
|
+
"h265": "libx265",
|
|
458
|
+
"H.265": "libx265",
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def video_writer(filename, framerate=30, crf=23, compression_speed='medium',
|
|
463
|
+
codec: Literal['libx264', 'libx265'] = 'libx264') -> 'VideoWriter':
|
|
464
|
+
"""
|
|
465
|
+
Create a video writer object for saving frames to a video file.
|
|
466
|
+
|
|
467
|
+
Parameters
|
|
468
|
+
----------
|
|
469
|
+
filename : str
|
|
470
|
+
The filename to save the video to.
|
|
471
|
+
framerate : int, default 30
|
|
472
|
+
The frame rate of the video.
|
|
473
|
+
crf : int, default 23
|
|
474
|
+
Constant Rate Factor for encoding quality (lower is better quality).
|
|
475
|
+
compression_speed : str, default 'medium'
|
|
476
|
+
Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
|
|
477
|
+
'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
|
|
478
|
+
codec : Literal['libx264', 'libx265'], default 'libx264'
|
|
479
|
+
The video codec to use for encoding. Can be any of a number of aliases for
|
|
480
|
+
these two codecs, including avc1/h264 vs hevc/hvc1/hev1/h265.
|
|
481
|
+
|
|
482
|
+
Returns
|
|
483
|
+
-------
|
|
484
|
+
VideoWriter
|
|
485
|
+
The video writer object supporting context manager protocol.
|
|
486
|
+
"""
|
|
487
|
+
try:
|
|
488
|
+
import av
|
|
489
|
+
except ImportError:
|
|
490
|
+
raise ImportError('Missing optional dependency for video processing,'
|
|
491
|
+
' run `pip install av tqdm`')
|
|
492
|
+
|
|
493
|
+
class VideoWriter:
|
|
494
|
+
def __init__(self, filename, framerate, crf, compression_speed, codec):
|
|
495
|
+
self.filename = filename
|
|
496
|
+
self.framerate = framerate
|
|
497
|
+
self.crf = crf
|
|
498
|
+
self.compression_speed = compression_speed
|
|
499
|
+
self.codec = codec_aliases[codec]
|
|
500
|
+
self.container = av.open(filename, mode='w')
|
|
501
|
+
self.stream = self.container.add_stream(codec, rate=framerate)
|
|
502
|
+
self.stream.pix_fmt = 'yuv420p'
|
|
503
|
+
self.stream.options = {'crf': str(crf), 'preset': compression_speed}
|
|
504
|
+
self._closed = False
|
|
505
|
+
self.stream.width = 0
|
|
506
|
+
self.stream.height = 0
|
|
507
|
+
|
|
508
|
+
def write(self, frame):
|
|
509
|
+
if not isinstance(frame, av.VideoFrame):
|
|
510
|
+
if frame.ndim == 3 and frame.shape[-1] == 3:
|
|
511
|
+
frame = av.VideoFrame.from_ndarray(frame, format='rgb24')
|
|
512
|
+
elif frame.ndim == 2:
|
|
513
|
+
frame = av.VideoFrame.from_ndarray(frame, format='gray')
|
|
514
|
+
else:
|
|
515
|
+
raise ValueError(f'Frame must be (H, W) or (H, W, 3) but was {frame.shape}')
|
|
516
|
+
if self.stream.width == 0:
|
|
517
|
+
self.stream.width = frame.width
|
|
518
|
+
if self.stream.height == 0:
|
|
519
|
+
self.stream.height = frame.height
|
|
520
|
+
for packet in self.stream.encode(frame):
|
|
521
|
+
self.container.mux(packet)
|
|
522
|
+
|
|
523
|
+
def __enter__(self):
|
|
524
|
+
return self
|
|
525
|
+
|
|
526
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
527
|
+
# Flush stream
|
|
528
|
+
for packet in self.stream.encode():
|
|
529
|
+
self.container.mux(packet)
|
|
530
|
+
self.container.close()
|
|
531
|
+
self._closed = True
|
|
532
|
+
|
|
533
|
+
return VideoWriter(filename, framerate, crf, compression_speed, codec)
|
|
534
|
+
|
|
535
|
+
|
|
450
536
|
def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
|
|
451
|
-
dim_order='yx', framerate=30, crf=23, compression_speed='medium'
|
|
537
|
+
dim_order='yx', framerate=30, crf=23, compression_speed='medium',
|
|
538
|
+
progress_bar=True, codec: Literal['libx264', 'libx265'] = 'libx264') -> None:
|
|
452
539
|
"""
|
|
453
540
|
Save a 3D numpy array of greyscale values OR a 4D numpy array of RGB values as a video
|
|
454
541
|
|
|
@@ -467,23 +554,57 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
|
|
|
467
554
|
The axis of the data array that will be played as time in the video.
|
|
468
555
|
|
|
469
556
|
color_axis : int or None, default None
|
|
470
|
-
If not None, specifies the axis of the color channels (e.g., -1 for last axis,
|
|
471
|
-
|
|
557
|
+
If not None, specifies the axis of the color channels (e.g., -1 for last axis,
|
|
558
|
+
1 for second axis).
|
|
559
|
+
If None, data must be 3D (greyscale) or 4D with one length 3 axis (RGB).
|
|
472
560
|
|
|
473
561
|
overwrite : bool, default False
|
|
474
562
|
Whether to overwrite the file if it already exists.
|
|
475
563
|
|
|
476
564
|
dim_order : 'yx' (default) or 'xy'
|
|
477
565
|
The order of the spatial dimensions in the input data.
|
|
566
|
+
|
|
567
|
+
framerate : int, default 30
|
|
568
|
+
The frame rate of the video.
|
|
569
|
+
|
|
570
|
+
crf : int, default 23
|
|
571
|
+
Constant Rate Factor that specifies amount of lossiness allowed in compression.
|
|
572
|
+
Lower values produce better quality, larger videos. crf=17 has no human-visible
|
|
573
|
+
compression artifacts. File size approximately doubles/halves each time you
|
|
574
|
+
add/subtract 6 from crf, so crf=17 produces files about twice as large as
|
|
575
|
+
the default crf=23.
|
|
576
|
+
|
|
577
|
+
compression_speed : str, default 'medium'
|
|
578
|
+
Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
|
|
579
|
+
'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
|
|
580
|
+
|
|
581
|
+
progress_bar : bool, default True
|
|
582
|
+
If True, display a progress bar.
|
|
583
|
+
|
|
584
|
+
codec : Literal['libx264', 'libx265'], default 'libx264'
|
|
585
|
+
The video codec to use for encoding. Can be any of a number of aliases for
|
|
586
|
+
these two codecs, including avc1/h264 vs hevc/hvc1/hev1/h265.
|
|
478
587
|
"""
|
|
479
588
|
try:
|
|
480
|
-
import
|
|
589
|
+
from tqdm import tqdm
|
|
481
590
|
except ImportError:
|
|
482
|
-
raise ImportError('
|
|
591
|
+
raise ImportError('Missing optional dependency for video processing,'
|
|
592
|
+
' run `pip install av tqdm`')
|
|
593
|
+
|
|
594
|
+
filename = os.path.expanduser(str(filename))
|
|
595
|
+
if filename.split('.')[-1].lower() not in ['mp4', 'mkv', 'avi', 'mov']:
|
|
596
|
+
filename += '.mp4'
|
|
597
|
+
if os.path.exists(filename) and not overwrite:
|
|
598
|
+
raise FileExistsError(f'File {filename} already exists. '
|
|
599
|
+
'Set overwrite=True to overwrite.')
|
|
483
600
|
|
|
601
|
+
if color_axis is None and data.ndim == 4:
|
|
602
|
+
color_axis = find_channel_axis(data, possible_channel_lengths=3)
|
|
603
|
+
if color_axis is None:
|
|
604
|
+
raise ValueError('4D input data must have an RGB (length 3) axis.')
|
|
484
605
|
if color_axis is not None:
|
|
485
606
|
if data.ndim != 4:
|
|
486
|
-
raise ValueError('Input data must be
|
|
607
|
+
raise ValueError('Input data must be 4D when color_axis is specified.')
|
|
487
608
|
# Move time axis to 0, color axis to -1
|
|
488
609
|
data = np.moveaxis(data, time_axis, 0)
|
|
489
610
|
if color_axis != -1:
|
|
@@ -493,22 +614,16 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
|
|
|
493
614
|
n_frames = data.shape[0]
|
|
494
615
|
height, width, channels = data.shape[1:]
|
|
495
616
|
if channels != 3:
|
|
496
|
-
raise ValueError('Color video must have 3 channels (RGB).')
|
|
617
|
+
raise ValueError(f'Color video must have 3 channels (RGB) but had {channels}.')
|
|
497
618
|
else:
|
|
498
619
|
if data.ndim != 3:
|
|
499
|
-
raise ValueError('Input data must be
|
|
620
|
+
raise ValueError('Input data must be 3D when color_axis is not specified.')
|
|
500
621
|
data = np.moveaxis(data, time_axis, 0)
|
|
501
622
|
if 'xy' in dim_order:
|
|
502
623
|
data = data.swapaxes(1, 2)
|
|
503
624
|
n_frames = data.shape[0]
|
|
504
625
|
height, width = data.shape[1:]
|
|
505
626
|
|
|
506
|
-
filename = os.path.expanduser(str(filename))
|
|
507
|
-
if filename.split('.')[-1].lower() not in ['mp4', 'mkv', 'avi', 'mov']:
|
|
508
|
-
filename += '.mp4'
|
|
509
|
-
if os.path.exists(filename) and not overwrite:
|
|
510
|
-
raise FileExistsError(f'File {filename} already exists. '
|
|
511
|
-
'Set overwrite=True to overwrite.')
|
|
512
627
|
extension = filename.split('.')[-1].lower()
|
|
513
628
|
if extension == 'mp4':
|
|
514
629
|
pad = [[0, 0], [0, 0], [0, 0]]
|
|
@@ -519,25 +634,11 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
|
|
|
519
634
|
if pad != [[0, 0], [0, 0], [0, 0]]:
|
|
520
635
|
data = np.pad(data, pad, mode='edge')
|
|
521
636
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
stream.width = data.shape[2]
|
|
528
|
-
|
|
529
|
-
for frame_i in range(n_frames):
|
|
530
|
-
if color_axis is not None:
|
|
531
|
-
frame = av.VideoFrame.from_ndarray(data[frame_i], format='rgb24')
|
|
532
|
-
else:
|
|
533
|
-
frame = av.VideoFrame.from_ndarray(data[frame_i], format='gray')
|
|
534
|
-
for packet in stream.encode(frame):
|
|
535
|
-
container.mux(packet)
|
|
536
|
-
|
|
537
|
-
for packet in stream.encode():
|
|
538
|
-
container.mux(packet)
|
|
539
|
-
|
|
540
|
-
container.close()
|
|
637
|
+
with video_writer(filename, framerate=framerate, crf=crf,
|
|
638
|
+
compression_speed=compression_speed, codec=codec) as writer:
|
|
639
|
+
for frame_i in tqdm(range(n_frames), total=n_frames,
|
|
640
|
+
desc='Saving video', disable=not progress_bar):
|
|
641
|
+
writer.write(data[frame_i])
|
|
541
642
|
|
|
542
643
|
|
|
543
644
|
def show(data,
|
|
@@ -546,7 +647,7 @@ def show(data,
|
|
|
546
647
|
mode: Literal['PIL', 'mpl'] = 'PIL',
|
|
547
648
|
convert_to_8bit=True,
|
|
548
649
|
channel_axis='guess',
|
|
549
|
-
**kwargs):
|
|
650
|
+
**kwargs) -> None:
|
|
550
651
|
"""
|
|
551
652
|
Display a numpy array of pixel values as an image. Supported types:
|
|
552
653
|
1-channel (grayscale) : data.shape must be (y, x)
|
|
@@ -610,7 +711,9 @@ def show(data,
|
|
|
610
711
|
imshow = show # Function name alias
|
|
611
712
|
|
|
612
713
|
|
|
613
|
-
def find_channel_axis(data,
|
|
714
|
+
def find_channel_axis(data,
|
|
715
|
+
possible_channel_axes=[-1, 0],
|
|
716
|
+
possible_channel_lengths=[2, 3, 4]) -> Union[int, None]:
|
|
614
717
|
"""
|
|
615
718
|
If the given numpy array has a shape suggesting that it has a
|
|
616
719
|
channel (color) axis (that is, any axis with length 2 (2-color),
|
|
@@ -621,23 +724,28 @@ def find_channel_axis(data, expected_channel_axis=[0, -1]):
|
|
|
621
724
|
data : numpy.ndarray
|
|
622
725
|
The numpy array to check for a channel axis.
|
|
623
726
|
|
|
624
|
-
|
|
727
|
+
possible_channel_axes : int or list of int, default [0, -1]
|
|
625
728
|
If None, any axis having length 2, 3, or 4 will be considered
|
|
626
729
|
a channel axis.
|
|
627
730
|
If an int, only that axis index will be checked.
|
|
628
731
|
If a list of ints, all axes with those indices will be checked.
|
|
629
|
-
The default value of [
|
|
732
|
+
The default value of [-1, 0] checks the last and first axes, which is
|
|
630
733
|
almost always where a channel axis will be found.
|
|
631
734
|
|
|
735
|
+
possible_channel_lengths : int or list of int, default [2, 3, 4]
|
|
736
|
+
If an int, only that length will be considered a channel axis.
|
|
737
|
+
If a list of ints, an axis with any of those lengths will be considered
|
|
738
|
+
a channel axis.
|
|
739
|
+
|
|
632
740
|
Returns
|
|
633
741
|
-------
|
|
634
742
|
int or None
|
|
635
743
|
The index of the channel axis, or None if no channel axis was found.
|
|
636
744
|
|
|
637
|
-
If
|
|
638
|
-
the
|
|
745
|
+
If possible_channel_axes is given, the returned value will be one of
|
|
746
|
+
the possible_channel_axes values, or None if no channel axis was found.
|
|
639
747
|
|
|
640
|
-
If
|
|
748
|
+
If possible_channel_axes is None, the returned value will be between
|
|
641
749
|
0 and data.ndim - 1, inclusive, or None if no channel axis was found.
|
|
642
750
|
|
|
643
751
|
Note that returning 0 means the channel axis was found and is the first
|
|
@@ -645,25 +753,24 @@ def find_channel_axis(data, expected_channel_axis=[0, -1]):
|
|
|
645
753
|
because 0 will evaluate to False even though the data has a channel axis.
|
|
646
754
|
Instead write `if find_channel_axis(data) is not None:`
|
|
647
755
|
"""
|
|
648
|
-
if isinstance(
|
|
649
|
-
|
|
650
|
-
if
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
756
|
+
if isinstance(possible_channel_axes, int):
|
|
757
|
+
possible_channel_axes = [possible_channel_axes]
|
|
758
|
+
if possible_channel_axes is None:
|
|
759
|
+
possible_channel_axes = range(data.ndim)
|
|
760
|
+
if isinstance(possible_channel_lengths, int):
|
|
761
|
+
possible_channel_lengths = [possible_channel_lengths]
|
|
762
|
+
for axis in possible_channel_axes:
|
|
763
|
+
if data.shape[axis] in possible_channel_lengths:
|
|
654
764
|
return axis
|
|
655
765
|
return None
|
|
656
766
|
|
|
657
767
|
|
|
658
768
|
# Flag to track if HEIF opener has been registered
|
|
659
769
|
_heif_opener_registered = False
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
def _ensure_heif_opener_registered():
|
|
770
|
+
def _ensure_heif_opener_registered() -> None:
|
|
663
771
|
"""Register the HEIF opener if not already registered."""
|
|
664
772
|
global _heif_opener_registered
|
|
665
773
|
if not _heif_opener_registered:
|
|
666
774
|
from pillow_heif import register_heif_opener
|
|
667
775
|
register_heif_opener()
|
|
668
776
|
_heif_opener_registered = True
|
|
669
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: numpyimage
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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: GNU GENERAL PUBLIC LICENSE
|
|
@@ -695,6 +695,9 @@ Requires-Dist: tifffile
|
|
|
695
695
|
Requires-Dist: pynrrd
|
|
696
696
|
Requires-Dist: matplotlib
|
|
697
697
|
Requires-Dist: opencv-python-headless
|
|
698
|
+
Provides-Extra: all
|
|
699
|
+
Requires-Dist: tqdm; extra == "all"
|
|
700
|
+
Requires-Dist: av; extra == "all"
|
|
698
701
|
Dynamic: license-file
|
|
699
702
|
|
|
700
703
|
# npimage
|
|
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = 'numpyimage'
|
|
7
|
-
version = '2.
|
|
7
|
+
version = '2.5.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'
|
|
@@ -30,5 +30,11 @@ dependencies = [
|
|
|
30
30
|
'opencv-python-headless',
|
|
31
31
|
]
|
|
32
32
|
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
all = [
|
|
35
|
+
"tqdm",
|
|
36
|
+
"av"
|
|
37
|
+
]
|
|
38
|
+
|
|
33
39
|
[tool.setuptools.packages.find]
|
|
34
40
|
include = ['npimage']
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import npimage
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_save_and_load_video(tmp_path=Path.cwd()):
|
|
9
|
+
# Create synthetic video data
|
|
10
|
+
data = np.full((120, 128, 256, 3), 255, dtype=np.uint8)
|
|
11
|
+
for i in range(120):
|
|
12
|
+
# Zero out the green (channel 1) channel along a moving line
|
|
13
|
+
npimage.drawline(
|
|
14
|
+
data[i],
|
|
15
|
+
pt1=(10 + i / 4, 10 + i, 1),
|
|
16
|
+
pt2=(90 + i / 4, 128 + i, 1),
|
|
17
|
+
value=0,
|
|
18
|
+
thickness=(3, 3, 1) # Thickness 1 along the color axis
|
|
19
|
+
)
|
|
20
|
+
video_path = tmp_path / 'test_video.mp4'
|
|
21
|
+
npimage.save_video(data, str(video_path), crf=15, overwrite=True)
|
|
22
|
+
loaded = npimage.load_video(str(video_path), progress_bar=False)
|
|
23
|
+
# Check shape
|
|
24
|
+
assert loaded.shape == data.shape
|
|
25
|
+
# Allow for some difference due to lossy encoding/decoding
|
|
26
|
+
assert np.allclose(loaded.mean(axis=-1), data.mean(axis=-1), atol=64)
|
|
27
|
+
print('test_save_and_load_video PASSED')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == '__main__':
|
|
31
|
+
test_save_and_load_video()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|