pixelflow 0.3.2__tar.gz → 0.5.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.
- {pixelflow-0.3.2 → pixelflow-0.5.0}/PKG-INFO +51 -21
- {pixelflow-0.3.2 → pixelflow-0.5.0}/README.md +50 -20
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/__init__.py +33 -5
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/detections/detections.py +33 -3
- pixelflow-0.5.0/pixelflow/media.py +961 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/transforms/__init__.py +3 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/transforms/detections.py +6 -2
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/transforms/image.py +74 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/validators.py +12 -1
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow.egg-info/PKG-INFO +51 -21
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow.egg-info/SOURCES.txt +0 -1
- pixelflow-0.3.2/pixelflow/assets.py +0 -378
- pixelflow-0.3.2/pixelflow/media.py +0 -458
- {pixelflow-0.3.2 → pixelflow-0.5.0}/LICENSE +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/__init__.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/anchors.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/blur.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/box.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/classification.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/crossing.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/dot.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/filled_box.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/fps_counter.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/grid_overlay.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/heatmap.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/keypoint.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/keypoint_skeleton.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/label.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/mask.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/motion_dots.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/motion_trails.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/oval.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/pixelate.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/polygon.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/scale_bar.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/utils.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/annotators/zones.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/arrays.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/buffer.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/classifications/__init__.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/classifications/classifications.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/classifications/converters.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/colors.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/crossings.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/detections/__init__.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/detections/converters.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/detections/filters.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/labels.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/slicer.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/smoother.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/strategies.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/timer.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/tracker/__init__.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/tracker/bytetrack.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/tracker/kalman_filter.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/tracker/matching.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/tracker/track.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/transforms/inverse.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow/zones.py +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow.egg-info/dependency_links.txt +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow.egg-info/requires.txt +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pixelflow.egg-info/top_level.txt +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/pyproject.toml +0 -0
- {pixelflow-0.3.2 → pixelflow-0.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pixelflow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Computer vision processing library for visualizing and analyzing detection results from ML models
|
|
5
5
|
Author-email: Emrah Nazif <emrah@datamarkin.com>, Datamarkin <support@datamarkin.com>
|
|
6
6
|
License: MIT
|
|
@@ -52,12 +52,13 @@ import pixelflow as pf
|
|
|
52
52
|
from ultralytics import YOLO
|
|
53
53
|
|
|
54
54
|
model = YOLO("yolo11n.pt")
|
|
55
|
-
video = pf.VideoReader("traffic.mp4"
|
|
55
|
+
video = pf.VideoReader("traffic.mp4")
|
|
56
56
|
tracker = pf.tracker.ByteTracker()
|
|
57
57
|
zones = pf.Zones()
|
|
58
58
|
zones.add_zone([(100, 400), (500, 400), (500, 600), (100, 600)], zone_id="entrance")
|
|
59
59
|
|
|
60
60
|
for frame in video:
|
|
61
|
+
frame = pf.transform.resize(frame, width=640)
|
|
61
62
|
detections = pf.from_ultralytics(model.predict(frame))
|
|
62
63
|
detections = tracker.update(detections)
|
|
63
64
|
zones.update(detections)
|
|
@@ -66,8 +67,7 @@ for frame in video:
|
|
|
66
67
|
frame = pf.annotate.label(frame, detections)
|
|
67
68
|
frame = pf.annotate.zones(frame, zones)
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
break
|
|
70
|
+
pf.display_video(frame, "Live") # raises DisplayExit when 'q' is pressed
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
## Installation
|
|
@@ -200,34 +200,64 @@ english_text = detections.filter_by_text_language("en")
|
|
|
200
200
|
|
|
201
201
|
### Media Handling
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
A source is anything that yields RGB `uint8` `HxWx3` arrays. `VideoReader` and
|
|
204
|
+
`CameraStream` are conveniences, not gates — a list, a generator, or your own reader
|
|
205
|
+
for the one camera that needs special handling all work identically everywhere else.
|
|
204
206
|
|
|
205
207
|
```python
|
|
206
208
|
import pixelflow as pf
|
|
207
209
|
|
|
208
210
|
# Read a video file
|
|
209
|
-
video = pf.VideoReader("video.mp4"
|
|
210
|
-
print(f"{video.width}x{video.height}, {video.fps}fps, {
|
|
211
|
+
video = pf.VideoReader("video.mp4")
|
|
212
|
+
print(f"{video.width}x{video.height}, {video.fps}fps, {video.frames} frames")
|
|
211
213
|
|
|
212
|
-
for frame in video:
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
for frame in video:
|
|
215
|
+
pf.display_video(frame, "Preview")
|
|
216
|
+
|
|
217
|
+
video.seek(0) # replay is explicit; iteration never rewinds on its own
|
|
215
218
|
|
|
216
|
-
#
|
|
217
|
-
|
|
219
|
+
# Every 5th frame. fps and frames are divided to match, so a writer built from
|
|
220
|
+
# this source stays correct.
|
|
221
|
+
video = pf.VideoReader("video.mp4", stride=5)
|
|
218
222
|
|
|
219
|
-
#
|
|
220
|
-
|
|
223
|
+
# Load a single image (EXIF orientation applied, always RGB uint8 HxWx3)
|
|
224
|
+
image = pf.read_image("photo.jpg")
|
|
225
|
+
|
|
226
|
+
# Webcam / network stream — same facts, same iteration, is_live is True
|
|
227
|
+
cam = pf.CameraStream(0)
|
|
221
228
|
cam = pf.CameraStream("rtsp://camera.local/stream")
|
|
229
|
+
```
|
|
222
230
|
|
|
223
|
-
|
|
224
|
-
|
|
231
|
+
Reading and resizing are two jobs — compose them:
|
|
232
|
+
|
|
233
|
+
```python
|
|
225
234
|
for frame in video:
|
|
226
|
-
|
|
227
|
-
writer.write(processed)
|
|
228
|
-
writer.close()
|
|
235
|
+
frame = pf.transform.resize(frame, width=640) # or height=
|
|
229
236
|
```
|
|
230
237
|
|
|
238
|
+
**Writing.** `like=` carries the frame rate across from a source, which is the one
|
|
239
|
+
value that has to and the one that is easy to get wrong — a strided reader yields
|
|
240
|
+
fewer frames per second than its file contains. Size is *not* carried: it comes from
|
|
241
|
+
the first frame written, because the loop in between is allowed to resize.
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
video = pf.VideoReader("input.mp4", stride=2)
|
|
245
|
+
|
|
246
|
+
with pf.VideoWriter("output.mp4", like=video) as writer:
|
|
247
|
+
for frame in video:
|
|
248
|
+
writer.write(process(frame))
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Every source reports the same facts, so consumers never branch on the source type:
|
|
252
|
+
|
|
253
|
+
| | `VideoReader` | `CameraStream` |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| `width` / `height` | from the file | from a frame decoded at open |
|
|
256
|
+
| `fps` | rate yielded, `None` if unknown | usually `None` — devices report 0 |
|
|
257
|
+
| `frames` | estimate, divided by stride | always `None` |
|
|
258
|
+
| `duration` | seconds, unaffected by stride | always `None` |
|
|
259
|
+
| `is_live` | `False` | `True` |
|
|
260
|
+
|
|
231
261
|
### Zone-Based Analytics
|
|
232
262
|
|
|
233
263
|
Define spatial regions and track what enters them:
|
|
@@ -280,7 +310,7 @@ import pixelflow as pf
|
|
|
280
310
|
|
|
281
311
|
tracker = pf.tracker.ByteTracker()
|
|
282
312
|
|
|
283
|
-
for frame in
|
|
313
|
+
for frame in video:
|
|
284
314
|
detections = pf.from_ultralytics(model.predict(frame))
|
|
285
315
|
detections = tracker.update(detections)
|
|
286
316
|
|
|
@@ -383,7 +413,7 @@ crossings.add_line(
|
|
|
383
413
|
direction="down" # Only count downward crossings
|
|
384
414
|
)
|
|
385
415
|
|
|
386
|
-
for frame in
|
|
416
|
+
for frame in video:
|
|
387
417
|
detections = tracker.update(pf.from_ultralytics(model.predict(frame)))
|
|
388
418
|
crossings.update(detections)
|
|
389
419
|
|
|
@@ -14,12 +14,13 @@ import pixelflow as pf
|
|
|
14
14
|
from ultralytics import YOLO
|
|
15
15
|
|
|
16
16
|
model = YOLO("yolo11n.pt")
|
|
17
|
-
video = pf.VideoReader("traffic.mp4"
|
|
17
|
+
video = pf.VideoReader("traffic.mp4")
|
|
18
18
|
tracker = pf.tracker.ByteTracker()
|
|
19
19
|
zones = pf.Zones()
|
|
20
20
|
zones.add_zone([(100, 400), (500, 400), (500, 600), (100, 600)], zone_id="entrance")
|
|
21
21
|
|
|
22
22
|
for frame in video:
|
|
23
|
+
frame = pf.transform.resize(frame, width=640)
|
|
23
24
|
detections = pf.from_ultralytics(model.predict(frame))
|
|
24
25
|
detections = tracker.update(detections)
|
|
25
26
|
zones.update(detections)
|
|
@@ -28,8 +29,7 @@ for frame in video:
|
|
|
28
29
|
frame = pf.annotate.label(frame, detections)
|
|
29
30
|
frame = pf.annotate.zones(frame, zones)
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
break
|
|
32
|
+
pf.display_video(frame, "Live") # raises DisplayExit when 'q' is pressed
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
@@ -162,34 +162,64 @@ english_text = detections.filter_by_text_language("en")
|
|
|
162
162
|
|
|
163
163
|
### Media Handling
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
A source is anything that yields RGB `uint8` `HxWx3` arrays. `VideoReader` and
|
|
166
|
+
`CameraStream` are conveniences, not gates — a list, a generator, or your own reader
|
|
167
|
+
for the one camera that needs special handling all work identically everywhere else.
|
|
166
168
|
|
|
167
169
|
```python
|
|
168
170
|
import pixelflow as pf
|
|
169
171
|
|
|
170
172
|
# Read a video file
|
|
171
|
-
video = pf.VideoReader("video.mp4"
|
|
172
|
-
print(f"{video.width}x{video.height}, {video.fps}fps, {
|
|
173
|
+
video = pf.VideoReader("video.mp4")
|
|
174
|
+
print(f"{video.width}x{video.height}, {video.fps}fps, {video.frames} frames")
|
|
173
175
|
|
|
174
|
-
for frame in video:
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
for frame in video:
|
|
177
|
+
pf.display_video(frame, "Preview")
|
|
178
|
+
|
|
179
|
+
video.seek(0) # replay is explicit; iteration never rewinds on its own
|
|
177
180
|
|
|
178
|
-
#
|
|
179
|
-
|
|
181
|
+
# Every 5th frame. fps and frames are divided to match, so a writer built from
|
|
182
|
+
# this source stays correct.
|
|
183
|
+
video = pf.VideoReader("video.mp4", stride=5)
|
|
180
184
|
|
|
181
|
-
#
|
|
182
|
-
|
|
185
|
+
# Load a single image (EXIF orientation applied, always RGB uint8 HxWx3)
|
|
186
|
+
image = pf.read_image("photo.jpg")
|
|
187
|
+
|
|
188
|
+
# Webcam / network stream — same facts, same iteration, is_live is True
|
|
189
|
+
cam = pf.CameraStream(0)
|
|
183
190
|
cam = pf.CameraStream("rtsp://camera.local/stream")
|
|
191
|
+
```
|
|
184
192
|
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
Reading and resizing are two jobs — compose them:
|
|
194
|
+
|
|
195
|
+
```python
|
|
187
196
|
for frame in video:
|
|
188
|
-
|
|
189
|
-
writer.write(processed)
|
|
190
|
-
writer.close()
|
|
197
|
+
frame = pf.transform.resize(frame, width=640) # or height=
|
|
191
198
|
```
|
|
192
199
|
|
|
200
|
+
**Writing.** `like=` carries the frame rate across from a source, which is the one
|
|
201
|
+
value that has to and the one that is easy to get wrong — a strided reader yields
|
|
202
|
+
fewer frames per second than its file contains. Size is *not* carried: it comes from
|
|
203
|
+
the first frame written, because the loop in between is allowed to resize.
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
video = pf.VideoReader("input.mp4", stride=2)
|
|
207
|
+
|
|
208
|
+
with pf.VideoWriter("output.mp4", like=video) as writer:
|
|
209
|
+
for frame in video:
|
|
210
|
+
writer.write(process(frame))
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Every source reports the same facts, so consumers never branch on the source type:
|
|
214
|
+
|
|
215
|
+
| | `VideoReader` | `CameraStream` |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| `width` / `height` | from the file | from a frame decoded at open |
|
|
218
|
+
| `fps` | rate yielded, `None` if unknown | usually `None` — devices report 0 |
|
|
219
|
+
| `frames` | estimate, divided by stride | always `None` |
|
|
220
|
+
| `duration` | seconds, unaffected by stride | always `None` |
|
|
221
|
+
| `is_live` | `False` | `True` |
|
|
222
|
+
|
|
193
223
|
### Zone-Based Analytics
|
|
194
224
|
|
|
195
225
|
Define spatial regions and track what enters them:
|
|
@@ -242,7 +272,7 @@ import pixelflow as pf
|
|
|
242
272
|
|
|
243
273
|
tracker = pf.tracker.ByteTracker()
|
|
244
274
|
|
|
245
|
-
for frame in
|
|
275
|
+
for frame in video:
|
|
246
276
|
detections = pf.from_ultralytics(model.predict(frame))
|
|
247
277
|
detections = tracker.update(detections)
|
|
248
278
|
|
|
@@ -345,7 +375,7 @@ crossings.add_line(
|
|
|
345
375
|
direction="down" # Only count downward crossings
|
|
346
376
|
)
|
|
347
377
|
|
|
348
|
-
for frame in
|
|
378
|
+
for frame in video:
|
|
349
379
|
detections = tracker.update(pf.from_ultralytics(model.predict(frame)))
|
|
350
380
|
crossings.update(detections)
|
|
351
381
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.5.0"
|
|
2
2
|
__author__ = "Datamarkin"
|
|
3
3
|
|
|
4
4
|
# Import core modules
|
|
@@ -9,8 +9,6 @@ from . import colors
|
|
|
9
9
|
from . import slicer
|
|
10
10
|
from . import smoother
|
|
11
11
|
from . import timer
|
|
12
|
-
from . import tracker
|
|
13
|
-
from . import assets
|
|
14
12
|
|
|
15
13
|
# Result types and their converters. Every model output PixelFlow understands becomes
|
|
16
14
|
# one of two things: Detections for anything that localises, Classifications for
|
|
@@ -28,7 +26,7 @@ from .classifications import __all__ as _CLASSIFICATION_EXPORTS
|
|
|
28
26
|
# Import specific functions for top-level access
|
|
29
27
|
from .media import (
|
|
30
28
|
DisplayExit, VideoReader, CameraStream, VideoWriter,
|
|
31
|
-
read_image, read_video, display_video, display_image, save_image,
|
|
29
|
+
read_image, read_video, encode_image, display_video, display_image, save_image,
|
|
32
30
|
close_display, to_pil, from_pil,
|
|
33
31
|
)
|
|
34
32
|
from .zones import Zones
|
|
@@ -38,6 +36,36 @@ from .buffer import Buffer
|
|
|
38
36
|
from .smoother import smooth
|
|
39
37
|
from .timer import TimeTracker
|
|
40
38
|
|
|
39
|
+
# ByteTrack is the only thing in PixelFlow that needs SciPy, and SciPy is most of what importing
|
|
40
|
+
# PixelFlow costs: 436 ms of 631 ms, from scipy.linalg in the Kalman filter and scipy.optimize in
|
|
41
|
+
# the assignment step. Every caller that only draws boxes or converts a model's output paid it.
|
|
42
|
+
#
|
|
43
|
+
# So `tracker` loads on first use instead of at import. This is PEP 562, the language feature for
|
|
44
|
+
# exactly this, and it is what SciPy and NumPy do with their own subpackages -- `pf.tracker`,
|
|
45
|
+
# `from pixelflow import tracker` and `import pixelflow.tracker` all still work, and the module is
|
|
46
|
+
# cached in globals() after the first access so the cost is paid once.
|
|
47
|
+
#
|
|
48
|
+
# Nothing else in PixelFlow imports tracker, and it publishes no top-level names of its own, which
|
|
49
|
+
# is what makes this a one-line deferral rather than a lazy-name table.
|
|
50
|
+
_LAZY = {"tracker"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def __getattr__(name):
|
|
54
|
+
"""Load a deferred subpackage on first access. See the note above."""
|
|
55
|
+
if name in _LAZY:
|
|
56
|
+
import importlib
|
|
57
|
+
|
|
58
|
+
module = importlib.import_module(f".{name}", __name__)
|
|
59
|
+
globals()[name] = module
|
|
60
|
+
return module
|
|
61
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def __dir__():
|
|
65
|
+
"""Keep the deferred subpackages visible to `dir()` and to tab completion."""
|
|
66
|
+
return sorted(set(globals()) | _LAZY)
|
|
67
|
+
|
|
68
|
+
|
|
41
69
|
# Define the public API
|
|
42
70
|
__all__ = [
|
|
43
71
|
# Result types and converters, declared by the subpackages that own them
|
|
@@ -59,6 +87,7 @@ __all__ = [
|
|
|
59
87
|
"VideoWriter",
|
|
60
88
|
"read_image",
|
|
61
89
|
"read_video",
|
|
90
|
+
"encode_image",
|
|
62
91
|
"display_video",
|
|
63
92
|
"display_image",
|
|
64
93
|
"save_image",
|
|
@@ -73,7 +102,6 @@ __all__ = [
|
|
|
73
102
|
"Crossings",
|
|
74
103
|
|
|
75
104
|
# Processing utilities
|
|
76
|
-
"assets",
|
|
77
105
|
"slicer",
|
|
78
106
|
"SlicedInference",
|
|
79
107
|
"auto_slice_size",
|
|
@@ -9,6 +9,7 @@ different ML frameworks and visualization tools.
|
|
|
9
9
|
|
|
10
10
|
import copy as copy_module
|
|
11
11
|
import io
|
|
12
|
+
import math
|
|
12
13
|
import json
|
|
13
14
|
import base64
|
|
14
15
|
import numpy as np
|
|
@@ -661,13 +662,18 @@ class Detections:
|
|
|
661
662
|
Appends a Detection instance to the internal collection, enabling batch
|
|
662
663
|
processing and filtering operations on the complete detection set.
|
|
663
664
|
|
|
665
|
+
A detection that does not describe an object is dropped rather than stored:
|
|
666
|
+
one whose `bbox` is None -- never set, or rejected by `validate_bbox` as
|
|
667
|
+
non-finite, inverted or enclosing no pixels -- and one whose confidence is
|
|
668
|
+
NaN, which no threshold can compare against and which strict JSON parsers
|
|
669
|
+
refuse. Dropping at the boundary is what lets every consumer downstream
|
|
670
|
+
assume a detection has a box, rather than each re-deciding what to do
|
|
671
|
+
about one that does not.
|
|
672
|
+
|
|
664
673
|
Args:
|
|
665
674
|
detection (Detection): Detection object to add to the collection.
|
|
666
675
|
Must be a valid Detection instance.
|
|
667
676
|
|
|
668
|
-
Raises:
|
|
669
|
-
TypeError: If detection is not a Detection instance.
|
|
670
|
-
|
|
671
677
|
Example:
|
|
672
678
|
>>> import pixelflow as pf
|
|
673
679
|
>>>
|
|
@@ -681,8 +687,32 @@ class Detections:
|
|
|
681
687
|
>>> detections.add_detection(detection2)
|
|
682
688
|
>>> print(f"Total detections: {len(detections)}")
|
|
683
689
|
"""
|
|
690
|
+
# Geometry was already judged by validate_bbox on assignment, which leaves
|
|
691
|
+
# None for anything that does not enclose pixels.
|
|
692
|
+
if detection.bbox is None:
|
|
693
|
+
return
|
|
694
|
+
if detection.confidence is not None and math.isnan(detection.confidence):
|
|
695
|
+
return
|
|
696
|
+
|
|
684
697
|
self.detections.append(detection)
|
|
685
698
|
|
|
699
|
+
def _drop_boxless(self) -> "Detections":
|
|
700
|
+
"""
|
|
701
|
+
Remove detections whose box was invalidated by an in-place update.
|
|
702
|
+
|
|
703
|
+
`add_detection` judges a detection on the way in, but a transform rewrites
|
|
704
|
+
`bbox` on detections already stored, where nothing re-checks them. One that
|
|
705
|
+
shrinks a box to nothing - padding by a negative fraction, a box rebuilt
|
|
706
|
+
from a single keypoint - leaves it None, and the detection would otherwise
|
|
707
|
+
stay in the collection: still counted, with no geometry, and fatal to any
|
|
708
|
+
consumer that assumes it has one.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
Detections: This collection, for chaining.
|
|
712
|
+
"""
|
|
713
|
+
self.detections = [d for d in self.detections if d.bbox is not None]
|
|
714
|
+
return self
|
|
715
|
+
|
|
686
716
|
def update_zones(self, zone_manager: Any) -> 'Detections':
|
|
687
717
|
"""
|
|
688
718
|
Update all detections with zone intersection information for spatial analytics.
|