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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numpyimage
3
- Version: 2.6.0
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 `core.py` does that, with `array = load(filename)`, `save(array, filename)`, and `show(array)` functions that let you easily handle a number of common image file formats without having to remember library-specific syntax. (Another choice of library to consider for accomplishing similar goals is [imageio](https://pypi.org/project/imageio/), which also supports loading videos through the FFmpeg wrapper library [pyav](https://pypi.org/project/av/).)
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
- - `core.py`: load, save, or show images.
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
- For now, check each function's docstring for more. A jupyter notebook demonstrating this package's functions will come later.
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 `core.py` does that, with `array = load(filename)`, `save(array, filename)`, and `show(array)` functions that let you easily handle a number of common image file formats without having to remember library-specific syntax. (Another choice of library to consider for accomplishing similar goals is [imageio](https://pypi.org/project/imageio/), which also supports loading videos through the FFmpeg wrapper library [pyav](https://pypi.org/project/av/).)
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
- - `core.py`: load, save, or show images.
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
- For now, check each function's docstring for more. A jupyter notebook demonstrating this package's functions will come later.
14
+ Check each function's docstring for more details.
14
15
 
15
16
 
16
17
  ### Installation
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- from .core import *
3
+ from .imageio import *
4
+ from .vidio import *
5
+
4
6
  from .operations import *
5
7
  from .align import *
6
8
  from .graphics import *
@@ -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 .core import save
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, Iterator
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.0
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 `core.py` does that, with `array = load(filename)`, `save(array, filename)`, and `show(array)` functions that let you easily handle a number of common image file formats without having to remember library-specific syntax. (Another choice of library to consider for accomplishing similar goals is [imageio](https://pypi.org/project/imageio/), which also supports loading videos through the FFmpeg wrapper library [pyav](https://pypi.org/project/av/).)
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
- - `core.py`: load, save, or show images.
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
- For now, check each function's docstring for more. A jupyter notebook demonstrating this package's functions will come later.
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
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
4
4
 
5
5
  [project]
6
6
  name = 'numpyimage'
7
- version = '2.6.0'
7
+ version = '2.6.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'
File without changes
File without changes
File without changes
File without changes
File without changes