adaptive-motion-preprocessing 1.0.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. adaptive_motion_preprocessing-1.0.0rc1/.github/dependabot.yml +11 -0
  2. adaptive_motion_preprocessing-1.0.0rc1/.github/workflows/ci.yml +92 -0
  3. adaptive_motion_preprocessing-1.0.0rc1/.github/workflows/publish.yml +49 -0
  4. adaptive_motion_preprocessing-1.0.0rc1/.gitignore +30 -0
  5. adaptive_motion_preprocessing-1.0.0rc1/LICENSE +21 -0
  6. adaptive_motion_preprocessing-1.0.0rc1/PKG-INFO +124 -0
  7. adaptive_motion_preprocessing-1.0.0rc1/README.md +94 -0
  8. adaptive_motion_preprocessing-1.0.0rc1/examples/custom_background_subtractor.py +78 -0
  9. adaptive_motion_preprocessing-1.0.0rc1/examples/from_video_file.py +50 -0
  10. adaptive_motion_preprocessing-1.0.0rc1/examples/live_camera.py +120 -0
  11. adaptive_motion_preprocessing-1.0.0rc1/pyproject.toml +56 -0
  12. adaptive_motion_preprocessing-1.0.0rc1/src/amprep/__init__.py +14 -0
  13. adaptive_motion_preprocessing-1.0.0rc1/src/amprep/adaptive_sampler.py +198 -0
  14. adaptive_motion_preprocessing-1.0.0rc1/src/amprep/background_subtractor.py +202 -0
  15. adaptive_motion_preprocessing-1.0.0rc1/src/amprep/frame_window.py +130 -0
  16. adaptive_motion_preprocessing-1.0.0rc1/src/amprep/motion_history_encoder.py +120 -0
  17. adaptive_motion_preprocessing-1.0.0rc1/src/amprep/motion_trigger.py +64 -0
  18. adaptive_motion_preprocessing-1.0.0rc1/src/amprep/noise_reducer.py +83 -0
  19. adaptive_motion_preprocessing-1.0.0rc1/src/amprep/preprocessor.py +163 -0
  20. adaptive_motion_preprocessing-1.0.0rc1/src/amprep/py.typed +0 -0
  21. adaptive_motion_preprocessing-1.0.0rc1/src/amprep/types.py +94 -0
  22. adaptive_motion_preprocessing-1.0.0rc1/tests/test_adaptive_sampler.py +461 -0
  23. adaptive_motion_preprocessing-1.0.0rc1/tests/test_background_subtractor.py +557 -0
  24. adaptive_motion_preprocessing-1.0.0rc1/tests/test_end_to_end.py +293 -0
  25. adaptive_motion_preprocessing-1.0.0rc1/tests/test_examples.py +115 -0
  26. adaptive_motion_preprocessing-1.0.0rc1/tests/test_frame_window.py +249 -0
  27. adaptive_motion_preprocessing-1.0.0rc1/tests/test_import.py +4 -0
  28. adaptive_motion_preprocessing-1.0.0rc1/tests/test_motion_history_encoder.py +187 -0
  29. adaptive_motion_preprocessing-1.0.0rc1/tests/test_motion_trigger.py +197 -0
  30. adaptive_motion_preprocessing-1.0.0rc1/tests/test_noise_reducer.py +197 -0
  31. adaptive_motion_preprocessing-1.0.0rc1/tests/test_pipeline.py +175 -0
  32. adaptive_motion_preprocessing-1.0.0rc1/tests/test_plugin_contract.py +271 -0
  33. adaptive_motion_preprocessing-1.0.0rc1/tests/test_process_input.py +242 -0
  34. adaptive_motion_preprocessing-1.0.0rc1/tests/test_types.py +28 -0
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: monthly
7
+
8
+ - package-ecosystem: pip
9
+ directory: /
10
+ schedule:
11
+ interval: monthly
@@ -0,0 +1,92 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ env:
14
+ FORCE_COLOR: "1"
15
+ PIP_DISABLE_PIP_VERSION_CHECK: "1"
16
+
17
+ jobs:
18
+ lint:
19
+ name: Lint
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+ cache: pip
28
+ cache-dependency-path: pyproject.toml
29
+
30
+ - name: Install dependencies
31
+ run: python -m pip install ruff
32
+
33
+ - name: Ruff check
34
+ run: ruff check --output-format=github .
35
+
36
+ - name: Ruff format
37
+ run: ruff format --check --diff .
38
+
39
+ test:
40
+ name: Test (${{ matrix.os }}, Python ${{ matrix.python-version }})
41
+ runs-on: ${{ matrix.os }}
42
+ strategy:
43
+ fail-fast: false
44
+ matrix:
45
+ os: [ubuntu-latest, windows-latest, macos-latest]
46
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+
50
+ - uses: actions/setup-python@v5
51
+ with:
52
+ python-version: ${{ matrix.python-version }}
53
+ cache: pip
54
+ cache-dependency-path: pyproject.toml
55
+
56
+ - name: Install OpenCV system libraries
57
+ if: runner.os == 'Linux'
58
+ run: |
59
+ sudo apt-get update
60
+ sudo apt-get install -y --no-install-recommends libgl1 libglib2.0-0
61
+
62
+ - name: Install package
63
+ run: python -m pip install -e ".[dev]"
64
+
65
+ - name: Run tests
66
+ run: pytest -v
67
+
68
+ build:
69
+ name: Build distributions
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+
74
+ - uses: actions/setup-python@v5
75
+ with:
76
+ python-version: "3.12"
77
+ cache: pip
78
+ cache-dependency-path: pyproject.toml
79
+
80
+ - name: Install build tooling
81
+ run: python -m pip install build twine
82
+
83
+ - name: Build sdist and wheel
84
+ run: python -m build
85
+
86
+ - name: Check metadata
87
+ run: twine check --strict dist/*
88
+
89
+ - uses: actions/upload-artifact@v4
90
+ with:
91
+ name: dist
92
+ path: dist/
@@ -0,0 +1,49 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distributions
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+
19
+ - name: Install build tooling
20
+ run: python -m pip install build twine
21
+
22
+ - name: Build sdist and wheel
23
+ run: python -m build
24
+
25
+ - name: Check metadata
26
+ run: twine check --strict dist/*
27
+
28
+ - uses: actions/upload-artifact@v4
29
+ with:
30
+ name: dist
31
+ path: dist/
32
+
33
+ publish:
34
+ name: Publish to PyPI
35
+ needs: build
36
+ runs-on: ubuntu-latest
37
+ environment:
38
+ name: pypi
39
+ url: https://pypi.org/p/adaptive-motion-preprocessing
40
+ permissions:
41
+ id-token: write # required for PyPI trusted publishing
42
+ steps:
43
+ - uses: actions/download-artifact@v4
44
+ with:
45
+ name: dist
46
+ path: dist/
47
+
48
+ - name: Publish
49
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,30 @@
1
+ # Byte-compiled / optimized
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ wheels/
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Test / coverage / lint caches
19
+ .pytest_cache/
20
+ .ruff_cache/
21
+ .coverage
22
+ .coverage.*
23
+ htmlcov/
24
+ .tox/
25
+
26
+ # Editors / OS
27
+ .vscode/
28
+ .idea/
29
+ .DS_Store
30
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sahar Darafsh
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.5
2
+ Name: adaptive-motion-preprocessing
3
+ Version: 1.0.0rc1
4
+ Summary: Adaptive motion preprocessing: video frames in, encoded motion images out.
5
+ Project-URL: Homepage, https://github.com/sdrfsh/adaptive-motion-preprocessing
6
+ Project-URL: Repository, https://github.com/sdrfsh/adaptive-motion-preprocessing
7
+ Project-URL: Issues, https://github.com/sdrfsh/adaptive-motion-preprocessing/issues
8
+ Author: Sahar Darafsh
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: computer-vision,motion-detection,opencv,preprocessing
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: numpy
25
+ Requires-Dist: opencv-python
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest; extra == 'dev'
28
+ Requires-Dist: ruff; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # 🎞️ Adaptive Motion Preprocessing
32
+
33
+ Turn video into motion images your neural network can read.
34
+
35
+ While something moves, each window of frames becomes one grayscale picture:
36
+ older frames dim, newer ones bright, so a single image shows where the
37
+ subject went and how fast. Every image has the same shape, so your model
38
+ never gets a surprise.
39
+
40
+ ## 📦 Install
41
+
42
+ ```sh
43
+ pip install adaptive-motion-preprocessing
44
+ ```
45
+
46
+ Then `import amprep`.
47
+
48
+ ## 🚀 Quick start
49
+
50
+ You bring the frames (any iterable of `uint8` BGR arrays) and the package
51
+ does the rest:
52
+
53
+ ```python
54
+ import cv2
55
+
56
+ from amprep import AdaptiveMotionPreprocessor
57
+
58
+
59
+ def frames_from(path):
60
+ capture = cv2.VideoCapture(path)
61
+ try:
62
+ if not capture.isOpened():
63
+ raise OSError(f"cannot open video: {path}")
64
+ while True:
65
+ ok, frame = capture.read()
66
+ if not ok:
67
+ return
68
+ yield frame
69
+ finally:
70
+ capture.release()
71
+
72
+
73
+ for image in AdaptiveMotionPreprocessor().process(frames_from("clip.mp4")):
74
+ print(image.data.shape)
75
+ ```
76
+
77
+ ## 🎥 Try it on your webcam
78
+
79
+ See your camera and the motion images side by side, live:
80
+
81
+ ```sh
82
+ git clone https://github.com/sdrfsh/adaptive-motion-preprocessing
83
+ cd adaptive-motion-preprocessing
84
+ pip install -e .
85
+ python examples/live_camera.py
86
+ ```
87
+
88
+ Stay out of shot for a second while it learns the background, then move.
89
+ Press `q` or `Esc` to quit. Add `--camera 1` for an external webcam, or
90
+ `--threshold 0.03` if it triggers when nothing is moving.
91
+
92
+ ## ⚙️ Settings
93
+
94
+ All optional keyword arguments of `AdaptiveMotionPreprocessor(...)`:
95
+
96
+ | Setting | Default | What it does |
97
+ | --- | --- | --- |
98
+ | `motion_threshold` | `0.01` | Share of the frame that must be moving before frames are collected |
99
+ | `window_frames` | `10` | Frames per window, and one image per full window |
100
+ | `sample_frames` | `4` | Frames painted into each image (at most `window_frames`) |
101
+ | `width`, `height` | `None` | Output size; leave unset to keep the frame size, or set both |
102
+ | `noise_reducer` | median filter | Your own `NoiseReducer` subclass |
103
+ | `background_subtractor` | KNN | Your own `BackgroundSubtractor` subclass |
104
+
105
+ ## 💡 Good to know
106
+
107
+ - ⏱️ **Frames, not seconds.** 10 frames is about 0.33 s at 30 fps and 1 s at
108
+ 10 fps. The package never reads the frame rate, so that math is yours.
109
+ - 🔁 **A steady stream.** While motion lasts you get one image every
110
+ `window_frames` frames. A half-full window is dropped when motion stops.
111
+ - 🌱 **Warm-up.** The default subtractor spends its first 4 frames learning
112
+ the background, so they never produce images. Change it with
113
+ `KNNBackgroundSubtractor(warmup_frames=...)`.
114
+ - 🎬 **New scene?** Call `reset()`. It forgets the background, any half-built
115
+ window and the frame size. Otherwise state carries over between
116
+ `process()` calls.
117
+ - 📐 **One frame size per scene.** Frames that change size mid-stream raise a
118
+ `ValueError`. Call `reset()` first if the change is on purpose.
119
+
120
+ ## 📚 Examples
121
+
122
+ - [examples/live_camera.py](examples/live_camera.py): watch it live on your webcam, camera and motion image side by side
123
+ - [examples/from_video_file.py](examples/from_video_file.py): run it on a video file
124
+ - [examples/custom_background_subtractor.py](examples/custom_background_subtractor.py): plug in your own stage
@@ -0,0 +1,94 @@
1
+ # 🎞️ Adaptive Motion Preprocessing
2
+
3
+ Turn video into motion images your neural network can read.
4
+
5
+ While something moves, each window of frames becomes one grayscale picture:
6
+ older frames dim, newer ones bright, so a single image shows where the
7
+ subject went and how fast. Every image has the same shape, so your model
8
+ never gets a surprise.
9
+
10
+ ## 📦 Install
11
+
12
+ ```sh
13
+ pip install adaptive-motion-preprocessing
14
+ ```
15
+
16
+ Then `import amprep`.
17
+
18
+ ## 🚀 Quick start
19
+
20
+ You bring the frames (any iterable of `uint8` BGR arrays) and the package
21
+ does the rest:
22
+
23
+ ```python
24
+ import cv2
25
+
26
+ from amprep import AdaptiveMotionPreprocessor
27
+
28
+
29
+ def frames_from(path):
30
+ capture = cv2.VideoCapture(path)
31
+ try:
32
+ if not capture.isOpened():
33
+ raise OSError(f"cannot open video: {path}")
34
+ while True:
35
+ ok, frame = capture.read()
36
+ if not ok:
37
+ return
38
+ yield frame
39
+ finally:
40
+ capture.release()
41
+
42
+
43
+ for image in AdaptiveMotionPreprocessor().process(frames_from("clip.mp4")):
44
+ print(image.data.shape)
45
+ ```
46
+
47
+ ## 🎥 Try it on your webcam
48
+
49
+ See your camera and the motion images side by side, live:
50
+
51
+ ```sh
52
+ git clone https://github.com/sdrfsh/adaptive-motion-preprocessing
53
+ cd adaptive-motion-preprocessing
54
+ pip install -e .
55
+ python examples/live_camera.py
56
+ ```
57
+
58
+ Stay out of shot for a second while it learns the background, then move.
59
+ Press `q` or `Esc` to quit. Add `--camera 1` for an external webcam, or
60
+ `--threshold 0.03` if it triggers when nothing is moving.
61
+
62
+ ## ⚙️ Settings
63
+
64
+ All optional keyword arguments of `AdaptiveMotionPreprocessor(...)`:
65
+
66
+ | Setting | Default | What it does |
67
+ | --- | --- | --- |
68
+ | `motion_threshold` | `0.01` | Share of the frame that must be moving before frames are collected |
69
+ | `window_frames` | `10` | Frames per window, and one image per full window |
70
+ | `sample_frames` | `4` | Frames painted into each image (at most `window_frames`) |
71
+ | `width`, `height` | `None` | Output size; leave unset to keep the frame size, or set both |
72
+ | `noise_reducer` | median filter | Your own `NoiseReducer` subclass |
73
+ | `background_subtractor` | KNN | Your own `BackgroundSubtractor` subclass |
74
+
75
+ ## 💡 Good to know
76
+
77
+ - ⏱️ **Frames, not seconds.** 10 frames is about 0.33 s at 30 fps and 1 s at
78
+ 10 fps. The package never reads the frame rate, so that math is yours.
79
+ - 🔁 **A steady stream.** While motion lasts you get one image every
80
+ `window_frames` frames. A half-full window is dropped when motion stops.
81
+ - 🌱 **Warm-up.** The default subtractor spends its first 4 frames learning
82
+ the background, so they never produce images. Change it with
83
+ `KNNBackgroundSubtractor(warmup_frames=...)`.
84
+ - 🎬 **New scene?** Call `reset()`. It forgets the background, any half-built
85
+ window and the frame size. Otherwise state carries over between
86
+ `process()` calls.
87
+ - 📐 **One frame size per scene.** Frames that change size mid-stream raise a
88
+ `ValueError`. Call `reset()` first if the change is on purpose.
89
+
90
+ ## 📚 Examples
91
+
92
+ - [examples/live_camera.py](examples/live_camera.py): watch it live on your webcam, camera and motion image side by side
93
+ - [examples/from_video_file.py](examples/from_video_file.py): run it on a video file
94
+ - [examples/custom_background_subtractor.py](examples/custom_background_subtractor.py): plug in your own stage
@@ -0,0 +1,78 @@
1
+ """Plug your own background subtractor into the pipeline.
2
+
3
+ Usage:
4
+ python examples/custom_background_subtractor.py
5
+
6
+ Subclass ``BackgroundSubtractor``, implement ``_apply`` and ``reset``, and
7
+ pass an instance by keyword. Nothing else changes: the same motion trigger,
8
+ windowing, sampling and encoding run behind it, and the motion images come
9
+ out in the same shape. The script runs the default pipeline and the custom
10
+ one over the same synthetic clip so the two can be compared.
11
+ """
12
+
13
+ import cv2
14
+ import numpy as np
15
+
16
+ from amprep import AdaptiveMotionPreprocessor, BackgroundSubtractor
17
+
18
+
19
+ class FrameDifferenceSubtractor(BackgroundSubtractor):
20
+ """Foreground is whatever changed since the previous frame.
21
+
22
+ Far cruder than the default KNN model, but it is useful from its very
23
+ first frame, so it has no warm-up to declare.
24
+
25
+ ``_apply`` receives a ``uint8`` ``(H, W, 3)`` BGR frame and must return a
26
+ ``uint8`` ``(H, W)`` mask. ``apply``, which you do not override, checks
27
+ both. ``reset`` forgets the previous frame when the scene changes.
28
+ """
29
+
30
+ def __init__(self, threshold: int = 25) -> None:
31
+ self._threshold = threshold
32
+ self._previous: np.ndarray | None = None
33
+
34
+ def _apply(self, frame: np.ndarray) -> np.ndarray:
35
+ grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
36
+ if self._previous is None:
37
+ mask = np.zeros_like(grey)
38
+ else:
39
+ changed = cv2.absdiff(grey, self._previous) > self._threshold
40
+ mask = np.where(changed, np.uint8(255), np.uint8(0))
41
+ self._previous = grey
42
+ return mask
43
+
44
+ def reset(self) -> None:
45
+ self._previous = None
46
+
47
+
48
+ def synthetic_clip() -> list[np.ndarray]:
49
+ """Ten still frames, a square sliding right for thirty, ten still again."""
50
+ frames = []
51
+ for index in range(50):
52
+ frame = np.full((120, 240, 3), 90, dtype=np.uint8)
53
+ if 10 <= index < 40:
54
+ left = 10 + (index - 10) * 6
55
+ frame[40:80, left : left + 40] = 200
56
+ frames.append(frame)
57
+ return frames
58
+
59
+
60
+ def show(title: str, preprocessor: AdaptiveMotionPreprocessor) -> None:
61
+ print(title)
62
+ for image in preprocessor.process(synthetic_clip()):
63
+ print(
64
+ f" shape {image.data.shape}, dtype {image.data.dtype}, "
65
+ f"frames {image.frame_indices}"
66
+ )
67
+
68
+
69
+ def main() -> None:
70
+ show("Default background subtractor (KNN):", AdaptiveMotionPreprocessor())
71
+ show(
72
+ "Custom background subtractor (FrameDifferenceSubtractor):",
73
+ AdaptiveMotionPreprocessor(background_subtractor=FrameDifferenceSubtractor()),
74
+ )
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
@@ -0,0 +1,50 @@
1
+ """Run the preprocessor over a video file.
2
+
3
+ Usage:
4
+ python examples/from_video_file.py path/to/clip.mp4
5
+
6
+ Prints the shape of each motion image the moment it is produced.
7
+
8
+ Reading video is the caller's job: the package takes any iterable of
9
+ ``uint8`` BGR frames and never opens a file or camera itself. ``frames_from``
10
+ below is the few lines that bridge the two. Keep its ``try``/``finally``:
11
+ without it the capture handle leaks whenever the loop is left early.
12
+ """
13
+
14
+ import argparse
15
+ import sys
16
+
17
+ import cv2
18
+
19
+ from amprep import AdaptiveMotionPreprocessor
20
+
21
+
22
+ def frames_from(path):
23
+ capture = cv2.VideoCapture(path)
24
+ try:
25
+ if not capture.isOpened():
26
+ raise OSError(f"cannot open video: {path}")
27
+ while True:
28
+ ok, frame = capture.read()
29
+ if not ok:
30
+ return
31
+ yield frame
32
+ finally:
33
+ capture.release()
34
+
35
+
36
+ def main() -> None:
37
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
38
+ parser.add_argument("path", help="video file to read")
39
+ args = parser.parse_args()
40
+
41
+ preprocessor = AdaptiveMotionPreprocessor()
42
+ try:
43
+ for number, image in enumerate(preprocessor.process(frames_from(args.path))):
44
+ print(f"motion image {number}: shape {image.data.shape}", flush=True)
45
+ except OSError as error:
46
+ sys.exit(str(error))
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
@@ -0,0 +1,120 @@
1
+ """Watch the preprocessor work on your webcam, live.
2
+
3
+ Usage:
4
+ python examples/live_camera.py
5
+ python examples/live_camera.py --camera 1 --window-frames 15
6
+
7
+ One window shows the camera on the left and the latest motion image on
8
+ the right. Move in front of the camera: while you keep moving, a new
9
+ motion image appears every ``window_frames`` frames. Press q or Esc, or
10
+ close the window, to quit.
11
+
12
+ The first few frames only teach the background, so stay out of shot for
13
+ a moment after starting.
14
+ """
15
+
16
+ import argparse
17
+ import sys
18
+
19
+ import cv2
20
+ import numpy as np
21
+
22
+ from amprep import AdaptiveMotionPreprocessor, MotionImage
23
+
24
+ WINDOW = "amprep live: camera | motion image"
25
+ QUIT_KEYS = {ord("q"), 27} # q, Esc
26
+ MAX_DISPLAY_WIDTH = 1600
27
+ """Wider views are shrunk for the screen. Only the display, never the input."""
28
+
29
+
30
+ def compose(frame: np.ndarray, image: MotionImage | None, count: int) -> np.ndarray:
31
+ """Put the camera frame and the latest motion image side by side."""
32
+ left = frame.copy()
33
+ if image is None:
34
+ right = np.zeros_like(frame)
35
+ caption = "waiting for motion..."
36
+ else:
37
+ right = cv2.cvtColor(image.data, cv2.COLOR_GRAY2BGR)
38
+ caption = f"motion image #{count} frames {image.frame_indices}"
39
+ _label(left, "camera")
40
+ _label(right, caption)
41
+ return np.hstack([left, right])
42
+
43
+
44
+ def _label(canvas: np.ndarray, text: str) -> None:
45
+ cv2.rectangle(canvas, (0, 0), (canvas.shape[1], 28), (0, 0, 0), thickness=-1)
46
+ cv2.putText(
47
+ canvas, text, (8, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1
48
+ )
49
+
50
+
51
+ class LiveView:
52
+ """The on-screen window, plus the latest motion image to draw in it."""
53
+
54
+ def __init__(self) -> None:
55
+ self.image: MotionImage | None = None
56
+ self.count = 0
57
+
58
+ def show(self, frame: np.ndarray) -> bool:
59
+ """Draw one frame. Returns ``False`` once the user wants to quit."""
60
+ view = compose(frame, self.image, self.count)
61
+ if view.shape[1] > MAX_DISPLAY_WIDTH:
62
+ scale = MAX_DISPLAY_WIDTH / view.shape[1]
63
+ view = cv2.resize(view, None, fx=scale, fy=scale)
64
+ cv2.imshow(WINDOW, view)
65
+
66
+ if cv2.waitKey(1) & 0xFF in QUIT_KEYS:
67
+ return False
68
+ # Closing the window with its X button makes it invisible.
69
+ return cv2.getWindowProperty(WINDOW, cv2.WND_PROP_VISIBLE) >= 1
70
+
71
+
72
+ def camera_frames(index: int, view: LiveView):
73
+ """Yield webcam frames, drawing each one after the pipeline has seen it.
74
+
75
+ Drawing happens here rather than in the loop over ``process()``,
76
+ because that loop only runs when a motion image comes out, while the
77
+ live video has to update on every frame.
78
+ """
79
+ capture = cv2.VideoCapture(index)
80
+ try:
81
+ if not capture.isOpened():
82
+ raise OSError(f"cannot open camera {index}")
83
+ while True:
84
+ ok, frame = capture.read()
85
+ if not ok:
86
+ return
87
+ yield frame
88
+ # By now the pipeline has processed this frame, and any motion
89
+ # image it produced is already in the view.
90
+ if not view.show(frame):
91
+ return
92
+ finally:
93
+ capture.release()
94
+
95
+
96
+ def main() -> None:
97
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
98
+ parser.add_argument("--camera", type=int, default=0, help="camera index")
99
+ parser.add_argument("--window-frames", type=int, default=10)
100
+ parser.add_argument("--threshold", type=float, default=0.01)
101
+ args = parser.parse_args()
102
+
103
+ preprocessor = AdaptiveMotionPreprocessor(
104
+ motion_threshold=args.threshold, window_frames=args.window_frames
105
+ )
106
+ view = LiveView()
107
+ try:
108
+ for image in preprocessor.process(camera_frames(args.camera, view)):
109
+ view.image = image
110
+ view.count += 1
111
+ except OSError as error:
112
+ sys.exit(str(error))
113
+ finally:
114
+ cv2.destroyAllWindows()
115
+
116
+ print(f"{view.count} motion images")
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()