Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better reprs and QOL #96

Merged
merged 47 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
18c06e3
Add serialization to dict at the video object level
talmo May 5, 2024
6bc26c4
Store and retrieve video backend metadata
talmo May 5, 2024
84f9542
Try to restore source video when available
talmo May 5, 2024
a15f44b
Rename symbol
talmo May 5, 2024
2dbaea0
Use backend metadata when available when serializing
talmo May 5, 2024
d933cc0
Fix backend metadata factory
talmo May 5, 2024
247f92e
Re-embed videos when saving labels with embedded videos
talmo May 5, 2024
d35f07e
Fix serialization and logic for checking for embedded images
talmo May 5, 2024
31341da
Fix multi-frame decoding
talmo May 5, 2024
4402cb4
Fix docstring order
talmo May 5, 2024
696ae60
Add method to embed a list of frames and update the objects
talmo May 5, 2024
fd9bae9
Fix order of operations
talmo May 5, 2024
b41aa38
Add embed_videos
talmo May 6, 2024
c8c5ebb
Fix mid-level embedding function
talmo May 17, 2024
5f70e1c
Hash videos by ID
talmo May 17, 2024
8fe51c0
Add property to return embedded frame indices
talmo May 17, 2024
c329bad
Hash LabeledFrame by ID and add convenience checks for instance types
talmo May 17, 2024
527de4e
Labels.user_labeled_frames
talmo May 17, 2024
77f7ef4
Fix JABS
talmo May 17, 2024
b71646f
Tests
talmo May 17, 2024
4e5734b
Add live coverage support
talmo May 17, 2024
0614541
Expose high level embedding
talmo Jun 5, 2024
3894837
Separate replace video and support restoring source
talmo Jun 5, 2024
a1864a6
Merge branch 'main' into talmo/embed-pkg-slp
talmo Jun 5, 2024
ac5f81c
Update method
talmo Jun 5, 2024
2f4485d
Append/extend
talmo Jun 5, 2024
d421849
Skeleton.edge_names
talmo Jun 5, 2024
4e1adf1
Skeleton repr
talmo Jun 5, 2024
dca61c0
Instance repr
talmo Jun 5, 2024
0149c55
Add better video docstrings
talmo Jun 5, 2024
0eebaeb
High level filename replacement
talmo Jun 5, 2024
fbde524
Type hinting
talmo Jun 5, 2024
a76680a
Lint
talmo Jun 5, 2024
64f88e5
Add Video(filename) syntactic sugar
talmo Jun 5, 2024
f5b5e61
Shim for py3.8
talmo Jun 5, 2024
8932247
Coverage
talmo Jun 5, 2024
a49caac
Merge branch 'talmo/embed-pkg-slp' into talmo/repr-n-qol
talmo Jun 5, 2024
1fba09a
Shim
talmo Jun 5, 2024
e234f9a
Windows fix
talmo Jun 5, 2024
eff9676
Windows test fix
talmo Jun 5, 2024
af4a817
Merge branch 'talmo/embed-pkg-slp' into talmo/repr-n-qol
talmo Jun 5, 2024
9adb57c
PredictedInstance repr
talmo Jun 5, 2024
5eee083
Windows test fix again
talmo Jun 5, 2024
6dee03e
Merge branch 'talmo/embed-pkg-slp' into talmo/repr-n-qol
talmo Jun 5, 2024
d2a1415
Fix test
talmo Jun 5, 2024
0cbf8f3
Merge branch 'talmo/embed-pkg-slp' into talmo/repr-n-qol
talmo Jun 5, 2024
9d4d225
Merge branch 'main' into talmo/repr-n-qol
talmo Jun 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion sleap_io/io/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ def load_video(filename: str, **kwargs) -> Video:
"""Load a video file.

Args:
filename: Path to a video file.
filename: The filename(s) of the video. Supported extensions: "mp4", "avi",
"mov", "mj2", "mkv", "h5", "hdf5", "slp", "png", "jpg", "jpeg", "tif",
"tiff", "bmp". If the filename is a list, a list of image filenames are
expected. If filename is a folder, it will be searched for images.

Returns:
A `Video` object.
Expand Down
23 changes: 23 additions & 0 deletions sleap_io/model/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,13 @@ def __len__(self) -> int:
"""Return the number of points in the instance."""
return len(self.points)

def __repr__(self) -> str:
"""Return a readable representation of the instance."""
pts = self.numpy().tolist()
track = f'"{self.track.name}"' if self.track is not None else self.track

return f"Instance(points={pts}, track={track})"

@property
def n_visible(self) -> int:
"""Return the number of visible points in the instance."""
Expand Down Expand Up @@ -327,6 +334,22 @@ class PredictedInstance(Instance):
score: float = 0.0
tracking_score: Optional[float] = 0

def __repr__(self) -> str:
"""Return a readable representation of the instance."""
pts = self.numpy().tolist()
track = f'"{self.track.name}"' if self.track is not None else self.track

score = str(self.score) if self.score is None else f"{self.score:.2f}"
tracking_score = (
str(self.tracking_score)
if self.tracking_score is None
else f"{self.tracking_score:.2f}"
)
return (
f"PredictedInstance(points={pts}, track={track}, "
f"score={score}, tracking_score={tracking_score})"
)

@classmethod
def from_numpy( # type: ignore[override]
cls,
Expand Down
140 changes: 137 additions & 3 deletions sleap_io/model/labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from attrs import define, field
from typing import Union, Optional, Any
import numpy as np

from pathlib import Path
from sleap_io.model.skeleton import Skeleton


Expand Down Expand Up @@ -57,6 +57,14 @@

def __attrs_post_init__(self):
"""Append videos, skeletons, and tracks seen in `labeled_frames` to `Labels`."""
self.update()

def update(self):
"""Update data structures based on contents.

This function will update the list of skeletons, videos and tracks from the
labeled frames, instances and suggestions.
"""
for lf in self.labeled_frames:
if lf.video not in self.videos:
self.videos.append(lf.video)
Expand All @@ -68,7 +76,13 @@
if inst.track is not None and inst.track not in self.tracks:
self.tracks.append(inst.track)

def __getitem__(self, key: int) -> list[LabeledFrame] | LabeledFrame:
for sf in self.suggestions:
if sf.video not in self.videos:
self.videos.append(sf.video)

def __getitem__(
self, key: int | slice | list[int] | np.ndarray | tuple[Video, int]
) -> list[LabeledFrame] | LabeledFrame:
"""Return one or more labeled frames based on indexing criteria."""
if type(key) == int:
return self.labeled_frames[key]
Expand Down Expand Up @@ -111,14 +125,58 @@
f"labeled_frames={len(self.labeled_frames)}, "
f"videos={len(self.videos)}, "
f"skeletons={len(self.skeletons)}, "
f"tracks={len(self.tracks)}"
f"tracks={len(self.tracks)}, "
f"suggestions={len(self.suggestions)}"
")"
)

def __str__(self) -> str:
"""Return a readable representation of the labels."""
return self.__repr__()

def append(self, lf: LabeledFrame, update: bool = True):
"""Append a labeled frame to the labels.

Args:
lf: A labeled frame to add to the labels.
update: If `True` (the default), update list of videos, tracks and
skeletons from the contents.
"""
self.labeled_frames.append(lf)

if update:
if lf.video not in self.videos:
self.videos.append(lf.video)

for inst in lf:
if inst.skeleton not in self.skeletons:
self.skeletons.append(inst.skeleton)

if inst.track is not None and inst.track not in self.tracks:
self.tracks.append(inst.track)

def extend(self, lfs: list[LabeledFrame], update: bool = True):
"""Append a labeled frame to the labels.

Args:
lfs: A list of labeled frames to add to the labels.
update: If `True` (the default), update list of videos, tracks and
skeletons from the contents.
"""
self.labeled_frames.extend(lfs)

if update:
for lf in lfs:
if lf.video not in self.videos:
self.videos.append(lf.video)

for inst in lf:
if inst.skeleton not in self.skeletons:
self.skeletons.append(inst.skeleton)

if inst.track is not None and inst.track not in self.tracks:
self.tracks.append(inst.track)

def numpy(
self,
video: Optional[Union[Video, int]] = None,
Expand Down Expand Up @@ -417,3 +475,79 @@
for sf in self.suggestions:
if sf.video in video_map:
sf.video = video_map[sf.video]

def replace_filenames(
self,
new_filenames: list[str | Path] | None = None,
filename_map: dict[str | Path, str | Path] | None = None,
prefix_map: dict[str | Path, str | Path] | None = None,
):
"""Replace video filenames.

Args:
new_filenames: List of new filenames. Must have the same length as the
number of videos in the labels.
filename_map: Dictionary mapping old filenames (keys) to new filenames
(values).
prefix_map: Dictonary mapping old prefixes (keys) to new prefixes (values).

Notes:
Only one of the argument types can be provided.
"""
n = 0
if new_filenames is not None:
n += 1
if filename_map is not None:
n += 1
if prefix_map is not None:
n += 1
if n != 1:
raise ValueError(
"Exactly one input method must be provided to replace filenames."
)

if new_filenames is not None:
if len(self.videos) != len(new_filenames):
raise ValueError(
f"Number of new filenames ({len(new_filenames)}) does not match "
f"the number of videos ({len(self.videos)})."
)

for video, new_filename in zip(self.videos, new_filenames):
video.replace_filename(new_filename)

elif filename_map is not None:
for video in self.videos:
for old_fn, new_fn in filename_map.items():
if type(video.filename) == list:
new_fns = []
for fn in video.filename:
if Path(fn) == Path(old_fn):
new_fns.append(new_fn)
else:
new_fns.append(fn)
video.replace_filename(new_fns)
else:
if Path(video.filename) == Path(old_fn):
video.replace_filename(new_fn)

elif prefix_map is not None:
for video in self.videos:
for old_prefix, new_prefix in prefix_map.items():
old_prefix, new_prefix = Path(old_prefix), Path(new_prefix)

if type(video.filename) == list:
new_fns = []
for fn in video.filename:
fn = Path(fn)
if fn.as_posix().startswith(old_prefix.as_posix()):
new_fns.append(new_prefix / fn.relative_to(old_prefix))
else:
new_fns.append(fn)

Check warning on line 546 in sleap_io/model/labels.py

View check run for this annotation

Codecov / codecov/patch

sleap_io/model/labels.py#L546

Added line #L546 was not covered by tests
video.replace_filename(new_fns)
else:
fn = Path(video.filename)
if fn.as_posix().startswith(old_prefix.as_posix()):
video.replace_filename(
new_prefix / fn.relative_to(old_prefix)
)
10 changes: 10 additions & 0 deletions sleap_io/model/skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ def edge_inds(self) -> list[Tuple[int, int]]:
for edge in self.edges
]

@property
def edge_names(self) -> list[str, str]:
"""Edge names as a list of 2-tuples with string node names."""
return [(edge.source.name, edge.destination.name) for edge in self.edges]

@property
def flipped_node_inds(self) -> list[int]:
"""Returns node indices that should be switched when horizontally flipping."""
Expand All @@ -183,6 +188,11 @@ def __len__(self) -> int:
"""Return the number of nodes in the skeleton."""
return len(self.nodes)

def __repr__(self) -> str:
"""Return a readable representation of the skeleton."""
nodes = ", ".join([f'"{node}"' for node in self.node_names])
return "Skeleton(" f"nodes=[{nodes}], " f"edges={self.edge_inds}" ")"

def index(self, node: Node | str) -> int:
"""Return the index of a node specified as a `Node` or string name."""
if type(node) == str:
Expand Down
19 changes: 15 additions & 4 deletions sleap_io/model/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import attrs
from typing import Tuple, Optional, Optional
import numpy as np
from sleap_io.io.video import VideoBackend, MediaVideo, HDF5Video
from sleap_io.io.video import VideoBackend, MediaVideo, HDF5Video, ImageVideo
from pathlib import Path


Expand All @@ -23,7 +23,10 @@
backend appropriately.

Attributes:
filename: The filename(s) of the video.
filename: The filename(s) of the video. Supported extensions: "mp4", "avi",
"mov", "mj2", "mkv", "h5", "hdf5", "slp", "png", "jpg", "jpeg", "tif",
"tiff", "bmp". If the filename is a list, a list of image filenames are
expected. If filename is a folder, it will be searched for images.
backend: An object that implements the basic methods for reading and
manipulating frames of a specific video type.
backend_metadata: A dictionary of metadata specific to the backend. This is
Expand All @@ -45,7 +48,12 @@
backend_metadata: dict[str, any] = attrs.field(factory=dict)
source_video: Optional[Video] = None

EXTS = MediaVideo.EXTS + HDF5Video.EXTS
EXTS = MediaVideo.EXTS + HDF5Video.EXTS + ImageVideo.EXTS

def __attrs_post_init__(self):
"""Post init syntactic sugar."""
if self.backend is None and self.exists():
self.open()

Check warning on line 56 in sleap_io/model/video.py

View check run for this annotation

Codecov / codecov/patch

sleap_io/model/video.py#L55-L56

Added lines #L55 - L56 were not covered by tests

def __attrs_post_init__(self):
"""Post init syntactic sugar."""
Expand All @@ -65,7 +73,10 @@
"""Create a Video from a filename.

Args:
filename: Path to video file(s).
filename: The filename(s) of the video. Supported extensions: "mp4", "avi",
"mov", "mj2", "mkv", "h5", "hdf5", "slp", "png", "jpg", "jpeg", "tif",
"tiff", "bmp". If the filename is a list, a list of image filenames are
expected. If filename is a folder, it will be searched for images.
dataset: Name of dataset in HDF5 file.
grayscale: Whether to force grayscale. If None, autodetect on first frame
load.
Expand Down
9 changes: 9 additions & 0 deletions tests/model/test_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ def test_instance():
inst = Instance({"A": [0, 1], "B": [2, 3]}, skeleton=Skeleton(["A", "B"]))
assert_equal(inst.numpy(), [[0, 1], [2, 3]])
assert type(inst["A"]) == Point
assert str(inst) == "Instance(points=[[0.0, 1.0], [2.0, 3.0]], track=None)"

inst.track = Track("trk")
assert str(inst) == 'Instance(points=[[0.0, 1.0], [2.0, 3.0]], track="trk")'

inst = Instance({"A": [0, 1]}, skeleton=Skeleton(["A", "B"]))
assert_equal(inst.numpy(), [[0, 1], [np.nan, np.nan]])
Expand Down Expand Up @@ -155,3 +159,8 @@ def test_predicted_instance():
assert inst[0].score == 0.4
assert inst[1].score == 0.5
assert inst.score == 0.6

assert (
str(inst) == "PredictedInstance(points=[[0.0, 1.0], [2.0, 3.0]], track=None, "
"score=0.60, tracking_score=None)"
)
Loading
Loading