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

User story/2061 boundingboxes of detections per frame #295

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
ecc7f4b
Show bounding boxes for current frame
briemla Aug 7, 2023
d12ba22
Add video control to switch current frame
briemla Aug 7, 2023
46a2083
Optimize loading of frame
briemla Aug 7, 2023
3c2d7aa
Load last frame instead of raising an exception
briemla Aug 7, 2023
269630b
Fix missing argument
briemla Aug 7, 2023
c2932e5
Decouple plotter from non domain code
briemla Aug 7, 2023
0ac26e7
Fix missing parameters
briemla Aug 7, 2023
1278d00
Remove duplicated code
briemla Aug 7, 2023
e2e3898
Update to plotter changes
briemla Aug 7, 2023
1778a5f
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Aug 8, 2023
d34e5d0
Merge remote-tracking branch 'origin/main' into user-story/2061-bound…
briemla Aug 17, 2023
78e30c2
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Aug 18, 2023
7e566ab
Use default color palette for bounding boxes
briemla Aug 22, 2023
067978c
Merge remote-tracking branch 'origin/main' into user-story/2061-bound…
briemla Aug 22, 2023
7bdff97
Use color palette provider to determine colors
briemla Aug 22, 2023
49274a9
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Aug 22, 2023
4fce7f4
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Sep 1, 2023
42fa449
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Sep 1, 2023
e5bab1a
Merge remote-tracking branch 'origin/main' into user-story/2061-bound…
briemla Sep 13, 2023
f78b8cb
Remove unused code
briemla Sep 13, 2023
e719aa9
Consider offset of frames in video and ottrk files.
briemla Sep 13, 2023
ac5b759
Merge remote-tracking branch 'origin/main' into user-story/2061-bound…
briemla Sep 13, 2023
ba7fb29
Merge remote-tracking branch 'origin/main' into user-story/2061-bound…
briemla Sep 22, 2023
032fedc
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Sep 22, 2023
8b414a2
Merge remote-tracking branch 'origin/main' into user-story/2061-bound…
briemla Oct 4, 2023
fa3a7d8
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Oct 4, 2023
a376b9f
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Oct 5, 2023
efb4847
Merge remote-tracking branch 'origin/main' into user-story/2061-bound…
briemla Oct 17, 2023
32ec727
Fix broken test
briemla Oct 18, 2023
bb3a316
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Oct 28, 2023
0498e21
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Oct 30, 2023
ffb69c2
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Nov 8, 2023
8e10cd9
Merge remote-tracking branch 'origin/main' into user-story/2061-bound…
briemla Dec 4, 2023
7333b8d
Use timezone information while parsing
briemla Dec 6, 2023
24998a0
Add bounding box plotter
briemla Dec 6, 2023
e770b32
Add fps property to video
briemla Dec 7, 2023
69ccaa1
Jump to next date range using skip time
briemla Dec 7, 2023
afbc8f4
Enable and disable skip buttons
briemla Dec 7, 2023
d2cfb6f
Fix incorrect calculation of frame number
briemla Dec 7, 2023
8e31564
Test reading correct frame number
briemla Dec 7, 2023
01b68ee
Store seconds of track to fix visualization of tracks from multiple f…
briemla Dec 11, 2023
32448ff
Clean up code and use correct data providers
briemla Dec 11, 2023
c0d528d
Reuse existing code
briemla Dec 11, 2023
76775c5
Change implementation of videos metadata to get metadata by path and …
briemla Dec 18, 2023
9816a4f
Introduce optional fps attribute at video
briemla Dec 18, 2023
e82a92f
Reuse filter to plot used point of bounding box
briemla Dec 18, 2023
a0f49f1
Show track points of bounding boxes of current frame
briemla Dec 18, 2023
db5cd6b
Use use case to get the current frame information
briemla Dec 20, 2023
bf35f35
Use correct fps to calculate step size
briemla Dec 20, 2023
3741699
Clean up filter
briemla Dec 20, 2023
e0301a7
Fix data providers
briemla Dec 20, 2023
ec78e7d
Use project dir to start tests
briemla Dec 20, 2023
2f50955
Remove unused parameter
briemla Dec 20, 2023
fc364a7
Filter the visualised frames by video
briemla Dec 20, 2023
69e4a92
Filter the visualised frames by video
briemla Dec 20, 2023
0fe5822
Clean up and document
briemla Dec 20, 2023
72ad3b7
Add missing init file
briemla Dec 20, 2023
d0c7cb4
Extract use cases to change currently visible frame
briemla Dec 20, 2023
daed555
Test previous and next frame use cases
briemla Dec 20, 2023
891fcf6
Test previous and next frame use cases
briemla Dec 20, 2023
ac47afb
Rename use cases
briemla Dec 20, 2023
70c7a5d
Remove unused code
briemla Dec 20, 2023
9faa209
Merge remote tracking origin/main into user-story/2061-boundingboxes-…
briemla Jan 23, 2024
1cb6edc
Fix dataframe provider with offset
briemla Jan 23, 2024
e9bdac8
Change marker
briemla Jan 23, 2024
9425ecd
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Jan 23, 2024
22727d8
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
randy-seng Jan 30, 2024
d73456a
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
briemla Jan 30, 2024
ec5e052
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
randy-seng Jan 30, 2024
bd05d0b
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
randy-seng Jan 31, 2024
7809ba3
Extend unit tests
randy-seng Feb 1, 2024
c472707
Move unit tests to existing test module and remove duplicate module
randy-seng Feb 1, 2024
bfad2a7
Consider milliseconds greater than FPS when switching to next frame
randy-seng Feb 1, 2024
53dd082
Remove redundant if statement
randy-seng Feb 1, 2024
2bc7cf0
Merge branch 'main' into user-story/2061-boundingboxes-of-detections-…
randy-seng Feb 1, 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
2 changes: 1 addition & 1 deletion .run/pytest-in-tests.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
Expand Down
24 changes: 24 additions & 0 deletions OTAnalytics/adapter_ui/view_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,27 @@ def set_frame_track_plotting(
@abstractmethod
def set_analysis_frame(self, frame: AbstractFrame) -> None:
raise NotImplementedError

@abstractmethod
def next_frame(self) -> None:
pass

@abstractmethod
def previous_frame(self) -> None:
pass

@abstractmethod
def update_skip_time(self, seconds: int, frames: int) -> None:
raise NotImplementedError

@abstractmethod
def get_skip_seconds(self) -> int:
raise NotImplementedError

@abstractmethod
def get_skip_frames(self) -> int:
raise NotImplementedError

@abstractmethod
def set_video_control_frame(self, frame: AbstractFrame) -> None:
raise NotImplementedError
14 changes: 14 additions & 0 deletions OTAnalytics/application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
TrackViewState,
VideosMetadata,
)
from OTAnalytics.application.ui.frame_control import (
SwitchToNextFrame,
SwitchToPreviousFrame,
)
from OTAnalytics.application.use_cases.config import SaveOtconfig
from OTAnalytics.application.use_cases.create_events import (
CreateEvents,
Expand Down Expand Up @@ -98,6 +102,8 @@ def __init__(
start_new_project: StartNewProject,
project_updater: ProjectUpdater,
load_track_files: LoadTrackFiles,
previous_frame: SwitchToPreviousFrame,
next_frame: SwitchToNextFrame,
) -> None:
self._datastore: Datastore = datastore
self.track_state: TrackState = track_state
Expand Down Expand Up @@ -129,6 +135,8 @@ def __init__(
self._track_repository_size = TrackRepositorySize(
self._datastore._track_repository
)
self._previous_frame = previous_frame
self._next_frame = next_frame

def connect_observers(self) -> None:
"""
Expand Down Expand Up @@ -449,6 +457,12 @@ def get_current_track_offset(self) -> Optional[RelativeOffsetCoordinate]:
"""
return self.track_view_state.track_offset.get()

def next_frame(self) -> None:
self._next_frame.set_next_frame()

def previous_frame(self) -> None:
self._previous_frame.set_previous_frame()

def update_date_range_tracks_filter(self, date_range: DateRange) -> None:
"""Update the date range of the track filter.

Expand Down
20 changes: 16 additions & 4 deletions OTAnalytics/application/datastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ def duration(self) -> timedelta:
return self.expected_duration
return timedelta(seconds=self.number_of_frames / self.recorded_fps)

@property
def fps(self) -> float:
if self.actual_fps:
return self.actual_fps
return self.recorded_fps


@dataclass(frozen=True)
class TrackParseResult:
Expand Down Expand Up @@ -120,7 +126,7 @@ def serialize(

class VideoParser(ABC):
@abstractmethod
def parse(self, file: Path) -> Video:
def parse(self, file: Path, start_date: Optional[datetime]) -> Video:
pass

@abstractmethod
Expand Down Expand Up @@ -294,7 +300,7 @@ def load_video_files(self, files: list[Path]) -> None:
videos = []
for file in files:
try:
videos.append(self._video_parser.parse(file))
videos.append(self._video_parser.parse(file, None))
except Exception as cause:
raised_exceptions.append(cause)
if raised_exceptions:
Expand Down Expand Up @@ -514,7 +520,11 @@ def get_video_for(self, track_id: TrackId) -> Optional[Video]:
def get_all_videos(self) -> list[Video]:
return self._video_repository.get_all()

def get_image_of_track(self, track_id: TrackId) -> Optional[TrackImage]:
def get_image_of_track(
self,
track_id: TrackId,
frame: int = 0,
) -> Optional[TrackImage]:
"""
Retrieve an image for the given track.

Expand All @@ -525,4 +535,6 @@ def get_image_of_track(self, track_id: TrackId) -> Optional[TrackImage]:
Optional[TrackImage]: an image of the track if the track is available and
the image can be loaded
"""
return video.get_frame(0) if (video := self.get_video_for(track_id)) else None
return (
video.get_frame(frame) if (video := self.get_video_for(track_id)) else None
)
7 changes: 7 additions & 0 deletions OTAnalytics/application/playback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from dataclasses import dataclass


@dataclass(frozen=True)
class SkipTime:
seconds: int
frames: int
78 changes: 72 additions & 6 deletions OTAnalytics/application/plotting.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from abc import abstractmethod
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from math import floor
from typing import Any, Callable, Generic, Iterable, Optional, Sequence, TypeVar

from OTAnalytics.application.state import (
ObservableOptionalProperty,
ObservableProperty,
Plotter,
TrackViewState,
VideosMetadata,
)
from OTAnalytics.domain.track import TrackImage
from OTAnalytics.domain.video import Video


class Layer:
Expand Down Expand Up @@ -98,16 +102,31 @@ def __add(self, image: TrackImage) -> None:
self._current_image = image


class VisualizationTimeProvider(ABC):
@abstractmethod
def get_time(self) -> datetime:
raise NotImplementedError


VideoProvider = Callable[[], list[Video]]


class TrackBackgroundPlotter(Plotter):
"""Plot video frame as background."""

def __init__(self, track_view_state: TrackViewState) -> None:
self._track_view_state = track_view_state
def __init__(
self,
video_provider: VideoProvider,
visualization_time_provider: VisualizationTimeProvider,
) -> None:
self._video_provider = video_provider
self._visualization_time_provider = visualization_time_provider

def plot(self) -> Optional[TrackImage]:
if videos := self._track_view_state.selected_videos.get():
if len(videos) > 0:
return videos[0].get_frame(0)
if videos := self._video_provider():
visualization_time = self._visualization_time_provider.get_time()
frame_number = videos[0].get_frame_number_for(visualization_time)
return videos[0].get_frame(frame_number)
return None


Expand Down Expand Up @@ -238,3 +257,50 @@ def _handle_remove(self, entities: Iterable[ENTITY]) -> None:
for entity in entities:
del self._plotter_mapping[entity]
del self._layer_mapping[entity]


class GetCurrentVideoPath:
"""
This use case provides the currently visible video path. It uses the current filters
end date to retrieve the corresponding file path.
"""

def __init__(
self,
state: TrackViewState,
videos_metadata: VideosMetadata,
) -> None:
self._state = state
self._videos_metadata = videos_metadata

def get_video(self) -> Optional[str]:
if end_date := self._state.filter_element.get().date_range.end_date:
if metadata := self._videos_metadata.get_metadata_for(end_date):
return metadata.path
return None


class GetCurrentFrame:
"""
This use case provides the currently visible frame. It uses the current filters
end date to retrieve the corresponding frame.
"""

def __init__(
self,
state: TrackViewState,
videos_metadata: VideosMetadata,
) -> None:
self._state = state
self._videos_metadata = videos_metadata

def get_frame_number(self) -> int:
if end_date := self._state.filter_element.get().date_range.end_date:
if metadata := self._videos_metadata.get_metadata_for(end_date):
time_in_video = end_date - metadata.start
if time_in_video < timedelta(0):
return 0
if time_in_video > metadata.duration:
return metadata.number_of_frames
return floor(metadata.fps * time_in_video.total_seconds())
return 0
44 changes: 39 additions & 5 deletions OTAnalytics/application/state.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import bisect
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Callable, Generic, Optional

from OTAnalytics.application.config import DEFAULT_TRACK_OFFSET
from OTAnalytics.application.datastore import Datastore, VideoMetadata
from OTAnalytics.application.playback import SkipTime
from OTAnalytics.application.use_cases.section_repository import GetSectionsById
from OTAnalytics.domain.date import DateRange
from OTAnalytics.domain.event import EventRepositoryEvent
Expand Down Expand Up @@ -185,6 +187,7 @@ def __init__(self) -> None:
self.selected_videos: ObservableProperty[list[Video]] = ObservableProperty[
list[Video]
](default=[])
self.skip_time = ObservableProperty[SkipTime](SkipTime(1, 0))

def reset(self) -> None:
"""Reset to default settings."""
Expand Down Expand Up @@ -404,19 +407,50 @@ def _update_image(self) -> None:

class VideosMetadata:
def __init__(self) -> None:
self._metadata: list[VideoMetadata] = []
self._metadata: dict[datetime, VideoMetadata] = {}
self._first_video_start: Optional[datetime] = None
self._last_video_end: Optional[datetime] = None

def update(self, metadata: VideoMetadata) -> None:
self._metadata.append(metadata)
self._metadata.sort(key=lambda current: current.start)
"""
Update the stored metadata.
"""
if metadata.start in self._metadata.keys():
raise ValueError(
f"metadata with start date {metadata.start} already exists."
)
self._metadata[metadata.start] = metadata
self._metadata = dict(sorted(self._metadata.items()))
self._update_start_end_by(metadata)

def _update_start_end_by(self, metadata: VideoMetadata) -> None:
if (not self._first_video_start) or metadata.start < self._first_video_start:
self._first_video_start = metadata.start
if (not self._last_video_end) or metadata.end > self._last_video_end:
self._last_video_end = metadata.end

def get_metadata_for(self, current: datetime) -> Optional[VideoMetadata]:
"""
Find the metadata for the given datetime. If the datetime matches exactly a
start time of a video, the corresponding VideoMetadata is returned. Otherwise,
the metadata of the video containing the datetime will be returned.
"""
if current in self._metadata:
return self._metadata[current]
keys = list(self._metadata.keys())
key = bisect.bisect_left(keys, current) - 1
metadata = self._metadata[keys[key]]
if metadata.start <= current <= metadata.end:
return metadata
return None

@property
def first_video_start(self) -> Optional[datetime]:
return self._metadata[0].recorded_start_date if self._metadata else None
return self._first_video_start

@property
def last_video_end(self) -> Optional[datetime]:
return self._metadata[-1].end if self._metadata else None
return self._last_video_end


class TracksMetadata(TrackListObserver):
Expand Down
Empty file.
57 changes: 57 additions & 0 deletions OTAnalytics/application/ui/frame_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from datetime import timedelta

from OTAnalytics.application.state import TrackViewState, VideosMetadata
from OTAnalytics.domain.date import DateRange


class SwitchToNextFrame:
def __init__(self, state: TrackViewState, videos_metadata: VideosMetadata) -> None:
self._state = state
self._videos_metadata = videos_metadata

def set_next_frame(self) -> None:
if filter_element := self._state.filter_element.get():
current_date_range = filter_element.date_range
if current_date_range.start_date and current_date_range.end_date:
if metadata := self._videos_metadata.get_metadata_for(
current_date_range.end_date
):
fps = metadata.fps
skip_time = self._state.skip_time.get()
subseconds = min(skip_time.frames, fps) / fps
milliseconds = subseconds * 1000
current_skip = timedelta(
seconds=skip_time.seconds, milliseconds=milliseconds
)
next_start = current_date_range.start_date + current_skip
next_end = current_date_range.end_date + current_skip
next_date_range = DateRange(next_start, next_end)
self._state.filter_element.set(
filter_element.derive_date(next_date_range)
)


class SwitchToPreviousFrame:
def __init__(self, state: TrackViewState, videos_metadata: VideosMetadata) -> None:
self._state = state
self._videos_metadata = videos_metadata

def set_previous_frame(self) -> None:
if filter_element := self._state.filter_element.get():
current_date_range = filter_element.date_range
if current_date_range.start_date and current_date_range.end_date:
if metadata := self._videos_metadata.get_metadata_for(
current_date_range.end_date
):
fps = metadata.fps
skip_time = self._state.skip_time.get()
subseconds = min(skip_time.frames, fps) / fps
current_skip = timedelta(seconds=skip_time.seconds) + timedelta(
seconds=subseconds
)
next_start = current_date_range.start_date - current_skip
next_end = current_date_range.end_date - current_skip
next_date_range = DateRange(next_start, next_end)
self._state.filter_element.set(
filter_element.derive_date(next_date_range)
)
Empty file.
3 changes: 3 additions & 0 deletions OTAnalytics/domain/track.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ def height(self) -> int:
"""
pass

def save(self, name: str) -> None:
self.as_image().save(name)


@dataclass(frozen=True)
class PilImage(TrackImage):
Expand Down
Loading
Loading