Skip to content

sleap-io v0.8.0

Latest

Choose a tag to compare

@talmo talmo released this 25 Jun 04:05
e934da2

sleap-io v0.8.0 Release Notes

Summary

sleap-io v0.8.0 is a feature release centered on getting data in and out from more places: load .slp and video directly from URLs and cloud buckets (including Google Drive share links), import full DeepLabCut projects (skeleton edges, source videos, train/test splits), and read COCO instance-segmentation datasets as SegmentationMask annotations with identity tracks. It rounds out the v0.7.0 segmentation architecture with mask provenance (to_user() / from_predicted), adds virtual on-read video cropping with a new apply-crops CLI, and ships a batch of rendering upgrades (mask overlays, track-identity coloring, grayscale and instance-less-frame fixes).

⚠️ Breaking change: Labels.merge() / Labels.match() now default to identity-based track matching instead of name-based. Pipelines that relied on collapsing same-named tracks across files must now pass track="name" explicitly. See Breaking Changes below before upgrading.

Highlights:

  • ⚠️ Merge track matching now defaults to identity, not name (#449)Labels.merge(other) no longer collapses tracks just because they share a name. Pass track="name" (or --track name on the CLI) to restore the old behavior. Audit downstream .merge() call sites before bumping.
  • Remote loadingsio.load_file / sio.load_video (and the CLI read commands) read .slp and video over http(s), s3, gs/gcs, az/abfs, and Google Drive share links via fsspec/PyAV (#439, #442, #441, #445, #501).
  • DeepLabCut project importsio.load_dlc_project() reads a DLC config.yaml/project directory into Labels (skeleton edges, source videos), sio.load_dlc_splits() returns the train/test split as a LabelsSet, and sio convert imports projects from the CLI (#424, #450, #496).
  • COCO instance segmentation — the COCO reader imports polygon and (compressed or uncompressed) RLE segmentation as SegmentationMask annotations and can map categories to identity tracks via category_as_track=True, also exposed in sio convert (#479, #487, #496).
  • Segmentation mask provenancePredictedSegmentationMask.to_user(), a from_predicted link on user masks, link-first mask merge, and .slp persistence of the provenance link (preserved across merge) (#472, #475, #478, #491).
  • Virtual on-read croppingVideo.crop() / CropVideoBackend present a cropped view without re-encoding, round-trip through .slp, and bake to real files via the new sio apply-crops command (#460).
  • Rendering upgrades — auto-drawn SegmentationMask overlays, track-identity coloring for masks/ROIs/bboxes, a grayscale-render crash fix, instance-less-frame rendering, and centroid scoping/scaling fixes (#461, #470, #466, #468, #494).
  • Smaller .slp files — mask RLE and ROI WKB datasets are now gzip-compressed on write (#463, #465).
  • Cross-platform CLIsio no longer crashes on the default Windows (cp1252) console (#486), and sio show surfaces SegmentationMask/ROI counts (#500).
  • scipy<1.18 pin in the [mat]/[all] extras to keep the LEAP .mat reader working (#485).

Installation / Upgrade

# One-off CLI usage (no installation needed)
uvx sleap-io@0.8.0 show labels.slp

# Install as CLI tool (new install)
uv tool install "sleap-io[all]"

# Upgrade existing CLI tool installation
uv tool upgrade sleap-io

# Add to project (new dependency)
uv add "sleap-io[all]"

# Upgrade existing project dependency
uv lock --upgrade-package sleap-io && uv sync

See installation docs for more options.


Breaking Changes

⚠️ Merge track matching now defaults to identity, not name (#449, #448)

Change: TrackMatcher's default method flipped from NAME to IDENTITY. This changes the default behavior of Labels.merge(), Labels.match(), and the sio merge CLI (--track default is now identity).

Previously, merging two label files that each contained a Track(name="track_0") would treat them as the same track (collapsing by name). Now, two tracks are only matched if they are the same Python object (identity) — so the merge produces two distinct tracks unless you opt back into name matching.

import sleap_io as sio

base = sio.load_file("session_a.slp")
other = sio.load_file("session_b.slp")

# NEW default (0.8.0): identity matching — same-named tracks stay separate
base.merge(other)                    # -> distinct tracks preserved

# RESTORE 0.7.x behavior: collapse tracks that share a name
base.merge(other, track="name")
# CLI: restore name-based collapsing
sio merge a.slp b.slp -o merged.slp --track name

Migration for downstream (sleap, sleap-nn): Human-in-the-loop and re-tracking flows that relied on name-based collapse will now produce duplicate tracks. Audit every .merge() / .match() call site and add track="name" where name-collapse is intended. sio unsplit already pins track="name" internally, so its behavior is unchanged. A spatial-divergence warning (#448) now fires when name-collision merges combine tracks at incompatible locations. Documented in docs/merging.md and docs/cli.md.

COCO polygon segmentation now defaults to rasterized masks, not vector ROIs (#479)

Change: When the COCO reader encounters polygon segmentation, it now produces a single UserSegmentationMask per annotation by default (segmentation_format="mask"). In v0.7.x, polygons were read as vector UserROI objects (one per ring).

# NEW default (0.8.0): polygon -> rasterized SegmentationMask
labels = sio.load_coco("instances.json")            # masks, no ROIs

# RESTORE 0.7.x vector-ROI behavior
labels = sio.load_coco("instances.json", segmentation_format="roi")

sio convert exposes both options too: --coco-segmentation {mask,roi} selects the representation and --coco-category-as-track maps categories to identity tracks (#496).

Structural skeleton deduplication in update / append / extend (#447)

Change: When you add instances whose skeletons are structurally identical (same ordered node names + edges + symmetries) but are distinct Python objects, Labels now canonicalizes them to a single Skeleton and rebinds the instances to it. Point data is preserved exactly (verified point-safe); only len(labels.skeletons) changes.

s1 = sio.Skeleton(["head", "thorax", "abdomen"])
s2 = sio.Skeleton(["head", "thorax", "abdomen"])   # structurally equal, distinct object
labels = sio.Labels(labeled_frames=[lf_using_s1, lf_using_s2])
len(labels.skeletons)   # 0.8.0: 1   (0.7.x: 2)

Skeletons with a different node order are still kept distinct. Code that counted len(labels.skeletons) or held references to now-canonicalized duplicate skeletons may observe a difference. Documented in docs/model/labels.md.

Re-saving .slp now trusts recorded video metadata by default (#483)

Change: save_slp gained prefer_metadata=True (the default). When a video carries recorded backend_metadata (shape / grayscale / fps), the serializer now writes those recorded values instead of re-decoding the live media. This avoids opening (and OOM-ing on) large or remote media just to re-stamp metadata on save.

Impact: Re-saving a project no longer refreshes fps/shape from the live media file. To force a refresh from the live backend, pass prefer_metadata=False:

labels.save("out.slp", prefer_metadata=False)   # re-read shape/fps/grayscale from media

Preflight-hardened: earlier 0.8.0 builds could serialize a stale/inconsistent shape after a resolution-changing relink (replace_filename) or a post-load grayscale flip. Both are fixed: a relink now invalidates the stale recorded shape/grayscale/fps (#490), and metadata serialization reconciles the channel count with the grayscale flag (#495). The common open → edit → save path remains byte-identical to v0.7.x.

scipy<1.18 upper bound added to [mat] and [all] extras (#485, #484)

Change: scipy 1.18.0 broke pymatreader mat-struct parsing (used by the LEAP .mat reader), so the mat and all extras now pin scipy<1.18. sleap-io itself does not import scipy; this only affects environments installing those extras.

Migration for downstream: packagers requesting sleap-io[all] alongside a dependency that requires scipy>=1.18 will hit a resolver conflict until pymatreader is fixed upstream.


New Features

Remote loading: URLs, cloud buckets, and Google Drive (#439, #442, #441, #445)

Load .slp files and videos directly from remote locations — no manual download step. Supported schemes: http, https, s3, gs/gcs, az/abfs, plus Google Drive share links.

import sleap_io as sio

# .slp over http(s) / cloud
labels = sio.load_file("https://example.com/predictions.slp")
labels = sio.load_file("s3://my-bucket/predictions.slp")

# Pass auth headers for protected .slp endpoints
labels = sio.load_file(
    "https://example.com/predictions.slp",
    headers={"Authorization": "Bearer <token>"},
)

# Remote video (streamed via PyAV)
video = sio.load_video("https://example.com/recording.mp4")

# Google Drive share link
labels = sio.load_file("https://drive.google.com/file/d/<id>/view")

# Clear the on-disk fsspec cache
sio.clear_remote_cache()

Hardening in #445 adds an auth probe, deterministic handle lifecycle/close, Google Drive capacity handling, and an fsspec cache. See docs/remote.md for the full guide.

The CLI's read commands (sio show, sio convert, sio render, sio export, …) also accept these URLs directly (#501):

sio show "https://example.com/predictions.slp"
sio convert "s3://my-bucket/predictions.slp" -o local.slp

Scope notes:

  • headers= is forwarded for .slp over HDF5. Remote media video cannot be authenticated via headers= (the av/imageio backend has no header plumbing); passing headers=/stream_mode= for a remote .mp4/.avi now raises a clear error instead of silently dropping them (#498) — use a pre-signed URL for protected media.
  • Only slp and video are loadable over a URL today; other formats (coco, dlc, csv, …) raise NotImplementedError over a URL — download locally first.

DeepLabCut project import (#424, #450)

Import a full DeepLabCut project — skeleton edges, source videos, and bodyparts — from its config.yaml or project directory, plus the train/test split as a LabelsSet.

import sleap_io as sio

# Whole project (config.yaml or the project directory)
labels = sio.load_dlc_project("my_project/config.yaml")
labels = sio.load_file("my_project/config.yaml")          # auto-detected

# Train/test splits as a LabelsSet
splits = sio.load_dlc_splits("my_project/config.yaml")
train, test = splits["train"], splits["test"]

sio convert now recognizes a DLC project directory / config.yaml and imports it (#496):

sio convert my_project/config.yaml -o project.slp
sio convert my_project/ -o project.slp --from dlc_project

Scope notes:

  • load_dlc_splits requires the labeled images to be present on disk; if they are absent it now emits a warning and returns empty splits (#492) rather than failing silently.

COCO instance-segmentation reader (#479)

The COCO reader now imports object detection/segmentation datasets: polygon and RLE segmentation become SegmentationMask annotations, and categories can be mapped to identity tracks.

import sleap_io as sio

# Polygon/RLE segmentation -> SegmentationMask (default)
labels = sio.load_coco("instances.json")

# Map COCO categories to identity tracks
labels = sio.load_coco("instances.json", category_as_track=True)

# Keep the v0.7.x vector-ROI representation instead of rasterizing
labels = sio.load_coco("instances.json", segmentation_format="roi")

Both compressed (LEB128 string-counts) and uncompressed (list-of-int) RLE, as well as polygon segmentation, are decoded (#487). sio convert exposes the identity/segmentation options too (#496):

sio convert instances.json --from coco -o out.slp \
    --coco-category-as-track --coco-segmentation roi

Segmentation mask provenance: to_user() and from_predicted (#472, #475, #478)

Predicted masks can now be "promoted" to user masks while remembering where they came from, mirroring the long-standing Instance / PredictedInstance relationship.

import numpy as np
import sleap_io as sio

pred = sio.PredictedSegmentationMask.from_numpy(arr, score=0.95)
user = pred.to_user()                 # UserSegmentationMask
user.from_predicted is pred           # True — provenance link preserved

# Link-first mask merge keeps user/predicted pairs together; the
# from_predicted link is persisted to and restored from .slp (#475).

predict → correct → consolidate workflows retain the predicted source through save/load, including when consolidation goes through Labels.merge(..., frame="auto") — the provenance link is remapped to the surviving copy on merge so it is no longer dropped (#491).

Virtual on-read video cropping + apply-crops CLI (#460)

Video.crop() returns a virtual cropped view backed by CropVideoBackend — frames are cropped on read, with no re-encoding. The crop round-trips through .slp (in a /video_crops table) and can later be baked into real cropped video files.

import sleap_io as sio

video = sio.load_video("recording.mp4")
cropped = video.crop(crop=(x1, y1, x2, y2))     # virtual view
cropped = video.crop(center=(cx, cy), size=(256, 256))

# Bake virtual crops into physical files and rewire references
labels.apply_crops("baked.slp")
# Materialize every virtual crop in an SLP into baked video files
sio apply-crops cropped.slp -o baked.slp --quality high
sio apply-crops cropped.slp -o baked.slp --dry-run

Note: baked crop dimensions are rounded up to a codec-friendly multiple of 16 (bottom/right padding), so e.g. a 100×100 crop bakes to 112×112. Coordinates are unchanged.

Rendering upgrades (#461, #470, #466, #468)

  • #461SegmentationMask overlays are now auto-drawn when rendering frames that contain masks, and a grayscale-render crash is fixed.
  • #470 — segmentation masks, ROIs, and bounding boxes are colored by track identity (matching pose coloring).
  • #466 — frames with no instances are rendered via the Labels path even when no skeletons exist.
  • #468render_image centroids are scoped to the rendered video (no cross-video bleed).
import sleap_io as sio

labels = sio.load_file("instances.coco.json", category_as_track=True)
# Masks auto-overlaid, colored by track identity:
sio.render_video(labels, "seg.mp4", color_by="track")

Overlay note: render_image(..., overlay=...) accepts either a single SegmentationMask/ROI/BoundingBox/LabelImage/ndarray or a list of them (#505).


Improvements

Smaller .slp files (#463, #465)

  • #465 — the roi_wkb dataset is gzip-compressed on write.
  • #463 — the mask_rle dataset is gzip-compressed on write.

These are write-time changes; older readers continue to read the compressed datasets transparently. ROI/mask-heavy projects (e.g. instance-segmentation imports) shrink substantially on disk.

Metadata-preferring .slp serialization (#483)

save_slp(prefer_metadata=True) (the new default) avoids decoding large/remote media on every save by trusting recorded backend_metadata. See Breaking Changes for the behavior change and prefer_metadata=False opt-out.

Path handling and helpful errors

  • #457transform_labels / transform_video accept str paths (not only Path).
  • #446 — a missing SLP metadata JSON attribute now raises a clear, actionable error instead of an opaque KeyError.
  • #438SkeletonEncoder now preserves edge-less (isolated) nodes in standalone skeleton JSON output (previously dropped). (Minor residual: standalone skeleton-JSON does not preserve isolated-node ordering; the .slp path is unaffected — see Known Issues.)

Embedded-subset shape resolution (#476)

  • #473/#476_get_effective_shape walks the source_video chain nearest-first, so embedded subsets resolve to their source video's shape and match correctly.

Mask provenance persistence and to_user() (#472, #475, #478)

See New Features — the from_predicted provenance link is persisted in .slp (format 2.4) and mask merge is link-first.

CLI reaches the new readers (#496, #500, #501)

  • #501sio show/convert/render/export/split/filenames accept remote URLs as input (http(s), s3, gs/gcs, az/abfs), matching the Python API.
  • #496sio convert imports DLC projects (config.yaml / project directory, --from dlc_project) and forwards COCO options (--coco-category-as-track, --coco-segmentation {mask,roi}).
  • #500sio show now reports SegmentationMask and ROI counts in the header and per-frame listing, so segmentation-only files no longer look empty.

Fixes

  • #467 (#466) — render instance-less frames via the Labels path when no skeletons exist.
  • #468 — scope render_image centroids to the rendered video.
  • #470 — color segmentation masks (and ROI/bbox) by track identity.
  • #461 — auto-draw SegmentationMask overlays and fix grayscale render crash.
  • #476 (#473) — resolve effective shape through the source chain so embedded subsets match.
  • #446 (#429) — raise a helpful error when an SLP metadata JSON attribute is missing.
  • #447 (#427) — structural skeleton dedup in Labels.update/append/extend (see Breaking Changes; point-safe).
  • #438 — preserve edge-less nodes in SkeletonEncoder JSON output.
  • #485 (#484) — pin scipy<1.18 in the mat/all extras to keep the LEAP .mat reader working.
  • #507sio reencode no longer deadlocks on a 0-frame input; it now defaults to .mp4 output and adds a --replace flag.
  • #480 — Analysis CSV / to_dataframe (all_frames) export now spans the full video length instead of stopping at the last labeled frame.
  • #481 — COCO reader: distinct images sharing a file_name no longer collide (each becomes its own frame), and a scored detection annotation reads as a PredictedSegmentationMask / PredictedROI.

Preflight fixes (release-hardening pass)

A pre-release adversarial audit surfaced and fixed the following before tagging:

  • #489Labels.merge() no longer silently misaligns per-node points when matched skeletons have a different node order (the structure matcher now reorders points by node name). Data-correctness fix.
  • #487 — the COCO reader decodes compressed (LEB128 string-counts) RLE masks instead of crashing with a TypeError.
  • #490 — a resolution-changing replace_filename relink invalidates stale recorded shape/grayscale/fps so save_slp(prefer_metadata=True) no longer writes a wrong shape.
  • #495 — metadata serialization reconciles the channel count with the grayscale flag, so a post-load grayscale flip survives a metadata save.
  • #491Labels.merge(..., frame="auto") remaps mask/instance from_predicted provenance to the surviving copy, so it is no longer dropped on save.
  • #486sio fix and sio render --help no longer crash on the default Windows (cp1252) console (stdout/stderr are reconfigured to UTF-8 at import).
  • #488sio show <dlc_project> / load_file(<dlc_project>, open_videos=…) no longer crash on an unexpected open_videos/lazy kwarg.
  • #493 — a degenerate COCO polygon with a valid bbox falls back to the bbox instead of being dropped.
  • #494 — centroid markers scale linearly with the render scale (no longer double-scaled / vanishing under preview/draft presets).
  • #492load_dlc_splits warns (instead of silently returning empty splits) when the labeled images are missing on disk.
  • #498 — a remote media video given unusable headers=/stream_mode= raises a clear error instead of silently dropping them.
  • #503save_coco writes track identity (attributes.object_id) for mask/ROI/bbox annotations, so a COCO round-trip preserves tracks (previously keypoint-only).
  • #502 — the standalone overlay helpers (draw_masks/draw_rois/draw_bboxes/draw_label_image) accept grayscale (H, W) / (H, W, 1) input instead of crashing.
  • #505render_image(..., overlay=<single object>) accepts a single SegmentationMask/ROI/BoundingBox (previously silently ignored unless wrapped in a list).
  • #504tracking_score and _instance_idx are preserved through mask/bbox/ROI conversions and resampled().
  • #506 — CLI/API polish: draw_centroids is exported at sio.*; the export command appears in sio --help groups; python -m sleap_io.io.cli works; and several doc corrections (palette default, video-crop note, removal of a nonexistent DeepLabCut .h5 reader claim).

Post-audit follow-ups (deferred low-severity findings)

  • #509LabeledFrame.is_user_labeled now counts user ROIs (a UserROI-only frame is correctly treated as user-labeled).
  • #510sio transform --crop/--scale/--rotate/--pad raise a clear error for a non-integer idx: prefix instead of a raw traceback.
  • #511sio convert --from dlc pointed at a DLC project errors with a pointer to --from dlc_project instead of misrouting to the single-CSV reader.
  • #512 — standalone skeleton JSON preserves isolated (edge-less) node order on round-trip (the .slp path was already correct).

Documentation

  • #458 — split the formats reference into per-format leaf pages, repoint remote cross-links, add a Guides landing page.
  • #454 — split spatial-annotation docs into Centroids / Boxes / ROIs / Segmentation.
  • #455 — content and format-reference corrections for 0.8.0.
  • #456 — add llms.txt, surface motion trails, rendering/CLI/nav polish.
  • #453 — repair non-runnable examples and guard pycon blocks.
  • #444 — new Remote loading guide (URLs, cloud, Google Drive, video).
  • #499 — add a "Breaking change in 0.8.0" callout to the COCO format reference (polygon → mask default), remove the documented-but-nonexistent sio render --crop auto / --crop-padding, and repair nine dead cross-link anchors.
  • #497 — execute runnable cropping.md examples (converted to pycon) so feature-doc API drift is caught by CI.

Known Issues

The pre-release audit's headline correctness, CLI, and Windows-console findings were all fixed before tagging (see Preflight fixes). The following minor items ship as-is and are tracked for follow-up:

  • sio show prints × (U+00D7) in video dimensions, which can render as mojibake when piped to a cp1252 consumer (the interactive console itself is now UTF-8).
  • Some cross-modality conversions downcast predicted → userto_polygon/to_roi return user-variant outputs regardless of source (documented design choice).

A fuller list of low/info observations from the audit is tracked internally for post-release cleanup.


Changelog

  • #438: fix(io): preserve edge-less nodes in SkeletonEncoder JSON output (@talmo)
  • #439: feat(io): Load .slp from URLs and cloud buckets via fsspec (PR 1/3) (@talmo)
  • #441: feat(io): Google Drive share-link loading (PR 3/3) (@talmo)
  • #442: feat(video): Remote video loading via PyAV (supersedes #440) (@talmo)
  • #444: docs: Add Remote loading guide (URLs, cloud, Google Drive, video) (@talmo)
  • #445: fix(io): harden remote URL loading (auth, handle lifecycle, Drive caps, fsspec cache) (@talmo)
  • #446: fix(io): raise helpful error when SLP metadata JSON attribute is missing (#429) (@talmo)
  • #447: fix(model): structural skeleton dedup in Labels.update/append/extend (#427) (@talmo)
  • #448: feat(model): warn on spatially-divergent track-name merge collisions (#425) (@talmo)
  • #449: feat(model)!: default Labels.merge track matching to identity, not name (#425) (@talmo)
  • #450: feat(io): import DLC project metadata - edges, source videos, splits (#424) (@talmo)
  • #452: chore(release): Bump version to 0.8.0 (@talmo)
  • #453: fix(docs): repair non-runnable examples and guard pycon blocks (@talmo)
  • #454: docs: split spatial annotations into Centroids/Boxes/ROIs/Segmentation (@talmo)
  • #455: docs: content & format-reference corrections for 0.8.0 (@talmo)
  • #456: docs: add llms.txt, surface motion trails, and rendering/cli/nav polish (@talmo)
  • #457: fix(transform): accept str paths in transform_labels / transform_video (@talmo)
  • #458: docs: split formats into leaf pages, repoint remote cross-links, add Guides landing (@talmo)
  • #460: feat: virtual on-read video cropping (CropVideoBackend) (@talmo)
  • #461: fix(render): auto-draw SegmentationMask overlays and fix grayscale crash (@talmo)
  • #463: perf(slp): gzip-compress the mask_rle dataset when writing .slp (#464) (@talmo)
  • #465: perf(slp): gzip-compress the roi_wkb dataset when writing .slp (@talmo)
  • #466/#467: fix(render): render instance-less frames via Labels path when no skeletons exist (@talmo)
  • #468: fix(render): scope render_image centroids to the rendered video (@talmo)
  • #470: fix(render): color segmentation masks (and ROI/bbox) by track identity (@talmo)
  • #472: feat(model): add PredictedSegmentationMask.to_user() + from_predicted provenance (@talmo)
  • #475: feat(slp): persist UserSegmentationMask.from_predicted (#474 Gap 2) (@talmo)
  • #476: fix(model): resolve effective shape through source chain so embedded subsets match (#473) (@talmo)
  • #478: feat(model): mask unused_predictions + link-first mask merge (#474 Gap 1) (@talmo)
  • #479: feat(coco): read instance-segmentation datasets (polygon→SegmentationMask, identity tracks) (@talmo)
  • #480: fix(csv): span the full video in all_frames DataFrame/CSV export (@alicup29)
  • #481: fix(coco): robust duplicate-filename frames + scored segmentation → predicted (@talmo)
  • #483: feat(slp): prefer recorded video metadata over decoding when serializing (@tom21100227)
  • #485: fix(deps): pin scipy<1.18 to keep pymatreader (LEAP reader) working (@tom21100227)
  • #486: fix(cli): make stdout/stderr UTF-8 so sio fix and render --help work on cp1252 (@talmo)
  • #487: fix(coco): decode compressed (LEB128 string-counts) RLE masks (@talmo)
  • #488: fix(io): tolerate loader kwargs in load_dlc_project/load_dlc_splits (sio show DLC) (@talmo)
  • #489: fix(model): reorder points by node name in merge so reordered skeletons don't misalign (@talmo)
  • #490: fix(slp): invalidate stale shape/grayscale/fps on a resolution-changing relink (@talmo)
  • #491: fix(model): remap mask/instance from_predicted on merge so provenance survives save (@talmo)
  • #492: fix(io): warn when DLC splits resolve empty (labeled images missing) (@talmo)
  • #493: fix(coco): fall back to bbox when a degenerate polygon yields no geometry (@talmo)
  • #494: fix(render): stop double-scaling centroid markers by scale (@talmo)
  • #495: fix(slp): reconcile serialized channel count with the grayscale flag (@talmo)
  • #496: feat(cli): reach DLC-project import and COCO seg/identity options from convert (@talmo)
  • #497: test(docs): execute runnable cropping.md examples (@talmo)
  • #498: fix(video): reject (not silently drop) headers/auth kwargs for remote media video (@talmo)
  • #499: docs: COCO breaking-change callout, remove nonexistent render --crop docs, fix dead anchors (@talmo)
  • #500: feat(cli): surface SegmentationMask/ROI counts in sio show (@talmo)
  • #501: feat(cli): accept remote URLs as input to read commands (@talmo)
  • #502: fix(render): handle grayscale input in overlay helpers (@talmo)
  • #503: fix(coco): write track identity (attributes.object_id) for mask/ROI/bbox (@talmo)
  • #504: fix(model): preserve tracking_score and _instance_idx through conversions (@talmo)
  • #505: fix(render): accept a single object as render_image overlay= (@talmo)
  • #506: chore(cli/docs): export draw_centroids, group export cmd, main guard, doc fixes (@talmo)
  • #507: fix(cli): fix reencode deadlock at 0 frames; default .mp4 output + --replace (@talmo)
  • #508: ci: use sysmon coverage core on py3.13 and raise test timeout to 60m (@talmo)
  • #509: fix(model): count ROIs in LabeledFrame.is_user_labeled (@talmo)
  • #510: fix(cli): clean error for a non-integer transform index prefix (@talmo)
  • #511: fix(cli): clear error for --from dlc on a DLC project (@talmo)
  • #512: fix(io): preserve isolated-node order in standalone skeleton JSON (@talmo)

Full Changelog: v0.7.1...v0.8.0