numpyimage 2.6.0__tar.gz → 2.6.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.
- {numpyimage-2.6.0 → numpyimage-2.6.1}/PKG-INFO +5 -4
- {numpyimage-2.6.0 → numpyimage-2.6.1}/README.md +4 -3
- {numpyimage-2.6.0 → numpyimage-2.6.1}/npimage/__init__.py +3 -1
- {numpyimage-2.6.0 → numpyimage-2.6.1}/npimage/graphics.py +1 -1
- numpyimage-2.6.0/npimage/core.py → numpyimage-2.6.1/npimage/imageio.py +1 -282
- numpyimage-2.6.1/npimage/vidio.py +489 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/numpyimage.egg-info/PKG-INFO +5 -4
- {numpyimage-2.6.0 → numpyimage-2.6.1}/numpyimage.egg-info/SOURCES.txt +2 -1
- {numpyimage-2.6.0 → numpyimage-2.6.1}/pyproject.toml +1 -1
- {numpyimage-2.6.0 → numpyimage-2.6.1}/LICENSE +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/npimage/align.py +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/npimage/nrrd_utils.py +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/npimage/operations.py +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/npimage/utils.py +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/numpyimage.egg-info/dependency_links.txt +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/numpyimage.egg-info/requires.txt +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/numpyimage.egg-info/top_level.txt +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/setup.cfg +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/tests/test_heic.py +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/tests/test_pbm.py +0 -0
- {numpyimage-2.6.0 → numpyimage-2.6.1}/tests/test_video.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: numpyimage
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.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: GNU GENERAL PUBLIC LICENSE
|
|
@@ -701,18 +701,19 @@ Requires-Dist: av; extra == "all"
|
|
|
701
701
|
Dynamic: license-file
|
|
702
702
|
|
|
703
703
|
# npimage
|
|
704
|
-
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 `
|
|
704
|
+
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/).)
|
|
705
705
|
|
|
706
706
|
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!)
|
|
707
707
|
|
|
708
708
|
|
|
709
709
|
### Documentation
|
|
710
|
-
- `
|
|
710
|
+
- `imageio.py`: load, save, or show images.
|
|
711
|
+
- `vidio.py`: load or save videos.
|
|
711
712
|
- `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
|
|
712
713
|
- `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
|
|
713
714
|
- `operations.py`: perform operations on images.
|
|
714
715
|
|
|
715
|
-
|
|
716
|
+
Check each function's docstring for more details.
|
|
716
717
|
|
|
717
718
|
|
|
718
719
|
### Installation
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# npimage
|
|
2
|
-
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 `
|
|
2
|
+
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
3
|
|
|
4
4
|
Want to draw simple shapes like lines, triangles, and circles into 3D numpy arrays? Frustrated that the python libraries you can find online like `opencv` and `skimage.draw` work on 2D arrays but not 3D? I wrote some functions in `graphics.py` that do the trick in 3D. (If you know of another library that can do this, please let me know!)
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
### Documentation
|
|
8
|
-
- `
|
|
8
|
+
- `imageio.py`: load, save, or show images.
|
|
9
|
+
- `vidio.py`: load or save videos.
|
|
9
10
|
- `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
|
|
10
11
|
- `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
|
|
11
12
|
- `operations.py`: perform operations on images.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
Check each function's docstring for more details.
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
### Installation
|
|
@@ -686,7 +686,7 @@ def floodfill(image, seed, fill_value, fill_diagonally=False,
|
|
|
686
686
|
|
|
687
687
|
if debug_frequency is not None and iteration % debug_frequency == 0:
|
|
688
688
|
print(f'Writing iteration{iteration}.nrrd')
|
|
689
|
-
from .
|
|
689
|
+
from .imageio import save
|
|
690
690
|
save(image, f'iteration{iteration}.nrrd')
|
|
691
691
|
|
|
692
692
|
#recurseifneighbors
|
|
@@ -5,11 +5,10 @@ Functions for reading, writing, and showing images.
|
|
|
5
5
|
Function list:
|
|
6
6
|
- load(filename) -> numpy.ndarray
|
|
7
7
|
- save(data, filename) -> Saves a numpy array as an nD image file
|
|
8
|
-
- save_video(data, filename) -> Saves a 3D numpy array as a video
|
|
9
8
|
- show(np_array) -> Displays a numpy array of pixel values as an image
|
|
10
9
|
"""
|
|
11
10
|
|
|
12
|
-
from typing import Literal, Union, Tuple
|
|
11
|
+
from typing import Literal, Union, Tuple
|
|
13
12
|
import os
|
|
14
13
|
import glob
|
|
15
14
|
from builtins import open as builtin_open
|
|
@@ -361,286 +360,6 @@ write = save # Function name alias
|
|
|
361
360
|
to_file = save # Function name alias
|
|
362
361
|
|
|
363
362
|
|
|
364
|
-
def load_video(filename,
|
|
365
|
-
return_framerate=False,
|
|
366
|
-
progress_bar=True) -> Union[np.ndarray, Tuple[np.ndarray, float]]:
|
|
367
|
-
"""
|
|
368
|
-
Load all images in a video file as a numpy array.
|
|
369
|
-
|
|
370
|
-
Parameters
|
|
371
|
-
----------
|
|
372
|
-
filename : str
|
|
373
|
-
Path to the video file
|
|
374
|
-
return_framerate : bool, default False
|
|
375
|
-
If True, return the frame rate of the video
|
|
376
|
-
progress_bar : bool, default True
|
|
377
|
-
If True, display a progress bar
|
|
378
|
-
|
|
379
|
-
Returns
|
|
380
|
-
-------
|
|
381
|
-
data : numpy.ndarray
|
|
382
|
-
The video frames as a numpy array, shape (num_frames, height, width, colors)
|
|
383
|
-
"""
|
|
384
|
-
try:
|
|
385
|
-
import av
|
|
386
|
-
from tqdm import tqdm
|
|
387
|
-
except ImportError:
|
|
388
|
-
raise ImportError('Missing optional dependency for video processing,'
|
|
389
|
-
' run `pip install av tqdm`')
|
|
390
|
-
|
|
391
|
-
container = av.open(filename)
|
|
392
|
-
stream = container.streams.video[0]
|
|
393
|
-
num_frames = stream.frames
|
|
394
|
-
if not num_frames or num_frames == 0:
|
|
395
|
-
# If we don't know the number of frames, we can't preallocate, so
|
|
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
|
|
403
|
-
else:
|
|
404
|
-
# Load first image to get shape and dtype
|
|
405
|
-
frame_iter = container.decode(stream)
|
|
406
|
-
first_frame = next(frame_iter)
|
|
407
|
-
first_img = first_frame.to_ndarray(format='rgb24')
|
|
408
|
-
# Preallocate memory for the entire array
|
|
409
|
-
data = np.empty((num_frames, *first_img.shape), dtype=first_img.dtype)
|
|
410
|
-
# Then fill it up frame by frame
|
|
411
|
-
data[0] = first_img
|
|
412
|
-
for i, frame in tqdm(enumerate(frame_iter), total=num_frames,
|
|
413
|
-
desc='Loading video', disable=not progress_bar):
|
|
414
|
-
if i == 0:
|
|
415
|
-
continue
|
|
416
|
-
img = frame.to_ndarray(format='rgb24')
|
|
417
|
-
data[i] = img
|
|
418
|
-
if return_framerate:
|
|
419
|
-
return data, float(stream.average_rate)
|
|
420
|
-
else:
|
|
421
|
-
return data
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
def lazy_load_video(filename) -> Iterator[np.ndarray]:
|
|
425
|
-
"""
|
|
426
|
-
Lazily load video frames as numpy arrays using PyAV.
|
|
427
|
-
|
|
428
|
-
Parameters
|
|
429
|
-
----------
|
|
430
|
-
filename : str
|
|
431
|
-
Path to the video file.
|
|
432
|
-
|
|
433
|
-
Yields
|
|
434
|
-
------
|
|
435
|
-
frame : np.ndarray
|
|
436
|
-
Video frame as a numpy array, shape (height, width, colors).
|
|
437
|
-
"""
|
|
438
|
-
try:
|
|
439
|
-
import av
|
|
440
|
-
except ImportError:
|
|
441
|
-
raise ImportError('Missing optional dependency for video processing,'
|
|
442
|
-
' run `pip install av tqdm`')
|
|
443
|
-
container = av.open(filename)
|
|
444
|
-
stream = container.streams.video[0]
|
|
445
|
-
for frame in container.decode(stream):
|
|
446
|
-
img = frame.to_ndarray(format='rgb24')
|
|
447
|
-
yield img
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
codec_aliases = {
|
|
451
|
-
"libx264": "libx264",
|
|
452
|
-
"avc1": "libx264",
|
|
453
|
-
"h264": "libx264",
|
|
454
|
-
"H.264": "libx264",
|
|
455
|
-
"libx265": "libx265",
|
|
456
|
-
"hevc": "libx265",
|
|
457
|
-
"hvc1": "libx265",
|
|
458
|
-
"hev1": "libx265",
|
|
459
|
-
"h265": "libx265",
|
|
460
|
-
"H.265": "libx265",
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
class VideoWriter:
|
|
465
|
-
"""
|
|
466
|
-
Create a video writer object for saving frames to a video file.
|
|
467
|
-
|
|
468
|
-
Parameters
|
|
469
|
-
----------
|
|
470
|
-
filename : str
|
|
471
|
-
The filename to save the video to.
|
|
472
|
-
framerate : int, default 30
|
|
473
|
-
The frame rate of the video.
|
|
474
|
-
crf : int, default 23
|
|
475
|
-
Constant Rate Factor for encoding quality (lower is better quality).
|
|
476
|
-
compression_speed : str, default 'medium'
|
|
477
|
-
Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
|
|
478
|
-
'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
|
|
479
|
-
codec : Literal['libx264', 'libx265'], default 'libx264'
|
|
480
|
-
The video codec to use for encoding. Can be any of a number of aliases for
|
|
481
|
-
these two codecs, including avc1/h264 vs hevc/hvc1/hev1/h265.
|
|
482
|
-
"""
|
|
483
|
-
def __init__(self, filename, framerate=30, crf=23, compression_speed='medium',
|
|
484
|
-
codec: Literal['libx264', 'libx265'] = 'libx264',
|
|
485
|
-
overwrite=False):
|
|
486
|
-
try:
|
|
487
|
-
import av
|
|
488
|
-
except ImportError:
|
|
489
|
-
raise ImportError('Missing optional dependency for video processing,'
|
|
490
|
-
' run `pip install av tqdm`')
|
|
491
|
-
self.av = av
|
|
492
|
-
filename = os.path.expanduser(str(filename))
|
|
493
|
-
if os.path.exists(filename) and not overwrite:
|
|
494
|
-
raise FileExistsError(f'File {filename} already exists. '
|
|
495
|
-
'Set overwrite=True to overwrite.')
|
|
496
|
-
self.filename = filename
|
|
497
|
-
self.framerate = framerate
|
|
498
|
-
self.crf = crf
|
|
499
|
-
self.compression_speed = compression_speed
|
|
500
|
-
self.codec = codec_aliases[codec.lower()]
|
|
501
|
-
self.container = av.open(filename, mode='w')
|
|
502
|
-
self.stream = self.container.add_stream(self.codec, rate=framerate)
|
|
503
|
-
self.stream.pix_fmt = 'yuv420p'
|
|
504
|
-
self.stream.options = {'crf': str(crf), 'preset': compression_speed}
|
|
505
|
-
self._closed = False
|
|
506
|
-
self.stream.width = 0
|
|
507
|
-
self.stream.height = 0
|
|
508
|
-
|
|
509
|
-
def write(self, frame):
|
|
510
|
-
if not isinstance(frame, self.av.VideoFrame):
|
|
511
|
-
if frame.ndim == 3 and frame.shape[-1] == 3:
|
|
512
|
-
frame = self.av.VideoFrame.from_ndarray(frame, format='rgb24')
|
|
513
|
-
elif frame.ndim == 2:
|
|
514
|
-
frame = self.av.VideoFrame.from_ndarray(frame, format='gray')
|
|
515
|
-
else:
|
|
516
|
-
raise ValueError(f'Frame must be (H, W) or (H, W, 3) but was {frame.shape}')
|
|
517
|
-
if self.stream.width == 0:
|
|
518
|
-
self.stream.width = frame.width
|
|
519
|
-
if self.stream.height == 0:
|
|
520
|
-
self.stream.height = frame.height
|
|
521
|
-
for packet in self.stream.encode(frame):
|
|
522
|
-
self.container.mux(packet)
|
|
523
|
-
|
|
524
|
-
def __enter__(self):
|
|
525
|
-
return self
|
|
526
|
-
|
|
527
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
528
|
-
# Flush stream
|
|
529
|
-
for packet in self.stream.encode():
|
|
530
|
-
self.container.mux(packet)
|
|
531
|
-
self.container.close()
|
|
532
|
-
self._closed = True
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
|
|
536
|
-
dim_order='yx', framerate=30, crf=23, compression_speed='medium',
|
|
537
|
-
progress_bar=True, codec: Literal['libx264', 'libx265'] = 'libx264') -> None:
|
|
538
|
-
"""
|
|
539
|
-
Save a 3D numpy array of greyscale values OR a 4D numpy array of RGB values as a video
|
|
540
|
-
|
|
541
|
-
Follows the PyAV cookbook section on generating video from numpy arrays:
|
|
542
|
-
https://pyav.basswood-io.com/docs/develop/cookbook/numpy.html#generating-video
|
|
543
|
-
|
|
544
|
-
Parameters
|
|
545
|
-
----------
|
|
546
|
-
data : numpy.ndarray or list of filenames
|
|
547
|
-
A 3D (grayscale) or 4D (RGB) numpy array of pixel values.
|
|
548
|
-
|
|
549
|
-
filename : str
|
|
550
|
-
The filename to save the video to.
|
|
551
|
-
|
|
552
|
-
time_axis : int, default 0
|
|
553
|
-
The axis of the data array that will be played as time in the video.
|
|
554
|
-
|
|
555
|
-
color_axis : int or None, default None
|
|
556
|
-
If not None, specifies the axis of the color channels (e.g., -1 for last axis,
|
|
557
|
-
1 for second axis).
|
|
558
|
-
If None, data must be 3D (greyscale) or 4D with one length 3 axis (RGB).
|
|
559
|
-
|
|
560
|
-
overwrite : bool, default False
|
|
561
|
-
Whether to overwrite the file if it already exists.
|
|
562
|
-
|
|
563
|
-
dim_order : 'yx' (default) or 'xy'
|
|
564
|
-
The order of the spatial dimensions in the input data.
|
|
565
|
-
|
|
566
|
-
framerate : int, default 30
|
|
567
|
-
The frame rate of the video.
|
|
568
|
-
|
|
569
|
-
crf : int, default 23
|
|
570
|
-
Constant Rate Factor that specifies amount of lossiness allowed in compression.
|
|
571
|
-
Lower values produce better quality, larger videos. crf=17 has no human-visible
|
|
572
|
-
compression artifacts. File size approximately doubles/halves each time you
|
|
573
|
-
add/subtract 6 from crf, so crf=17 produces files about twice as large as
|
|
574
|
-
the default crf=23.
|
|
575
|
-
|
|
576
|
-
compression_speed : str, default 'medium'
|
|
577
|
-
Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
|
|
578
|
-
'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
|
|
579
|
-
|
|
580
|
-
progress_bar : bool, default True
|
|
581
|
-
If True, display a progress bar.
|
|
582
|
-
|
|
583
|
-
codec : Literal['libx264', 'libx265'], default 'libx264'
|
|
584
|
-
The video codec to use for encoding. Can be any of a number of aliases for
|
|
585
|
-
these two codecs, including avc1/h264 vs hevc/hvc1/hev1/h265.
|
|
586
|
-
"""
|
|
587
|
-
try:
|
|
588
|
-
from tqdm import tqdm
|
|
589
|
-
except ImportError:
|
|
590
|
-
raise ImportError('Missing optional dependency for video processing,'
|
|
591
|
-
' run `pip install av tqdm`')
|
|
592
|
-
|
|
593
|
-
filename = os.path.expanduser(str(filename))
|
|
594
|
-
if filename.split('.')[-1].lower() not in ['mp4', 'mkv', 'avi', 'mov']:
|
|
595
|
-
filename += '.mp4'
|
|
596
|
-
if os.path.exists(filename) and not overwrite:
|
|
597
|
-
raise FileExistsError(f'File {filename} already exists. '
|
|
598
|
-
'Set overwrite=True to overwrite.')
|
|
599
|
-
|
|
600
|
-
if color_axis is None and data.ndim == 4:
|
|
601
|
-
color_axis = find_channel_axis(data, possible_channel_lengths=3)
|
|
602
|
-
if color_axis is None:
|
|
603
|
-
raise ValueError('4D input data must have an RGB (length 3) axis.')
|
|
604
|
-
if color_axis is not None:
|
|
605
|
-
if data.ndim != 4:
|
|
606
|
-
raise ValueError('Input data must be 4D when color_axis is specified.')
|
|
607
|
-
# Move time axis to 0, color axis to -1
|
|
608
|
-
data = np.moveaxis(data, time_axis, 0)
|
|
609
|
-
if color_axis != -1:
|
|
610
|
-
data = np.moveaxis(data, color_axis, -1)
|
|
611
|
-
if 'xy' in dim_order:
|
|
612
|
-
data = data.swapaxes(1, 2)
|
|
613
|
-
n_frames = data.shape[0]
|
|
614
|
-
height, width, channels = data.shape[1:]
|
|
615
|
-
if channels != 3:
|
|
616
|
-
raise ValueError(f'Color video must have 3 channels (RGB) but had {channels}.')
|
|
617
|
-
else:
|
|
618
|
-
if data.ndim != 3:
|
|
619
|
-
raise ValueError('Input data must be 3D when color_axis is not specified.')
|
|
620
|
-
data = np.moveaxis(data, time_axis, 0)
|
|
621
|
-
if 'xy' in dim_order:
|
|
622
|
-
data = data.swapaxes(1, 2)
|
|
623
|
-
n_frames = data.shape[0]
|
|
624
|
-
height, width = data.shape[1:]
|
|
625
|
-
|
|
626
|
-
extension = filename.split('.')[-1].lower()
|
|
627
|
-
if extension == 'mp4':
|
|
628
|
-
pad = [[0, 0], [0, 0], [0, 0]]
|
|
629
|
-
if height % 2 != 0:
|
|
630
|
-
pad[1][1] = 1
|
|
631
|
-
if width % 2 != 0:
|
|
632
|
-
pad[2][1] = 1
|
|
633
|
-
if pad != [[0, 0], [0, 0], [0, 0]]:
|
|
634
|
-
data = np.pad(data, pad, mode='edge')
|
|
635
|
-
|
|
636
|
-
with VideoWriter(filename, framerate=framerate, crf=crf,
|
|
637
|
-
compression_speed=compression_speed, codec=codec,
|
|
638
|
-
overwrite=overwrite) 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])
|
|
642
|
-
|
|
643
|
-
|
|
644
363
|
def show(data,
|
|
645
364
|
dim_order='yx',
|
|
646
365
|
data_type: Literal['image', 'segmentation'] = 'image',
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Functions for reading, writing, and showing images.
|
|
4
|
+
|
|
5
|
+
Function list:
|
|
6
|
+
- load_video(filename) -> np.ndarray
|
|
7
|
+
- lazy_load_video(filename) -> Iterator[np.ndarray]
|
|
8
|
+
- save_video(data, filename) -> Saves a 3D numpy array as a video
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Union, Tuple, Iterator, Literal
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import threading
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from .imageio import find_channel_axis
|
|
22
|
+
|
|
23
|
+
codec_aliases = {
|
|
24
|
+
"libx264": "libx264",
|
|
25
|
+
"avc1": "libx264",
|
|
26
|
+
"h264": "libx264",
|
|
27
|
+
"H.264": "libx264",
|
|
28
|
+
"libx265": "libx265",
|
|
29
|
+
"hevc": "libx265",
|
|
30
|
+
"hvc1": "libx265",
|
|
31
|
+
"hev1": "libx265",
|
|
32
|
+
"h265": "libx265",
|
|
33
|
+
"H.265": "libx265",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_video(filename,
|
|
38
|
+
return_framerate=False,
|
|
39
|
+
progress_bar=True) -> Union[np.ndarray, Tuple[np.ndarray, float]]:
|
|
40
|
+
"""
|
|
41
|
+
Load all images in a video file as a numpy array.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
filename : str
|
|
46
|
+
Path to the video file
|
|
47
|
+
return_framerate : bool, default False
|
|
48
|
+
If True, return the frame rate of the video
|
|
49
|
+
progress_bar : bool, default True
|
|
50
|
+
If True, display a progress bar
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
data : numpy.ndarray
|
|
55
|
+
The video frames as a numpy array, shape (num_frames, height, width, colors)
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
import av
|
|
59
|
+
from tqdm import tqdm
|
|
60
|
+
except ImportError:
|
|
61
|
+
raise ImportError('Missing optional dependency for video processing,'
|
|
62
|
+
' run `pip install av tqdm`')
|
|
63
|
+
|
|
64
|
+
container = av.open(filename)
|
|
65
|
+
stream = container.streams.video[0]
|
|
66
|
+
num_frames = stream.frames
|
|
67
|
+
if not num_frames or num_frames == 0:
|
|
68
|
+
# If we don't know the number of frames, we can't preallocate, so
|
|
69
|
+
# it's hard to do better than the following approach which temporarily
|
|
70
|
+
# uses double the amount of RAM compared to the preallocated approach.
|
|
71
|
+
data = np.array(list(lazy_load_video(filename)))
|
|
72
|
+
if return_framerate:
|
|
73
|
+
return data, float(stream.average_rate)
|
|
74
|
+
else:
|
|
75
|
+
return data
|
|
76
|
+
else:
|
|
77
|
+
# Load first image to get shape and dtype
|
|
78
|
+
frame_iter = container.decode(stream)
|
|
79
|
+
first_frame = next(frame_iter)
|
|
80
|
+
first_img = first_frame.to_ndarray(format='rgb24')
|
|
81
|
+
# Preallocate memory for the entire array
|
|
82
|
+
data = np.empty((num_frames, *first_img.shape), dtype=first_img.dtype)
|
|
83
|
+
# Then fill it up frame by frame
|
|
84
|
+
data[0] = first_img
|
|
85
|
+
for i, frame in tqdm(enumerate(frame_iter), total=num_frames,
|
|
86
|
+
desc='Loading video', disable=not progress_bar):
|
|
87
|
+
if i == 0:
|
|
88
|
+
continue
|
|
89
|
+
img = frame.to_ndarray(format='rgb24')
|
|
90
|
+
data[i] = img
|
|
91
|
+
if return_framerate:
|
|
92
|
+
return data, float(stream.average_rate)
|
|
93
|
+
else:
|
|
94
|
+
return data
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def lazy_load_video(filename) -> Iterator[np.ndarray]:
|
|
98
|
+
"""
|
|
99
|
+
Lazily load video frames as numpy arrays using PyAV.
|
|
100
|
+
|
|
101
|
+
This iterator yields images in the order they appear in the video.
|
|
102
|
+
If you want reasonably fast random access to arbitrary frames in
|
|
103
|
+
a video, use the VideoStreamer class instead.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
filename : str
|
|
108
|
+
Path to the video file.
|
|
109
|
+
|
|
110
|
+
Yields
|
|
111
|
+
------
|
|
112
|
+
frame : np.ndarray
|
|
113
|
+
Video frame as a numpy array, shape (height, width, colors).
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
import av
|
|
117
|
+
except ImportError:
|
|
118
|
+
raise ImportError('Missing optional dependency for video processing,'
|
|
119
|
+
' run `pip install av tqdm`')
|
|
120
|
+
container = av.open(filename)
|
|
121
|
+
stream = container.streams.video[0]
|
|
122
|
+
for frame in container.decode(stream):
|
|
123
|
+
img = frame.to_ndarray(format='rgb24')
|
|
124
|
+
yield img
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class VideoStreamer:
|
|
128
|
+
def __init__(self, filename):
|
|
129
|
+
try:
|
|
130
|
+
import av
|
|
131
|
+
except ImportError:
|
|
132
|
+
raise ImportError('Missing optional dependency for video processing,'
|
|
133
|
+
' run `pip install av tqdm`')
|
|
134
|
+
self.filename = Path(filename)
|
|
135
|
+
if not self.filename.exists():
|
|
136
|
+
raise FileNotFoundError(f"File {filename} not found")
|
|
137
|
+
self.index_filename = Path(str(filename).replace('.mp4', '_index.json'))
|
|
138
|
+
self._load_index()
|
|
139
|
+
|
|
140
|
+
self.container = av.open(str(self.filename))
|
|
141
|
+
self.stream = self.container.streams.video[0]
|
|
142
|
+
self.time_base = self.stream.time_base
|
|
143
|
+
self._shape = None
|
|
144
|
+
self._first_frame = None
|
|
145
|
+
self._current_frame_number = None
|
|
146
|
+
self._lock = threading.Lock()
|
|
147
|
+
|
|
148
|
+
def _build_index(self):
|
|
149
|
+
print("Building index for fast random frame access...")
|
|
150
|
+
cmd = [
|
|
151
|
+
'ffprobe',
|
|
152
|
+
'-select_streams', 'v:0',
|
|
153
|
+
'-show_frames',
|
|
154
|
+
'-show_entries', 'frame=pkt_pos,pkt_pts_time,coded_picture_number',
|
|
155
|
+
'-of', 'json',
|
|
156
|
+
self.filename
|
|
157
|
+
]
|
|
158
|
+
result = subprocess.run(cmd, stdout=subprocess.PIPE,
|
|
159
|
+
stderr=subprocess.PIPE, text=True)
|
|
160
|
+
if result.returncode != 0:
|
|
161
|
+
raise RuntimeError(f"ffprobe failed: {result.stderr}")
|
|
162
|
+
frames = json.loads(result.stdout).get('frames', [])
|
|
163
|
+
time_index = [float(frame['pkt_pts_time']) for frame in frames]
|
|
164
|
+
if len(time_index) == 0:
|
|
165
|
+
raise RuntimeError("No frames found in video")
|
|
166
|
+
self.n_frames = len(time_index)
|
|
167
|
+
self.t0 = time_index[0]
|
|
168
|
+
index = {
|
|
169
|
+
'n_frames': self.n_frames,
|
|
170
|
+
't0': self.t0,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Determine whether the video is constant or variable framerate
|
|
174
|
+
time_deltas = np.diff(time_index) if len(time_index) > 1 else None
|
|
175
|
+
if time_deltas is not None and np.allclose(time_deltas, time_deltas[0], atol=1e-6):
|
|
176
|
+
self.timestep = float(time_deltas[0]) # The video is constant framerate
|
|
177
|
+
index['timestep'] = self.timestep
|
|
178
|
+
else:
|
|
179
|
+
index.update({
|
|
180
|
+
'timestep': 'variable', # The video is variable framerate
|
|
181
|
+
'time_index': time_index,
|
|
182
|
+
})
|
|
183
|
+
with open(self.index_filename, 'w') as f:
|
|
184
|
+
json.dump(index, f)
|
|
185
|
+
print(f"Saved index to {self.index_filename} so this slow step is not needed again.")
|
|
186
|
+
|
|
187
|
+
def _load_index(self):
|
|
188
|
+
if not self.index_filename.exists():
|
|
189
|
+
self._build_index()
|
|
190
|
+
else:
|
|
191
|
+
with open(self.index_filename, 'r') as f:
|
|
192
|
+
index = json.load(f)
|
|
193
|
+
self.n_frames = index['n_frames']
|
|
194
|
+
self.t0 = index['t0']
|
|
195
|
+
|
|
196
|
+
if not isinstance(index['timestep'], (str, float, int)):
|
|
197
|
+
raise ValueError('Malformed index: timestep is not a string or number')
|
|
198
|
+
|
|
199
|
+
if index['timestep'] == 'variable':
|
|
200
|
+
self.timestep = 'variable'
|
|
201
|
+
self.time_index = index['time_index']
|
|
202
|
+
else:
|
|
203
|
+
self.timestep = float(index['timestep'])
|
|
204
|
+
if self.timestep <= 0:
|
|
205
|
+
raise ValueError('Malformed index: timestep is not positive')
|
|
206
|
+
|
|
207
|
+
def frame_to_time(self, frame_number):
|
|
208
|
+
if self.timestep == 'variable':
|
|
209
|
+
return self.time_index[frame_number]
|
|
210
|
+
else:
|
|
211
|
+
return self.timestep * frame_number + self.t0
|
|
212
|
+
|
|
213
|
+
def __getitem__(self, key):
|
|
214
|
+
if isinstance(key, tuple):
|
|
215
|
+
frame_idx = key[0]
|
|
216
|
+
frame = self._get_frame(frame_idx)
|
|
217
|
+
if len(key) == 1:
|
|
218
|
+
return frame
|
|
219
|
+
else:
|
|
220
|
+
return frame[key[1:]]
|
|
221
|
+
else:
|
|
222
|
+
return self._get_frame(key)
|
|
223
|
+
|
|
224
|
+
def _get_frame(self, frame_number):
|
|
225
|
+
"""
|
|
226
|
+
Provides access to random frames as fast as is reasonable when getting
|
|
227
|
+
frames from compressed video in python.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def decode_until(frame_number) -> np.ndarray:
|
|
231
|
+
"""
|
|
232
|
+
Decode forward from the current frame in the stream until we get
|
|
233
|
+
to the requested frame number.
|
|
234
|
+
"""
|
|
235
|
+
target_time = self.frame_to_time(frame_number)
|
|
236
|
+
for frame in self.container.decode(self.stream):
|
|
237
|
+
frame_time = float(frame.pts * self.time_base)
|
|
238
|
+
if abs(frame_time - target_time) < 1e-6:
|
|
239
|
+
frame = frame.to_ndarray(format='rgb24')
|
|
240
|
+
self._current_frame_number = frame_number
|
|
241
|
+
return frame
|
|
242
|
+
if frame_time > target_time:
|
|
243
|
+
raise RuntimeError(f"Frame with time {target_time} not found after"
|
|
244
|
+
f" seeking – current frame time: {frame_time}")
|
|
245
|
+
raise RuntimeError(f"Frame with time {target_time} not found after"
|
|
246
|
+
f" seeking – current frame time: {frame_time}")
|
|
247
|
+
|
|
248
|
+
if isinstance(frame_number, slice): # Support slicing
|
|
249
|
+
start, stop, step = frame_number.indices(self.n_frames)
|
|
250
|
+
return np.array([self._get_frame(i) for i in range(start, stop, step)])
|
|
251
|
+
with self._lock:
|
|
252
|
+
if frame_number < 0 or frame_number >= self.n_frames:
|
|
253
|
+
raise IndexError(f"Frame {frame_number} out of range: [0, {self.n_frames})")
|
|
254
|
+
|
|
255
|
+
if (self._current_frame_number is None
|
|
256
|
+
or frame_number <= self._current_frame_number
|
|
257
|
+
or frame_number > self._current_frame_number + 100):
|
|
258
|
+
target_time = self.frame_to_time(frame_number)
|
|
259
|
+
seek_time = int(target_time / float(self.time_base))
|
|
260
|
+
# The following actually seeks to the closest keyframe before
|
|
261
|
+
# seek_time, because it's not possible to seek directly to
|
|
262
|
+
# non-keyframes due to video files being compressed.
|
|
263
|
+
self.container.seek(seek_time, any_frame=False,
|
|
264
|
+
backward=True, stream=self.stream)
|
|
265
|
+
# Now we decode frames forward until we get to the requested frame
|
|
266
|
+
return decode_until(frame_number)
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def first_frame(self):
|
|
270
|
+
if self._first_frame is None:
|
|
271
|
+
self._first_frame = self[0]
|
|
272
|
+
return self._first_frame
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def shape(self):
|
|
276
|
+
# (num_frames, height, width, channels)
|
|
277
|
+
if self._shape is None:
|
|
278
|
+
self._shape = (self.n_frames,) + self.first_frame.shape
|
|
279
|
+
return self._shape
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def ndim(self):
|
|
283
|
+
return len(self.shape)
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def dtype(self):
|
|
287
|
+
return self.first_frame.dtype
|
|
288
|
+
|
|
289
|
+
def __len__(self):
|
|
290
|
+
return self.n_frames
|
|
291
|
+
|
|
292
|
+
def close(self):
|
|
293
|
+
self.container.close()
|
|
294
|
+
|
|
295
|
+
def __del__(self):
|
|
296
|
+
try:
|
|
297
|
+
self.close()
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class VideoWriter:
|
|
303
|
+
"""
|
|
304
|
+
Create a video writer object for saving frames to a video file.
|
|
305
|
+
|
|
306
|
+
Example usage:
|
|
307
|
+
>>> with VideoWriter('output.mp4', framerate=30) as writer:
|
|
308
|
+
>>> for i in range(n_frames):
|
|
309
|
+
>>> frame = do_something_to_build_an_image(i)
|
|
310
|
+
>>> writer.write(frame)
|
|
311
|
+
|
|
312
|
+
This allows you to write a bunch of frames to a video file without
|
|
313
|
+
ever needing to store all the frames in memory at once. If you have all
|
|
314
|
+
your frames in memory already, you could use save_video(data, filename)
|
|
315
|
+
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
filename : str
|
|
319
|
+
The filename to save the video to.
|
|
320
|
+
framerate : int, default 30
|
|
321
|
+
The frame rate of the video.
|
|
322
|
+
crf : int, default 23
|
|
323
|
+
Constant Rate Factor for encoding quality (lower is better quality).
|
|
324
|
+
compression_speed : str, default 'medium'
|
|
325
|
+
Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
|
|
326
|
+
'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
|
|
327
|
+
codec : Literal['libx264', 'libx265'], default 'libx264'
|
|
328
|
+
The video codec to use for encoding. Can be any of a number of aliases for
|
|
329
|
+
these two codecs, including avc1/h264 vs hevc/hvc1/hev1/h265.
|
|
330
|
+
"""
|
|
331
|
+
def __init__(self, filename, framerate=30, crf=23, compression_speed='medium',
|
|
332
|
+
codec: Literal['libx264', 'libx265'] = 'libx264',
|
|
333
|
+
overwrite=False):
|
|
334
|
+
try:
|
|
335
|
+
import av
|
|
336
|
+
except ImportError:
|
|
337
|
+
raise ImportError('Missing optional dependency for video processing,'
|
|
338
|
+
' run `pip install av tqdm`')
|
|
339
|
+
self.av = av
|
|
340
|
+
filename = os.path.expanduser(str(filename))
|
|
341
|
+
if os.path.exists(filename) and not overwrite:
|
|
342
|
+
raise FileExistsError(f'File {filename} already exists. '
|
|
343
|
+
'Set overwrite=True to overwrite.')
|
|
344
|
+
self.filename = filename
|
|
345
|
+
self.framerate = framerate
|
|
346
|
+
self.crf = crf
|
|
347
|
+
self.compression_speed = compression_speed
|
|
348
|
+
self.codec = codec_aliases[codec.lower()]
|
|
349
|
+
self.container = av.open(filename, mode='w')
|
|
350
|
+
self.stream = self.container.add_stream(self.codec, rate=framerate)
|
|
351
|
+
self.stream.pix_fmt = 'yuv420p'
|
|
352
|
+
self.stream.options = {'crf': str(crf), 'preset': compression_speed}
|
|
353
|
+
self._closed = False
|
|
354
|
+
self.stream.width = 0
|
|
355
|
+
self.stream.height = 0
|
|
356
|
+
|
|
357
|
+
def write(self, frame):
|
|
358
|
+
if not isinstance(frame, self.av.VideoFrame):
|
|
359
|
+
if frame.ndim == 3 and frame.shape[-1] == 3:
|
|
360
|
+
frame = self.av.VideoFrame.from_ndarray(frame, format='rgb24')
|
|
361
|
+
elif frame.ndim == 2:
|
|
362
|
+
frame = self.av.VideoFrame.from_ndarray(frame, format='gray')
|
|
363
|
+
else:
|
|
364
|
+
raise ValueError(f'Frame must be (H, W) or (H, W, 3) but was {frame.shape}')
|
|
365
|
+
if self.stream.width == 0:
|
|
366
|
+
self.stream.width = frame.width
|
|
367
|
+
if self.stream.height == 0:
|
|
368
|
+
self.stream.height = frame.height
|
|
369
|
+
for packet in self.stream.encode(frame):
|
|
370
|
+
self.container.mux(packet)
|
|
371
|
+
|
|
372
|
+
def __enter__(self):
|
|
373
|
+
return self
|
|
374
|
+
|
|
375
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
376
|
+
# Flush stream
|
|
377
|
+
for packet in self.stream.encode():
|
|
378
|
+
self.container.mux(packet)
|
|
379
|
+
self.container.close()
|
|
380
|
+
self._closed = True
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
|
|
384
|
+
dim_order='yx', framerate=30, crf=23, compression_speed='medium',
|
|
385
|
+
progress_bar=True, codec: Literal['libx264', 'libx265'] = 'libx264') -> None:
|
|
386
|
+
"""
|
|
387
|
+
Save a 3D numpy array of greyscale values OR a 4D numpy array of RGB values as a video
|
|
388
|
+
|
|
389
|
+
Follows the PyAV cookbook section on generating video from numpy arrays:
|
|
390
|
+
https://pyav.basswood-io.com/docs/develop/cookbook/numpy.html#generating-video
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
data : numpy.ndarray or list of filenames
|
|
395
|
+
A 3D (grayscale) or 4D (RGB) numpy array of pixel values.
|
|
396
|
+
|
|
397
|
+
filename : str
|
|
398
|
+
The filename to save the video to.
|
|
399
|
+
|
|
400
|
+
time_axis : int, default 0
|
|
401
|
+
The axis of the data array that will be played as time in the video.
|
|
402
|
+
|
|
403
|
+
color_axis : int or None, default None
|
|
404
|
+
If not None, specifies the axis of the color channels (e.g., -1 for last axis,
|
|
405
|
+
1 for second axis).
|
|
406
|
+
If None, data must be 3D (greyscale) or 4D with one length 3 axis (RGB).
|
|
407
|
+
|
|
408
|
+
overwrite : bool, default False
|
|
409
|
+
Whether to overwrite the file if it already exists.
|
|
410
|
+
|
|
411
|
+
dim_order : 'yx' (default) or 'xy'
|
|
412
|
+
The order of the spatial dimensions in the input data.
|
|
413
|
+
|
|
414
|
+
framerate : int, default 30
|
|
415
|
+
The frame rate of the video.
|
|
416
|
+
|
|
417
|
+
crf : int, default 23
|
|
418
|
+
Constant Rate Factor that specifies amount of lossiness allowed in compression.
|
|
419
|
+
Lower values produce better quality, larger videos. crf=17 has no human-visible
|
|
420
|
+
compression artifacts. File size approximately doubles/halves each time you
|
|
421
|
+
add/subtract 6 from crf, so crf=17 produces files about twice as large as
|
|
422
|
+
the default crf=23.
|
|
423
|
+
|
|
424
|
+
compression_speed : str, default 'medium'
|
|
425
|
+
Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
|
|
426
|
+
'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
|
|
427
|
+
|
|
428
|
+
progress_bar : bool, default True
|
|
429
|
+
If True, display a progress bar.
|
|
430
|
+
|
|
431
|
+
codec : Literal['libx264', 'libx265'], default 'libx264'
|
|
432
|
+
The video codec to use for encoding. Can be any of a number of aliases for
|
|
433
|
+
these two codecs, including avc1/h264 vs hevc/hvc1/hev1/h265.
|
|
434
|
+
"""
|
|
435
|
+
try:
|
|
436
|
+
from tqdm import tqdm
|
|
437
|
+
except ImportError:
|
|
438
|
+
raise ImportError('Missing optional dependency for video processing,'
|
|
439
|
+
' run `pip install av tqdm`')
|
|
440
|
+
|
|
441
|
+
filename = os.path.expanduser(str(filename))
|
|
442
|
+
if filename.split('.')[-1].lower() not in ['mp4', 'mkv', 'avi', 'mov']:
|
|
443
|
+
filename += '.mp4'
|
|
444
|
+
if os.path.exists(filename) and not overwrite:
|
|
445
|
+
raise FileExistsError(f'File {filename} already exists. '
|
|
446
|
+
'Set overwrite=True to overwrite.')
|
|
447
|
+
|
|
448
|
+
if color_axis is None and data.ndim == 4:
|
|
449
|
+
color_axis = find_channel_axis(data, possible_channel_lengths=3)
|
|
450
|
+
if color_axis is None:
|
|
451
|
+
raise ValueError('4D input data must have an RGB (length 3) axis.')
|
|
452
|
+
if color_axis is not None:
|
|
453
|
+
if data.ndim != 4:
|
|
454
|
+
raise ValueError('Input data must be 4D when color_axis is specified.')
|
|
455
|
+
# Move time axis to 0, color axis to -1
|
|
456
|
+
data = np.moveaxis(data, time_axis, 0)
|
|
457
|
+
if color_axis != -1:
|
|
458
|
+
data = np.moveaxis(data, color_axis, -1)
|
|
459
|
+
if 'xy' in dim_order:
|
|
460
|
+
data = data.swapaxes(1, 2)
|
|
461
|
+
n_frames = data.shape[0]
|
|
462
|
+
height, width, channels = data.shape[1:]
|
|
463
|
+
if channels != 3:
|
|
464
|
+
raise ValueError(f'Color video must have 3 channels (RGB) but had {channels}.')
|
|
465
|
+
else:
|
|
466
|
+
if data.ndim != 3:
|
|
467
|
+
raise ValueError('Input data must be 3D when color_axis is not specified.')
|
|
468
|
+
data = np.moveaxis(data, time_axis, 0)
|
|
469
|
+
if 'xy' in dim_order:
|
|
470
|
+
data = data.swapaxes(1, 2)
|
|
471
|
+
n_frames = data.shape[0]
|
|
472
|
+
height, width = data.shape[1:]
|
|
473
|
+
|
|
474
|
+
extension = filename.split('.')[-1].lower()
|
|
475
|
+
if extension == 'mp4':
|
|
476
|
+
pad = [[0, 0], [0, 0], [0, 0]]
|
|
477
|
+
if height % 2 != 0:
|
|
478
|
+
pad[1][1] = 1
|
|
479
|
+
if width % 2 != 0:
|
|
480
|
+
pad[2][1] = 1
|
|
481
|
+
if pad != [[0, 0], [0, 0], [0, 0]]:
|
|
482
|
+
data = np.pad(data, pad, mode='edge')
|
|
483
|
+
|
|
484
|
+
with VideoWriter(filename, framerate=framerate, crf=crf,
|
|
485
|
+
compression_speed=compression_speed, codec=codec,
|
|
486
|
+
overwrite=overwrite) as writer:
|
|
487
|
+
for frame_i in tqdm(range(n_frames), total=n_frames,
|
|
488
|
+
desc='Saving video', disable=not progress_bar):
|
|
489
|
+
writer.write(data[frame_i])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: numpyimage
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.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: GNU GENERAL PUBLIC LICENSE
|
|
@@ -701,18 +701,19 @@ Requires-Dist: av; extra == "all"
|
|
|
701
701
|
Dynamic: license-file
|
|
702
702
|
|
|
703
703
|
# npimage
|
|
704
|
-
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 `
|
|
704
|
+
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/).)
|
|
705
705
|
|
|
706
706
|
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!)
|
|
707
707
|
|
|
708
708
|
|
|
709
709
|
### Documentation
|
|
710
|
-
- `
|
|
710
|
+
- `imageio.py`: load, save, or show images.
|
|
711
|
+
- `vidio.py`: load or save videos.
|
|
711
712
|
- `graphics.py`: draw points, lines, triangles, circles, or spheres into 2D or 3D numpy arrays representing image volumes.
|
|
712
713
|
- `nrrd_utils.py`: compress or read metadata from `.nrrd` files.
|
|
713
714
|
- `operations.py`: perform operations on images.
|
|
714
715
|
|
|
715
|
-
|
|
716
|
+
Check each function's docstring for more details.
|
|
716
717
|
|
|
717
718
|
|
|
718
719
|
### Installation
|
|
@@ -3,11 +3,12 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
npimage/__init__.py
|
|
5
5
|
npimage/align.py
|
|
6
|
-
npimage/core.py
|
|
7
6
|
npimage/graphics.py
|
|
7
|
+
npimage/imageio.py
|
|
8
8
|
npimage/nrrd_utils.py
|
|
9
9
|
npimage/operations.py
|
|
10
10
|
npimage/utils.py
|
|
11
|
+
npimage/vidio.py
|
|
11
12
|
numpyimage.egg-info/PKG-INFO
|
|
12
13
|
numpyimage.egg-info/SOURCES.txt
|
|
13
14
|
numpyimage.egg-info/dependency_links.txt
|
|
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
|