pixelflow 0.3.1__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.
Files changed (64) hide show
  1. {pixelflow-0.3.1 → pixelflow-0.5.0}/PKG-INFO +51 -21
  2. {pixelflow-0.3.1 → pixelflow-0.5.0}/README.md +50 -20
  3. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/__init__.py +33 -5
  4. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/detections/detections.py +33 -3
  5. pixelflow-0.5.0/pixelflow/media.py +961 -0
  6. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/smoother.py +48 -11
  7. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/transforms/__init__.py +3 -0
  8. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/transforms/detections.py +6 -2
  9. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/transforms/image.py +74 -0
  10. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/validators.py +12 -1
  11. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow.egg-info/PKG-INFO +51 -21
  12. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow.egg-info/SOURCES.txt +0 -1
  13. pixelflow-0.3.1/pixelflow/assets.py +0 -378
  14. pixelflow-0.3.1/pixelflow/media.py +0 -458
  15. {pixelflow-0.3.1 → pixelflow-0.5.0}/LICENSE +0 -0
  16. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/__init__.py +0 -0
  17. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/anchors.py +0 -0
  18. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/blur.py +0 -0
  19. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/box.py +0 -0
  20. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/classification.py +0 -0
  21. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/crossing.py +0 -0
  22. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/dot.py +0 -0
  23. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/filled_box.py +0 -0
  24. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/fps_counter.py +0 -0
  25. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/grid_overlay.py +0 -0
  26. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/heatmap.py +0 -0
  27. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/keypoint.py +0 -0
  28. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/keypoint_skeleton.py +0 -0
  29. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/label.py +0 -0
  30. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/mask.py +0 -0
  31. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/motion_dots.py +0 -0
  32. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/motion_trails.py +0 -0
  33. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/oval.py +0 -0
  34. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/pixelate.py +0 -0
  35. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/polygon.py +0 -0
  36. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/scale_bar.py +0 -0
  37. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/utils.py +0 -0
  38. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/annotators/zones.py +0 -0
  39. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/arrays.py +0 -0
  40. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/buffer.py +0 -0
  41. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/classifications/__init__.py +0 -0
  42. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/classifications/classifications.py +0 -0
  43. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/classifications/converters.py +0 -0
  44. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/colors.py +0 -0
  45. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/crossings.py +0 -0
  46. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/detections/__init__.py +0 -0
  47. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/detections/converters.py +0 -0
  48. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/detections/filters.py +0 -0
  49. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/labels.py +0 -0
  50. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/slicer.py +0 -0
  51. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/strategies.py +0 -0
  52. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/timer.py +0 -0
  53. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/tracker/__init__.py +0 -0
  54. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/tracker/bytetrack.py +0 -0
  55. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/tracker/kalman_filter.py +0 -0
  56. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/tracker/matching.py +0 -0
  57. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/tracker/track.py +0 -0
  58. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/transforms/inverse.py +0 -0
  59. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow/zones.py +0 -0
  60. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow.egg-info/dependency_links.txt +0 -0
  61. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow.egg-info/requires.txt +0 -0
  62. {pixelflow-0.3.1 → pixelflow-0.5.0}/pixelflow.egg-info/top_level.txt +0 -0
  63. {pixelflow-0.3.1 → pixelflow-0.5.0}/pyproject.toml +0 -0
  64. {pixelflow-0.3.1 → 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.1
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", width=640)
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
- if pf.show_frame("Live", frame) == ord('q'):
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
- Purpose-built classes for videos, cameras, and images:
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", width=640)
210
- print(f"{video.width}x{video.height}, {video.fps}fps, {len(video)} frames")
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: # Replayable — resets on each iteration
213
- if pf.show_frame("Preview", frame) == ord('q'):
214
- break
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
- # Load a single image
217
- image = pf.read_image("photo.jpg", width=640)
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
- # Webcam / network stream
220
- cam = pf.CameraStream(0, width=640)
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
- # Write processed video
224
- writer = pf.VideoWriter("output.mp4", fps=video.fps)
231
+ Reading and resizing are two jobs — compose them:
232
+
233
+ ```python
225
234
  for frame in video:
226
- processed = process(frame)
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 media.frames:
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 media.frames:
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", width=640)
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
- if pf.show_frame("Live", frame) == ord('q'):
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
- Purpose-built classes for videos, cameras, and images:
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", width=640)
172
- print(f"{video.width}x{video.height}, {video.fps}fps, {len(video)} frames")
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: # Replayable — resets on each iteration
175
- if pf.show_frame("Preview", frame) == ord('q'):
176
- break
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
- # Load a single image
179
- image = pf.read_image("photo.jpg", width=640)
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
- # Webcam / network stream
182
- cam = pf.CameraStream(0, width=640)
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
- # Write processed video
186
- writer = pf.VideoWriter("output.mp4", fps=video.fps)
193
+ Reading and resizing are two jobs — compose them:
194
+
195
+ ```python
187
196
  for frame in video:
188
- processed = process(frame)
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 media.frames:
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 media.frames:
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.3.1"
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.