adaptive-motion-preprocessing 1.0.0rc2__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/.github/workflows/ci.yml +7 -7
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/.github/workflows/publish.yml +7 -7
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/PKG-INFO +5 -1
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/README.md +4 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/pyproject.toml +1 -1
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/frame_window.py +14 -16
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/motion_trigger.py +1 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/preprocessor.py +15 -1
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/types.py +14 -23
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_adaptive_sampler.py +2 -3
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_frame_window.py +24 -32
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_motion_history_encoder.py +1 -2
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_pipeline.py +55 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/.github/dependabot.yml +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/.gitignore +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/LICENSE +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/examples/custom_background_subtractor.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/examples/from_video_file.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/examples/live_camera.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/__init__.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/adaptive_sampler.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/background_subtractor.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/motion_history_encoder.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/noise_reducer.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/py.typed +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_background_subtractor.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_end_to_end.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_examples.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_import.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_motion_trigger.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_noise_reducer.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_plugin_contract.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_process_input.py +0 -0
- {adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_types.py +0 -0
|
@@ -23,9 +23,9 @@ jobs:
|
|
|
23
23
|
name: Lint
|
|
24
24
|
runs-on: ubuntu-latest
|
|
25
25
|
steps:
|
|
26
|
-
- uses: actions/checkout@
|
|
26
|
+
- uses: actions/checkout@v7
|
|
27
27
|
|
|
28
|
-
- uses: actions/setup-python@
|
|
28
|
+
- uses: actions/setup-python@v7
|
|
29
29
|
with:
|
|
30
30
|
python-version: "3.12"
|
|
31
31
|
cache: pip
|
|
@@ -49,9 +49,9 @@ jobs:
|
|
|
49
49
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
50
50
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
51
51
|
steps:
|
|
52
|
-
- uses: actions/checkout@
|
|
52
|
+
- uses: actions/checkout@v7
|
|
53
53
|
|
|
54
|
-
- uses: actions/setup-python@
|
|
54
|
+
- uses: actions/setup-python@v7
|
|
55
55
|
with:
|
|
56
56
|
python-version: ${{ matrix.python-version }}
|
|
57
57
|
cache: pip
|
|
@@ -73,9 +73,9 @@ jobs:
|
|
|
73
73
|
name: Build distributions
|
|
74
74
|
runs-on: ubuntu-latest
|
|
75
75
|
steps:
|
|
76
|
-
- uses: actions/checkout@
|
|
76
|
+
- uses: actions/checkout@v7
|
|
77
77
|
|
|
78
|
-
- uses: actions/setup-python@
|
|
78
|
+
- uses: actions/setup-python@v7
|
|
79
79
|
with:
|
|
80
80
|
python-version: "3.12"
|
|
81
81
|
cache: pip
|
|
@@ -90,7 +90,7 @@ jobs:
|
|
|
90
90
|
- name: Check metadata
|
|
91
91
|
run: twine check --strict dist/*
|
|
92
92
|
|
|
93
|
-
- uses: actions/upload-artifact@
|
|
93
|
+
- uses: actions/upload-artifact@v7
|
|
94
94
|
with:
|
|
95
95
|
name: dist
|
|
96
96
|
path: dist/
|
|
@@ -27,9 +27,9 @@ jobs:
|
|
|
27
27
|
is-new: ${{ steps.version.outputs.is-new }}
|
|
28
28
|
prerelease: ${{ steps.version.outputs.prerelease }}
|
|
29
29
|
steps:
|
|
30
|
-
- uses: actions/checkout@
|
|
30
|
+
- uses: actions/checkout@v7
|
|
31
31
|
|
|
32
|
-
- uses: actions/setup-python@
|
|
32
|
+
- uses: actions/setup-python@v7
|
|
33
33
|
with:
|
|
34
34
|
python-version: "3.12"
|
|
35
35
|
|
|
@@ -79,9 +79,9 @@ jobs:
|
|
|
79
79
|
if: needs.check-version.outputs.is-new == 'true'
|
|
80
80
|
runs-on: ubuntu-latest
|
|
81
81
|
steps:
|
|
82
|
-
- uses: actions/checkout@
|
|
82
|
+
- uses: actions/checkout@v7
|
|
83
83
|
|
|
84
|
-
- uses: actions/setup-python@
|
|
84
|
+
- uses: actions/setup-python@v7
|
|
85
85
|
with:
|
|
86
86
|
python-version: "3.12"
|
|
87
87
|
|
|
@@ -94,7 +94,7 @@ jobs:
|
|
|
94
94
|
- name: Check metadata
|
|
95
95
|
run: twine check --strict dist/*
|
|
96
96
|
|
|
97
|
-
- uses: actions/upload-artifact@
|
|
97
|
+
- uses: actions/upload-artifact@v7
|
|
98
98
|
with:
|
|
99
99
|
name: dist
|
|
100
100
|
path: dist/
|
|
@@ -110,7 +110,7 @@ jobs:
|
|
|
110
110
|
permissions:
|
|
111
111
|
id-token: write # required for PyPI trusted publishing
|
|
112
112
|
steps:
|
|
113
|
-
- uses: actions/download-artifact@
|
|
113
|
+
- uses: actions/download-artifact@v8
|
|
114
114
|
with:
|
|
115
115
|
name: dist
|
|
116
116
|
path: dist/
|
|
@@ -126,7 +126,7 @@ jobs:
|
|
|
126
126
|
permissions:
|
|
127
127
|
contents: write # create the tag and release
|
|
128
128
|
steps:
|
|
129
|
-
- uses: actions/download-artifact@
|
|
129
|
+
- uses: actions/download-artifact@v8
|
|
130
130
|
with:
|
|
131
131
|
name: dist
|
|
132
132
|
path: dist/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: adaptive-motion-preprocessing
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Adaptive motion preprocessing: video frames in, encoded motion images out.
|
|
5
5
|
Project-URL: Homepage, https://github.com/sdrfsh/adaptive-motion-preprocessing
|
|
6
6
|
Project-URL: Repository, https://github.com/sdrfsh/adaptive-motion-preprocessing
|
|
@@ -114,6 +114,10 @@ All optional keyword arguments of `AdaptiveMotionPreprocessor(...)`:
|
|
|
114
114
|
- 🎬 **New scene?** Call `reset()`. It forgets the background, any half-built
|
|
115
115
|
window and the frame size. Otherwise state carries over between
|
|
116
116
|
`process()` calls.
|
|
117
|
+
- 🎚️ **Tuning the threshold.** `preprocessor.last_motion_fraction` is the
|
|
118
|
+
share of the latest frame that was moving. Read it once with the scene
|
|
119
|
+
still and once with something moving, and set `motion_threshold`
|
|
120
|
+
between the two.
|
|
117
121
|
- 📐 **One frame size per scene.** Frames that change size mid-stream raise a
|
|
118
122
|
`ValueError`. Call `reset()` first if the change is on purpose.
|
|
119
123
|
|
|
@@ -84,6 +84,10 @@ All optional keyword arguments of `AdaptiveMotionPreprocessor(...)`:
|
|
|
84
84
|
- 🎬 **New scene?** Call `reset()`. It forgets the background, any half-built
|
|
85
85
|
window and the frame size. Otherwise state carries over between
|
|
86
86
|
`process()` calls.
|
|
87
|
+
- 🎚️ **Tuning the threshold.** `preprocessor.last_motion_fraction` is the
|
|
88
|
+
share of the latest frame that was moving. Read it once with the scene
|
|
89
|
+
still and once with something moving, and set `motion_threshold`
|
|
90
|
+
between the two.
|
|
87
91
|
- 📐 **One frame size per scene.** Frames that change size mid-stream raise a
|
|
88
92
|
`ValueError`. Call `reset()` first if the change is on purpose.
|
|
89
93
|
|
|
@@ -15,8 +15,8 @@ the two is the caller's arithmetic.
|
|
|
15
15
|
MIN_WINDOW_FRAMES = 2
|
|
16
16
|
"""Fewest frames a window can be asked to hold.
|
|
17
17
|
|
|
18
|
-
The adaptive sampler
|
|
19
|
-
|
|
18
|
+
The adaptive sampler measures change between consecutive masks, which
|
|
19
|
+
needs at least one pair of frames to be a comparison at all.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
|
|
@@ -32,9 +32,9 @@ class FrameWindowCollector:
|
|
|
32
32
|
trigger: idle ACTIVE x 10 ACTIVE x 10 idle
|
|
33
33
|
window: [1..10] -> out [1..10] -> out (partial, dropped)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
Only the foreground masks are kept. Both stages downstream, the
|
|
36
|
+
sampler and the encoder, read masks alone, so holding on to the
|
|
37
|
+
full-colour frames as well would cost memory for nothing.
|
|
38
38
|
|
|
39
39
|
A partial window is discarded when motion stops. Velocity is measured
|
|
40
40
|
across a window, so a window cut short describes a slower movement
|
|
@@ -74,7 +74,7 @@ class FrameWindowCollector:
|
|
|
74
74
|
Zero between windows as well as while idle: a window that is
|
|
75
75
|
handed over leaves nothing behind it.
|
|
76
76
|
"""
|
|
77
|
-
return len(self.
|
|
77
|
+
return len(self._masks)
|
|
78
78
|
|
|
79
79
|
def reset(self) -> None:
|
|
80
80
|
"""Drop whatever was being collected and start again empty.
|
|
@@ -93,15 +93,14 @@ class FrameWindowCollector:
|
|
|
93
93
|
just handed a full one over. Should ``reset`` ever grow behaviour
|
|
94
94
|
that belongs to a new scene, it must not fire on those.
|
|
95
95
|
"""
|
|
96
|
-
self._frames: list[Frame] = []
|
|
97
96
|
self._masks: list[Frame] = []
|
|
98
97
|
|
|
99
|
-
def update(self,
|
|
100
|
-
"""Take one frame, and return a window if
|
|
98
|
+
def update(self, mask: Frame, active: bool) -> Window | None:
|
|
99
|
+
"""Take one frame's mask, and return a window if it completed one.
|
|
101
100
|
|
|
102
101
|
Args:
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
mask: The foreground mask of the frame captured at this
|
|
103
|
+
moment, from the background subtractor.
|
|
105
104
|
active: The trigger's verdict on this frame. ``False`` drops
|
|
106
105
|
this frame and discards anything part-collected with it.
|
|
107
106
|
|
|
@@ -111,18 +110,17 @@ class FrameWindowCollector:
|
|
|
111
110
|
so consecutive windows share no frames.
|
|
112
111
|
"""
|
|
113
112
|
if not active:
|
|
114
|
-
if self.
|
|
115
|
-
_log.debug("motion ended, dropping %d frames", len(self.
|
|
113
|
+
if self._masks:
|
|
114
|
+
_log.debug("motion ended, dropping %d frames", len(self._masks))
|
|
116
115
|
self._clear()
|
|
117
116
|
return None
|
|
118
117
|
|
|
119
|
-
self._frames.append(frame)
|
|
120
118
|
self._masks.append(mask)
|
|
121
119
|
|
|
122
|
-
if len(self.
|
|
120
|
+
if len(self._masks) < self._window_frames:
|
|
123
121
|
return None
|
|
124
122
|
|
|
125
|
-
window = Window(
|
|
123
|
+
window = Window(masks=tuple(self._masks))
|
|
126
124
|
# Emptied rather than kept, so the next frame opens the next
|
|
127
125
|
# window: that is what "no cooldown" means here.
|
|
128
126
|
self._clear()
|
|
@@ -88,6 +88,20 @@ class AdaptiveMotionPreprocessor:
|
|
|
88
88
|
self._frames_seen = 0
|
|
89
89
|
self._frame_shape: tuple[int, ...] | None = None
|
|
90
90
|
|
|
91
|
+
@property
|
|
92
|
+
def last_motion_fraction(self) -> float:
|
|
93
|
+
"""Share of the most recent frame that was moving, in ``[0, 1]``.
|
|
94
|
+
|
|
95
|
+
The reading ``motion_threshold`` is compared against, so this is
|
|
96
|
+
the number to watch when tuning it on real footage: print it while
|
|
97
|
+
the scene is still and while something moves, and set the
|
|
98
|
+
threshold between the two.
|
|
99
|
+
|
|
100
|
+
Zero before the first frame is measured, during the background
|
|
101
|
+
subtractor's warm-up, and after ``reset()``.
|
|
102
|
+
"""
|
|
103
|
+
return self._trigger.last_fraction
|
|
104
|
+
|
|
91
105
|
def process(self, frames: Iterable[Frame]) -> Iterator[MotionImage]:
|
|
92
106
|
"""Turn a stream of frames into encoded motion images.
|
|
93
107
|
|
|
@@ -135,7 +149,7 @@ class AdaptiveMotionPreprocessor:
|
|
|
135
149
|
continue
|
|
136
150
|
|
|
137
151
|
active = self._trigger.update(mask)
|
|
138
|
-
window = self._collector.update(
|
|
152
|
+
window = self._collector.update(mask, active)
|
|
139
153
|
if window is not None:
|
|
140
154
|
yield self._encoder.encode(window, self._sampler.select(window))
|
|
141
155
|
|
{adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/types.py
RENAMED
|
@@ -54,41 +54,32 @@ class MotionImage:
|
|
|
54
54
|
|
|
55
55
|
@dataclass(frozen=True)
|
|
56
56
|
class Window:
|
|
57
|
-
"""A full run of consecutive
|
|
57
|
+
"""A full run of consecutive foreground masks, collected while motion lasted.
|
|
58
58
|
|
|
59
59
|
The handover between the collector and the two stages that read it:
|
|
60
|
-
the adaptive sampler measures how fast
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
from.
|
|
60
|
+
the adaptive sampler measures how fast the foreground changes across
|
|
61
|
+
``masks``, and the encoder paints the masks the sampler picked. Both
|
|
62
|
+
work on masks alone, so the frames themselves are not carried: a
|
|
63
|
+
window of full-colour frames would cost memory and nothing would
|
|
64
|
+
read it.
|
|
66
65
|
|
|
67
66
|
Contract:
|
|
68
|
-
``
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
whole one.
|
|
67
|
+
``masks`` is in capture order, one per frame, each a ``uint8``
|
|
68
|
+
``(H, W)`` array. A window is never empty and never partial: the
|
|
69
|
+
collector emits one only once it is full, so a stage reading this
|
|
70
|
+
never has to ask whether it got a whole one.
|
|
73
71
|
|
|
74
72
|
Attributes:
|
|
75
|
-
|
|
76
|
-
masks: The foreground mask for each frame, in the same order.
|
|
73
|
+
masks: The foreground mask of each frame, in capture order.
|
|
77
74
|
"""
|
|
78
75
|
|
|
79
|
-
frames: tuple[Frame, ...]
|
|
80
76
|
masks: tuple[Frame, ...]
|
|
81
77
|
|
|
82
78
|
def __post_init__(self) -> None:
|
|
83
|
-
"""Ensure
|
|
84
|
-
if
|
|
85
|
-
raise ValueError(
|
|
86
|
-
f"Window needs one mask per frame, got {len(self.frames)} "
|
|
87
|
-
f"frames and {len(self.masks)} masks"
|
|
88
|
-
)
|
|
89
|
-
if not self.frames:
|
|
79
|
+
"""Ensure there is anything here."""
|
|
80
|
+
if not self.masks:
|
|
90
81
|
raise ValueError("Window must not be empty")
|
|
91
82
|
|
|
92
83
|
def __len__(self) -> int:
|
|
93
84
|
"""The number of frames in the window."""
|
|
94
|
-
return len(self.
|
|
85
|
+
return len(self.masks)
|
|
@@ -27,9 +27,8 @@ standing still: a property of the scaffold, not of the sampler.
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _window(masks: list[np.ndarray]) -> Window:
|
|
30
|
-
"""Wrap masks in a window
|
|
31
|
-
|
|
32
|
-
return Window(frames=blanks, masks=tuple(masks))
|
|
30
|
+
"""Wrap masks in a window."""
|
|
31
|
+
return Window(masks=tuple(masks))
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
def _moving_window(frames: int, pixels_per_frame: float) -> Window:
|
|
@@ -13,27 +13,19 @@ SHAPE = (4, 4)
|
|
|
13
13
|
"""Small enough to be free, large enough to be a real array."""
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def _frame(value: int) -> Frame:
|
|
17
|
-
"""A frame carrying ``value``, so its place in a window is readable."""
|
|
18
|
-
return np.full((*SHAPE, 3), value, dtype=np.uint8)
|
|
19
|
-
|
|
20
|
-
|
|
21
16
|
def _mask(value: int) -> Frame:
|
|
22
|
-
"""
|
|
17
|
+
"""A mask carrying ``value``, so its place in a window is readable."""
|
|
23
18
|
return np.full(SHAPE, value, dtype=np.uint8)
|
|
24
19
|
|
|
25
20
|
|
|
26
21
|
def _feed(collector: FrameWindowCollector, count: int, active: bool = True, start=0):
|
|
27
22
|
"""Push ``count`` numbered frames through and return what came out."""
|
|
28
|
-
return [
|
|
29
|
-
collector.update(_frame(i), _mask(i), active)
|
|
30
|
-
for i in range(start, start + count)
|
|
31
|
-
]
|
|
23
|
+
return [collector.update(_mask(i), active) for i in range(start, start + count)]
|
|
32
24
|
|
|
33
25
|
|
|
34
26
|
def _values(window: Window) -> list[int]:
|
|
35
|
-
"""The numbers the window's
|
|
36
|
-
return [int(
|
|
27
|
+
"""The numbers the window's masks were built from, in order."""
|
|
28
|
+
return [int(mask[0, 0]) for mask in window.masks]
|
|
37
29
|
|
|
38
30
|
|
|
39
31
|
def test_default_window_is_ten_frames():
|
|
@@ -102,15 +94,21 @@ def test_the_window_carries_its_frames_in_order():
|
|
|
102
94
|
assert _values(window) == [0, 1, 2, 3]
|
|
103
95
|
|
|
104
96
|
|
|
105
|
-
def
|
|
106
|
-
"""
|
|
107
|
-
collector = FrameWindowCollector(window_frames=
|
|
97
|
+
def test_the_window_holds_the_masks_it_was_given():
|
|
98
|
+
"""The very arrays handed in come out, not copies or derivations."""
|
|
99
|
+
collector = FrameWindowCollector(window_frames=3)
|
|
100
|
+
masks = [_mask(i) for i in range(3)]
|
|
108
101
|
|
|
109
|
-
window =
|
|
102
|
+
window = [collector.update(mask, True) for mask in masks][-1]
|
|
110
103
|
|
|
111
|
-
assert
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
assert all(a is b for a, b in zip(window.masks, masks, strict=True))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_the_window_carries_no_frames():
|
|
108
|
+
"""Nothing downstream reads frames, so none are held in memory."""
|
|
109
|
+
window = _feed(FrameWindowCollector(window_frames=2), 2)[-1]
|
|
110
|
+
|
|
111
|
+
assert not hasattr(window, "frames")
|
|
114
112
|
|
|
115
113
|
|
|
116
114
|
def test_the_next_window_starts_on_the_very_next_frame():
|
|
@@ -151,7 +149,7 @@ def test_a_partial_window_is_discarded_when_motion_stops():
|
|
|
151
149
|
|
|
152
150
|
assert collector.pending == 6
|
|
153
151
|
|
|
154
|
-
assert collector.update(
|
|
152
|
+
assert collector.update(_mask(99), active=False) is None
|
|
155
153
|
assert collector.pending == 0
|
|
156
154
|
|
|
157
155
|
|
|
@@ -159,7 +157,7 @@ def test_a_discarded_window_is_never_emitted_later():
|
|
|
159
157
|
"""The dropped fragment does not reappear at the front of the next window."""
|
|
160
158
|
collector = FrameWindowCollector(window_frames=4)
|
|
161
159
|
_feed(collector, 3)
|
|
162
|
-
collector.update(
|
|
160
|
+
collector.update(_mask(99), active=False)
|
|
163
161
|
|
|
164
162
|
window = _feed(collector, 4, start=10)[-1]
|
|
165
163
|
|
|
@@ -179,7 +177,7 @@ def test_motion_stopping_on_a_full_window_loses_nothing():
|
|
|
179
177
|
collector = FrameWindowCollector(window_frames=3)
|
|
180
178
|
|
|
181
179
|
results = _feed(collector, 3)
|
|
182
|
-
after = collector.update(
|
|
180
|
+
after = collector.update(_mask(99), active=False)
|
|
183
181
|
|
|
184
182
|
assert isinstance(results[-1], Window)
|
|
185
183
|
assert after is None
|
|
@@ -190,7 +188,7 @@ def test_bursts_are_collected_independently():
|
|
|
190
188
|
collector = FrameWindowCollector(window_frames=3)
|
|
191
189
|
|
|
192
190
|
_feed(collector, 2)
|
|
193
|
-
collector.update(
|
|
191
|
+
collector.update(_mask(99), active=False)
|
|
194
192
|
window = _feed(collector, 3, start=20)[-1]
|
|
195
193
|
|
|
196
194
|
assert _values(window) == [20, 21, 22]
|
|
@@ -229,21 +227,15 @@ def test_the_properties_are_read_only(name):
|
|
|
229
227
|
setattr(collector, name, 3)
|
|
230
228
|
|
|
231
229
|
|
|
232
|
-
def test_window_rejects_a_mask_count_that_does_not_match():
|
|
233
|
-
"""A window with a frame missing its mask is not constructible."""
|
|
234
|
-
with pytest.raises(ValueError, match="one mask per frame"):
|
|
235
|
-
Window(frames=(_frame(0), _frame(1)), masks=(_mask(0),))
|
|
236
|
-
|
|
237
|
-
|
|
238
230
|
def test_window_rejects_being_empty():
|
|
239
231
|
"""An empty window describes nothing and is never emitted."""
|
|
240
232
|
with pytest.raises(ValueError, match="must not be empty"):
|
|
241
|
-
Window(
|
|
233
|
+
Window(masks=())
|
|
242
234
|
|
|
243
235
|
|
|
244
236
|
def test_window_is_frozen():
|
|
245
237
|
"""A window is a handover, not a workspace for the stage that gets it."""
|
|
246
|
-
window = Window(
|
|
238
|
+
window = Window(masks=(_mask(0),))
|
|
247
239
|
|
|
248
240
|
with pytest.raises(AttributeError):
|
|
249
|
-
window.
|
|
241
|
+
window.masks = ()
|
|
@@ -173,3 +173,58 @@ def test_reset_allows_a_new_frame_size():
|
|
|
173
173
|
preprocessor.reset()
|
|
174
174
|
|
|
175
175
|
list(preprocessor.process([np.zeros((30, 40, 3), np.uint8)] * 3)) # no error
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---- last_motion_fraction ----
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_last_motion_fraction_starts_at_zero():
|
|
182
|
+
assert AdaptiveMotionPreprocessor().last_motion_fraction == 0.0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_last_motion_fraction_reads_the_latest_frame():
|
|
186
|
+
"""The block is 20x20 of a 60x80 frame, so about a twelfth of it moves.
|
|
187
|
+
|
|
188
|
+
About, because the default median filter rounds the block's corners
|
|
189
|
+
off before the mask is taken.
|
|
190
|
+
"""
|
|
191
|
+
preprocessor = AdaptiveMotionPreprocessor(background_subtractor=_Threshold())
|
|
192
|
+
moving = _scene(0, 1, 0)
|
|
193
|
+
|
|
194
|
+
list(preprocessor.process(moving))
|
|
195
|
+
|
|
196
|
+
assert preprocessor.last_motion_fraction == pytest.approx(
|
|
197
|
+
20 * 20 / (H * W), rel=0.05
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_last_motion_fraction_drops_to_zero_on_a_still_frame():
|
|
202
|
+
preprocessor = AdaptiveMotionPreprocessor(background_subtractor=_Threshold())
|
|
203
|
+
|
|
204
|
+
list(preprocessor.process(_scene(0, 1, 0)))
|
|
205
|
+
list(preprocessor.process([np.full((H, W, 3), 120, np.uint8)]))
|
|
206
|
+
|
|
207
|
+
assert preprocessor.last_motion_fraction == 0.0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_last_motion_fraction_is_untouched_during_warmup():
|
|
211
|
+
"""Warm-up masks are not trusted, so they are not reported either."""
|
|
212
|
+
preprocessor = AdaptiveMotionPreprocessor()
|
|
213
|
+
|
|
214
|
+
list(preprocessor.process(_scene(0, 4, 0))) # all four are warm-up
|
|
215
|
+
|
|
216
|
+
assert preprocessor.last_motion_fraction == 0.0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_reset_clears_last_motion_fraction():
|
|
220
|
+
preprocessor = AdaptiveMotionPreprocessor(background_subtractor=_Threshold())
|
|
221
|
+
list(preprocessor.process(_scene(0, 1, 0)))
|
|
222
|
+
|
|
223
|
+
preprocessor.reset()
|
|
224
|
+
|
|
225
|
+
assert preprocessor.last_motion_fraction == 0.0
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_last_motion_fraction_is_read_only():
|
|
229
|
+
with pytest.raises(AttributeError):
|
|
230
|
+
AdaptiveMotionPreprocessor().last_motion_fraction = 0.5
|
|
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
|
{adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/src/amprep/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_import.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{adaptive_motion_preprocessing-1.0.0rc2 → adaptive_motion_preprocessing-1.1.0}/tests/test_types.py
RENAMED
|
File without changes
|