├── src └── napari_video │ ├── __init__.py │ ├── napari.yaml │ └── napari_video.py ├── .napari └── config.yml ├── .github └── workflows │ └── plugin_preview.yml ├── pyproject.toml ├── LICENSE ├── setup.py ├── README.md └── .gitignore /src/napari_video/__init__.py: -------------------------------------------------------------------------------- 1 | """napari plugin for reading videos.""" 2 | 3 | __version__ = "0.2.13" 4 | -------------------------------------------------------------------------------- /.napari/config.yml: -------------------------------------------------------------------------------- 1 | 2 | # Add labels the plugin from the EDAM Bioimaging ontology 3 | labels: 4 | ontology: EDAM-BIOIMAGING:alpha06 5 | terms: 6 | - Image time series 7 | -------------------------------------------------------------------------------- /src/napari_video/napari.yaml: -------------------------------------------------------------------------------- 1 | name: napari_video 2 | display_name: video 3 | contributions: 4 | commands: 5 | - id: napari_video.get_reader 6 | title: Read video files 7 | python_name: napari_video.napari_video:napari_get_reader 8 | readers: 9 | - command: napari_video.get_reader 10 | filename_patterns: ["*.mp4", "*.mov", "*.avi"] 11 | accepts_directories: false 12 | -------------------------------------------------------------------------------- /.github/workflows/plugin_preview.yml: -------------------------------------------------------------------------------- 1 | 2 | name: napari hub Preview Page # we use this name to find your preview page artifact, so don't change it! 3 | # For more info on this action, see https://github.com/chanzuckerberg/napari-hub-preview-action/blob/main/action.yml 4 | 5 | on: 6 | pull_request: 7 | branches: 8 | - '**' 9 | 10 | jobs: 11 | preview-page: 12 | name: Preview Page Deploy 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@v2 18 | 19 | - name: napari hub Preview Page Builder 20 | uses: chanzuckerberg/napari-hub-preview-action@v0.1.5 21 | with: 22 | hub-ref: main 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [tool.flit.metadata] 6 | module = "napari_video" 7 | author = "Jan Clemens" 8 | author-email = "clemensjan@googlemail.com" 9 | home-page = "https://github.com/janclemenslab/napari-video" 10 | classifiers = [ 11 | "License :: OSI Approved :: MIT License", 12 | "Framework :: napari", 13 | "Operating System :: OS Independent", 14 | ] 15 | requires-python = ">=3.6" 16 | requires = ['numpy', 'pyvideoreader'] 17 | description-file = "README.md" 18 | 19 | [tool.flit.entrypoints."napari.manifest"] 20 | napari_video = "napari_video:napari.yaml" 21 | 22 | [tool.flit.metadata.urls] 23 | "Bug Tracker" = "https://github.com/janclemenslab/napari-video/issues" 24 | "Documentation" = "https://github.com/janclemenslab/napari-video/blob/main/README.md" 25 | "Source Code" = "https://github.com/janclemenslab/napari-video" 26 | "User Support" = "https://github.com/janclemenslab/napari-video/issues" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Jan Clemens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import codecs 3 | import re 4 | import os 5 | 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | 9 | def read(*parts): 10 | with codecs.open(os.path.join(here, *parts), 'r') as fp: 11 | return fp.read() 12 | 13 | 14 | def find_version(*file_paths): 15 | version_file = read(*file_paths) 16 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 17 | version_file, re.M) 18 | if version_match: 19 | return version_match.group(1) 20 | raise RuntimeError("Unable to find version string.") 21 | 22 | 23 | # read the contents of your README file 24 | this_directory = os.path.abspath(os.path.dirname(__file__)) 25 | with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f: 26 | long_description = f.read() 27 | 28 | 29 | setup(name='napari_video', 30 | version=find_version("src/napari_video/__init__.py"), 31 | description='napari_video', 32 | long_description=long_description, 33 | long_description_content_type="text/markdown", 34 | url='http://github.com/janclemenslab/napari-video', 35 | author='Jan Clemens', 36 | author_email='clemensjan@googlemail.com', 37 | license='MIT', 38 | classifiers=['Framework :: napari'], 39 | packages=find_packages('src'), 40 | package_dir={'': 'src'}, 41 | python_requires='>=3.6', 42 | install_requires=['numpy', 'pyvideoreader'], 43 | include_package_data=True, 44 | zip_safe=False, 45 | entry_points={'napari.manifest': 'napari_video = napari_video:napari.yaml'}, 46 | ) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # napari-video 2 | [![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/napari_video)](https://napari-hub.org/plugins/napari_video) 3 | 4 | Napari plugin for working with videos. 5 | 6 | Relies on [pyvideoreader](https://pypi.org/project/pyvideoreader/) as a backend which itself uses [opencv](https://opencv.org) for reading videos. 7 | 8 | ## Installation 9 | ```shell 10 | pip install napari[all] napari_video 11 | ``` 12 | 13 | ## Usage 14 | From a terminal: 15 | ```shell 16 | napari video.avi 17 | ``` 18 | 19 | Or from within python: 20 | ```shell 21 | import napari 22 | from napari_video.napari_video import VideoReaderNP 23 | 24 | path='video.mp4' 25 | vr = VideoReaderNP(path) 26 | with napari.gui_qt(): 27 | viewer = napari.view_image(vr, name=path) 28 | ``` 29 | 30 | ## Internals 31 | `napari_video.napari_video.VideoReaderNP` exposes a video with a numpy-like interface, using opencv as a backend. 32 | 33 | For instance, open a video: 34 | ```python 35 | vr = VideoReaderNP('video.avi') 36 | print(vr) 37 | ``` 38 | ``` 39 | video.avi with 60932 frames of size (920, 912, 3) at 100.00 fps 40 | ``` 41 | Then 42 | 43 | - `vr[100]` will return the 100th frame as a numpy array with shape `(902, 912, 3)`. 44 | - `vr[100:200:10]` will return 10 frames evenly spaced between frame number 100 and 200 (shape `(10, 902, 912, 3)`). 45 | - Note that by default, single-frame and slice indexing return 3D and 4D arrays, respectively. To consistently return 4D arrays, open the video with `remove_leading_singleton=False`. `vr[100]` will then return a `(1, 902, 912, 3)` array. 46 | - We can also request specific ROIs and channels. For instance, `vr[100:200:10,100:400,800:850,1]` will return an array with shape `(10, 300, 50, 1)`. 47 | 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | src/deepsongsegmenter.egg-info 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | # docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # MATLAB 108 | *.asv 109 | 110 | 111 | # LEAP-specific 112 | dat/* 113 | dat.*/ 114 | res* 115 | fig* 116 | *.err 117 | *.out 118 | scratch 119 | /tests/ 120 | 121 | 122 | /* 123 | /*/ 124 | !/src/ 125 | !/env/ 126 | !/docs/ 127 | !/*.toml 128 | !/.github 129 | src/*.egg-info 130 | 131 | -------------------------------------------------------------------------------- /src/napari_video/napari_video.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from videoreader import VideoReader 3 | import cv2 4 | 5 | 6 | class VideoReaderNP(VideoReader): 7 | """VideoReader posing as numpy array.""" 8 | 9 | def __init__(self, filename: str, remove_leading_singleton: bool = True): 10 | """ 11 | 12 | Args: 13 | filename (str): filename of the video 14 | remove_leading_singleton (bool, optional): Remove leading singleton dimension when returning single frames. Defaults to True. 15 | """ 16 | super().__init__(filename) 17 | self.remove_leading_singleton = remove_leading_singleton 18 | 19 | def __getitem__(self, index): 20 | # numpy-like slice imaging into arbitrary dims of the video 21 | # ugly.hacky but works 22 | frames = None 23 | if isinstance(index, int): # single frame 24 | ret, frames = self.read(index) 25 | frames = cv2.cvtColor(frames, cv2.COLOR_BGR2RGB) 26 | elif isinstance(index, slice): # slice of frames 27 | frames = np.stack([self[ii] for ii in range(*index.indices(len(self)))]) 28 | elif isinstance(index, range): # range of frames 29 | frames = np.stack([self[ii] for ii in index]) 30 | elif isinstance(index, tuple): # unpack tuple of indices 31 | if isinstance(index[0], slice): 32 | indices = range(*index[0].indices(len(self))) 33 | elif isinstance(index[0], (np.integer, int)): 34 | indices = int(index[0]) 35 | else: 36 | indices = None 37 | 38 | if indices is not None: 39 | frames = self[indices] 40 | 41 | # index into pixels and channels 42 | for cnt, idx in enumerate(index[1:]): 43 | if isinstance(idx, slice): 44 | ix = range(*idx.indices(self.shape[cnt + 1])) 45 | elif isinstance(idx, int): 46 | ix = range(idx - 1, idx) 47 | else: 48 | continue 49 | 50 | if frames.ndim == 4: # ugly indexing from the back (-1,-2 etc) 51 | cnt = cnt + 1 52 | frames = np.take(frames, ix, axis=cnt) 53 | 54 | if self.remove_leading_singleton and frames is not None: 55 | if frames.shape[0] == 1: 56 | frames = frames[0] 57 | return frames 58 | 59 | @property 60 | def dtype(self): 61 | return np.uint8 62 | 63 | @property 64 | def shape(self): 65 | return (self.number_of_frames, *self.frame_shape) 66 | 67 | @property 68 | def ndim(self): 69 | return len(self.shape) + 1 70 | 71 | @property 72 | def size(self): 73 | return np.prod(self.shape) 74 | 75 | def min(self): 76 | return 0 77 | 78 | def max(self): 79 | return 255 80 | 81 | 82 | def video_file_reader(path): 83 | array = VideoReaderNP(path, remove_leading_singleton=False) 84 | return [(array, {"name": path}, "image")] 85 | 86 | 87 | def napari_get_reader(path): 88 | # remember, path can be a list, so we check it's type first... 89 | if isinstance(path, str) and any([path.endswith(ext) for ext in [".mp4", ".mov", ".avi"]]): 90 | # If we recognize the format, we return the actual reader function 91 | return video_file_reader 92 | # otherwise we return None. 93 | return None 94 | --------------------------------------------------------------------------------