PixelFlow
The computer vision library that gets out of your way.
PixelFlow provides a unified, intuitive API for object detection, tracking, annotation, and video processing. Write clean, readable pipelines that work with any ML framework.
import pixelflow as pf
from ultralytics import YOLO
model = YOLO("yolo11n.pt")
video = pf.VideoReader("traffic.mp4")
tracker = pf.tracker.ByteTracker()
zones = pf.Zones()
zones.add_zone([(100, 400), (500, 400), (500, 600), (100, 600)], zone_id="entrance")
for frame in video:
frame = pf.transform.resize(frame, width=640)
detections = pf.from_ultralytics(model.predict(frame))
detections = tracker.update(detections)
zones.update(detections)
frame = pf.annotate.box(frame, detections)
frame = pf.annotate.label(frame, detections)
frame = pf.annotate.zones(frame, zones)
pf.display_video(frame, "Live") # raises DisplayExit when 'q' is pressed
Installation
pip install pixelflow
Core Concepts
Two Result Types
Every model output becomes one of two things. Detections for anything that localises — detection, segmentation, keypoints, OCR. Classifications for anything that only names.
They are peers, not variants of each other: three detections are three objects, while three classifications are three competing answers about one image.
Detections - One Format, Every Framework
Convert outputs from any ML framework into a unified format:
import pixelflow as pf
# Ultralytics YOLO
from ultralytics import YOLO
model = YOLO("yolo11n.pt")
detections = pf.from_ultralytics(model.predict(image))
# Detectron2
from detectron2.engine import DefaultPredictor
predictor = DefaultPredictor(cfg)
detections = pf.from_detectron2(predictor(image), labels=["person", "car"])
# Mayaku (returns Instances directly; take labels from the checkpoint)
from mayaku import from_pretrained
predictor = from_pretrained("mayaku-n-det")
detections = pf.from_mayaku(predictor("photo.jpg"), labels=predictor.class_names)
# HuggingFace Transformers
from transformers import pipeline
detector = pipeline("object-detection")
detections = pf.from_transformers(detector(image))
# Florence-2 - captions and OCR reads land in det.text, <OD> classes in det.class_name
detections = pf.from_florence2(model_output, task_prompt="<OD>")
# SAM (Segment Anything)
detections = pf.from_sam(masks, scores)
# RF-DETR
detections = pf.from_rfdetr(model_output)
# Supervision
detections = pf.from_supervision(sv_detections)
# EasyOCR - text lands in det.text, the quad in det.segments
import easyocr
reader = easyocr.Reader(['en'])
detections = pf.from_easyocr(reader.readtext("sign.jpg"))
# Plain arrays (numpy or torch) - no framework container to convert from
detections = pf.from_arrays(boxes, scores, class_ids, labels=model.class_names)
Classifications - When the Model Only Names
A classifier localises nothing, so its result carries no geometry at all:
import pixelflow as pf
# Ultralytics -cls checkpoints
from ultralytics import YOLO
model = YOLO("yolo11n-cls.pt")
result = pf.from_ultralytics_classification(model.predict("dog.jpg"))
result.top1.class_name # 'golden retriever'
result.top_k(5) # -> Classifications, best first
result.to_json()
# Plain scores - CLIP zero-shot, a vendored ResNet, anything with a score vector
prompts = ["a photo of a cat", "a photo of a dog"]
result = pf.from_scores(similarities, labels=prompts)
# Multi-label: however many labels genuinely match, not a fixed count
matches = result.filter_by_confidence(0.3)
# Draw it
frame = pf.annotate.classification(frame, result, top_k=3)
Scores are reported exactly as the model emitted them. PixelFlow does not normalise, re-softmax, or assume they sum to 1, so a softmax over a fixed class list and an independent per-label similarity both survive intact. A model emitting logits should be converted by the caller, who is the only one who knows which convention applies.
Per-frame paths can skip building a row per class:
result = pf.from_ultralytics_classification(model.predict(frame), top_k=5)
Powerful Filtering
Chain filters for complex queries with zero overhead:
# Get high-confidence people in the parking zone, tracked for 5+ seconds
results = (detections
.filter_by_confidence(min_confidence=0.7)
.filter_by_class_id("person")
.filter_by_zones(["parking_lot"])
.filter_by_tracking_duration(min_seconds=5.0))
# Size-based filtering
large_objects = detections.filter_by_size(min_area=10000)
tall_objects = detections.filter_by_dimensions(min_height=200)
squares = detections.filter_by_aspect_ratio(min_ratio=0.9, max_ratio=1.1)
# Spatial filtering
left_side = detections.filter_by_position(max_x=frame_width // 2)
# Remove overlapping detections
unique = detections.remove_duplicates(iou_threshold=0.5)
# OCR-specific filters
titles = detections.filter_by_text_level("title")
english_text = detections.filter_by_text_language("en")
Media Handling
A source is anything that yields RGB uint8 HxWx3 arrays. VideoReader and
CameraStream are conveniences, not gates — a list, a generator, or your own reader
for the one camera that needs special handling all work identically everywhere else.
import pixelflow as pf
# Read a video file
video = pf.VideoReader("video.mp4")
print(f"{video.width}x{video.height}, {video.fps}fps, {video.frames} frames")
for frame in video:
pf.display_video(frame, "Preview")
video.seek(0) # replay is explicit; iteration never rewinds on its own
# Every 5th frame. fps and frames are divided to match, so a writer built from
# this source stays correct.
video = pf.VideoReader("video.mp4", stride=5)
# Load a single image (EXIF orientation applied, always RGB uint8 HxWx3)
image = pf.read_image("photo.jpg")
# Webcam / network stream — same facts, same iteration, is_live is True
cam = pf.CameraStream(0)
cam = pf.CameraStream("rtsp://camera.local/stream")
Reading and resizing are two jobs — compose them:
for frame in video:
frame = pf.transform.resize(frame, width=640) # or height=
Writing. like= carries the frame rate across from a source, which is the one
value that has to and the one that is easy to get wrong — a strided reader yields
fewer frames per second than its file contains. Size is not carried: it comes from
the first frame written, because the loop in between is allowed to resize.
video = pf.VideoReader("input.mp4", stride=2)
with pf.VideoWriter("output.mp4", like=video) as writer:
for frame in video:
writer.write(process(frame))
Every source reports the same facts, so consumers never branch on the source type:
VideoReader |
CameraStream |
|
|---|---|---|
width / height |
from the file | from a frame decoded at open |
fps |
rate yielded, None if unknown |
usually None — devices report 0 |
frames |
estimate, divided by stride | always None |
duration |
seconds, unaffected by stride | always None |
is_live |
False |
True |
Zone-Based Analytics
Define spatial regions and track what enters them:
import pixelflow as pf
zones = pf.Zones()
# Define zones with different trigger strategies
zones.add_zone(
polygon=[(100, 400), (300, 400), (300, 600), (100, 600)],
zone_id="entrance",
name="Main Entrance",
trigger_strategy="bottom_center" # Trigger when bottom-center of bbox enters
)
zones.add_zone(
polygon=[(500, 200), (700, 200), (700, 500), (500, 500)],
zone_id="restricted",
trigger_strategy="percentage",
overlap_threshold=0.3 # Trigger when 30% of bbox is inside
)
# Update detections with zone info
zones.update(detections)
# Access zone statistics
print(zones.get_zone_counts()) # {'entrance': 3, 'restricted': 1}
print(zones.get_zone_stats()) # Detailed stats including total_entered
# Filter by zone
entrance_only = zones.filter_by_zones(detections, ["entrance"])
not_restricted = zones.filter_by_zones(detections, ["restricted"], exclude=True)
Trigger Strategies:
center- Bounding box center point (default)bottom_center- Bottom-center point (great for people/vehicles)percentage- Percentage of bbox overlapoverlap- Any intersection- Multiple strategies with
mode="any"ormode="all"
Object Tracking
Built-in ByteTrack for multi-object tracking:
import pixelflow as pf
tracker = pf.tracker.ByteTracker()
for frame in video:
detections = pf.from_ultralytics(model.predict(frame))
detections = tracker.update(detections)
for det in detections:
print(f"Track ID: {det.tracker_id}, Class: {det.class_name}")
print(f"First seen: {det.first_seen_time}, Duration: {det.total_time}s")
Rich Annotations
Beautiful, customizable visualizations:
import pixelflow as pf
# Bounding boxes and labels
frame = pf.annotate.box(frame, detections, thickness=2)
frame = pf.annotate.label(frame, detections, font_scale=0.6)
# Segmentation masks
frame = pf.annotate.mask(frame, detections, opacity=0.4)
# Keypoints and skeletons (pose estimation)
frame = pf.annotate.keypoint(frame, detections)
frame = pf.annotate.keypoint_skeleton(frame, detections)
# Zone visualization
frame = pf.annotate.zones(frame, zones)
frame = pf.annotate.crossings(frame, crossings)
# Privacy protection
frame = pf.annotate.blur(frame, detections, kernel_size=51)
frame = pf.annotate.pixelate(frame, detections, pixel_size=15)
# Shapes
frame = pf.annotate.oval(frame, detections)
frame = pf.annotate.polygon(frame, detections)
Image Transforms
Comprehensive image and detection-aware transformations:
import pixelflow as pf
# Image-only transforms
rotated = pf.transform.rotate(image, 45)
flipped = pf.transform.flip_horizontal(image)
cropped = pf.transform.crop(image, [100, 50, 500, 400])
# Enhancement
enhanced = pf.transform.clahe(image)
gray = pf.transform.to_grayscale(image)
corrected = pf.transform.gamma_correction(image, gamma=1.2)
# Detection-aware transforms (coordinates update automatically)
rotated_img, rotated_dets = pf.transform.rotate_detections(image, detections, 45)
flipped_img, flipped_dets = pf.transform.flip_horizontal_detections(image, detections)
cropped_img, cropped_dets = pf.transform.crop_detections(image, detections, bbox=[100, 50, 500, 400])
# Automatic inverse transforms (undo all transforms)
original_coords = pf.transform.inverse_transforms(transformed_detections)
Sliced Inference for Large Images
Detect small objects in high-resolution images:
import pixelflow as pf
slicer = pf.SlicedInference(
slice_height=640,
slice_width=640,
overlap_ratio_h=0.2,
overlap_ratio_w=0.2
)
def detector(image):
return pf.from_ultralytics(model.predict(image))
# Run on 4K image - automatically slices, detects, and merges
large_image = cv2.imread("satellite.jpg") # 4000x3000
detections = slicer.predict(large_image, detector)
Line Crossing Detection
Track objects crossing defined lines:
import pixelflow as pf
crossings = pf.Crossings()
crossings.add_line(
start=(100, 300),
end=(500, 300),
line_id="entry_line",
direction="down" # Only count downward crossings
)
for frame in video:
detections = tracker.update(pf.from_ultralytics(model.predict(frame)))
crossings.update(detections)
print(f"Crossed: {crossings.get_counts()}")
frame = pf.annotate.crossings(frame, crossings)
Serialization
Export and import detection data:
# To JSON
json_str = detections.to_json()
# To dictionary (for pandas, etc.)
data = detections.to_dict()
df = pd.DataFrame(data)
# With metrics
report = detections.to_json_with_metrics()
Supported Frameworks
| Framework | Converter | Features |
|---|---|---|
| None (plain numpy/torch arrays) | from_arrays() |
Boxes, masks, keypoints |
| Ultralytics (YOLO) | from_ultralytics() |
Boxes, masks, keypoints |
| Detectron2 | from_detectron2() |
Boxes, masks, keypoints |
| Mayaku | from_mayaku() |
Boxes, masks, keypoints |
| HuggingFace Transformers | from_transformers() |
Boxes, scores |
| Florence-2 | from_florence2() |
Boxes, quads, polygons, captions |
| SAM | from_sam() |
Masks, scores |
| EfficientTAM | from_efficienttam() |
Masks, scores |
| RF-DETR | from_rfdetr() |
Boxes, masks |
| Supervision | from_supervision() |
Boxes, masks, keypoints |
| Falcon Perception | from_falcon_perception() |
Boxes, masks |
| EasyOCR | from_easyocr() |
Quads, text, scores |
| Datamarkin API | from_datamarkin() |
Boxes, masks, keypoints |
Documentation
Full documentation available at https://datamarkin.com/docs/pixelflow/
Contributing
Contributions welcome! Please read our contributing guidelines.
License
MIT License - see LICENSE file for details.
Built with love by Datamarkin
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pixelflow-0.5.0.tar.gz.
File metadata
- Download URL: pixelflow-0.5.0.tar.gz
- Upload date:
- Size: 178.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e504c84eca67bfa4f45470e722ea7d4c3e1a275983bdbbf33db79f1b7fb76d06
|
|
| MD5 |
ba41a9520695ee0ce4f9b705a787ca56
|
|
| BLAKE2b-256 |
2838a9df891ab175a6392bb7faf016da659f18a71ddbd94848d98441cd8f08c8
|
Provenance
The following attestation bundles were made for pixelflow-0.5.0.tar.gz:
Publisher:
publish.yml on datamarkin/pixelflow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pixelflow-0.5.0.tar.gz -
Subject digest:
e504c84eca67bfa4f45470e722ea7d4c3e1a275983bdbbf33db79f1b7fb76d06 - Sigstore transparency entry: 2627133275
- Sigstore integration time:
-
Permalink:
datamarkin/pixelflow@5bb62f908973ed620b078ff897a2d27a2a73d492 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/datamarkin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5bb62f908973ed620b078ff897a2d27a2a73d492 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pixelflow-0.5.0-py3-none-any.whl.
File metadata
- Download URL: pixelflow-0.5.0-py3-none-any.whl
- Upload date:
- Size: 203.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c804926397f9752b91f01fcf08b305ad4ed4d68b7c0b28a7a78d076b53cd7395
|
|
| MD5 |
3e18bfde34f36f8cfd71a51efce869e5
|
|
| BLAKE2b-256 |
05787b3c380e1313512888583fb092b824cd571477a7723dae30e0c1b04c3acd
|
Provenance
The following attestation bundles were made for pixelflow-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on datamarkin/pixelflow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pixelflow-0.5.0-py3-none-any.whl -
Subject digest:
c804926397f9752b91f01fcf08b305ad4ed4d68b7c0b28a7a78d076b53cd7395 - Sigstore transparency entry: 2627133284
- Sigstore integration time:
-
Permalink:
datamarkin/pixelflow@5bb62f908973ed620b078ff897a2d27a2a73d492 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/datamarkin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5bb62f908973ed620b078ff897a2d27a2a73d492 -
Trigger Event:
push
-
Statement type: