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

feat: LEAP-1367: Add TimelineLabels to mark parts of the video #6191

Merged
merged 44 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2200a7f
feat: LEAP-1362: Add TimelineLabels to mark parts of the video
hlomzik Aug 13, 2024
c64dfae
Add special appearance for timeline regions
hlomzik Aug 13, 2024
c042f70
Properly serialize timelinelabels
hlomzik Aug 13, 2024
d5689bc
Tiny fixes
hlomzik Aug 13, 2024
549bb4f
ci: Build frontend
robot-ci-heartex Aug 13, 2024
fd7583a
Allow to draw only on empty space or special line
hlomzik Aug 14, 2024
e264642
Add quazi-line to create new regions
hlomzik Aug 14, 2024
d253ff3
Merge remote-tracking branch 'origin/fb-leap-1362/timeline-regions' i…
hlomzik Aug 14, 2024
e26a79f
Fix drawing on empty space (allowed) and region label (forbidden)
hlomzik Aug 15, 2024
009832d
Show correct region index like in Outliner
hlomzik Aug 15, 2024
ffec5bf
Trigger CI
hlomzik Aug 15, 2024
3fbd316
Merge branch 'develop' into 'fb-leap-1362/timeline-regions'
hlomzik Aug 16, 2024
339beec
Add `timelineHeight` param to Video tag
hlomzik Aug 20, 2024
de8253b
Fix missing index passing + some optional types
hlomzik Aug 20, 2024
ed7744e
Add only the final state to undo history after drawing the region
hlomzik Aug 20, 2024
7bdf8ed
Don't break existing Video rectangles regions
hlomzik Aug 20, 2024
9abef00
Suppress timeline labels behaviour if there are no TimelineLabels
hlomzik Aug 20, 2024
f7422f0
Merge branch 'develop' into 'fb-leap-1362/timeline-regions'
hlomzik Aug 20, 2024
490bf0c
Fix conditions for drawing frames in the timeline
hlomzik Aug 23, 2024
f368be7
Fix labels behaviour
hlomzik Aug 23, 2024
7b879eb
Make TimelineLabels to behave like regular labels
hlomzik Aug 23, 2024
730ee50
Disable extra controls for timeline regions
hlomzik Aug 23, 2024
d5f7cbc
Reverse regions only if TimelineLabels is presented
hlomzik Aug 23, 2024
3369227
Fix and improve docs about TimelineLabels
hlomzik Aug 23, 2024
dae4848
Added a screenshot and a bit of text
Aug 23, 2024
f16a781
Fixing image link
Aug 23, 2024
bc245cc
Making fixes per Andrew's feedback
Aug 23, 2024
e9c9166
Fix RichText docs to unblock docs auto-generation
hlomzik Aug 23, 2024
e2599ce
ci: Build frontend
robot-ci-heartex Aug 23, 2024
7777cc2
Update wording for results JSON
Aug 23, 2024
79529d7
Small comment about undo history management
hlomzik Aug 28, 2024
4665000
Split adding rectangle and timeline regions
hlomzik Aug 28, 2024
776480e
Merge branch 'develop' into fb-leap-1362/timeline-regions
hlomzik Aug 28, 2024
2226699
ci: Build frontend
robot-ci-heartex Aug 28, 2024
bf32fb7
Fix issues with srubbing and hopping in Video
hlomzik Aug 30, 2024
98c9c53
Merge branch 'develop' into 'fb-leap-1362/timeline-regions'
hlomzik Sep 3, 2024
09110df
Tiny fixes, comments, renamed setSequence -> setRanges
hlomzik Sep 3, 2024
d827b31
Fix region restoration for VideoRectangles
hlomzik Sep 3, 2024
4bb0985
Fix test to move indicator window back and forth
hlomzik Sep 4, 2024
8dfcb2d
Fix hovered and selected state for all regions in video timeline
hlomzik Sep 5, 2024
ab969e8
Merge branch 'develop' into fb-leap-1362/timeline-regions
hlomzik Sep 6, 2024
21f8ad5
Fix linting
hlomzik Sep 9, 2024
56539fe
Style lint fixes
hlomzik Sep 9, 2024
f1272b6
ci: Build frontend
robot-ci-heartex Sep 9, 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
58 changes: 58 additions & 0 deletions docs/source/tags/timelinelabels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: TimelineLabels
type: tags
order: 429
is_new: t
meta_title: TimelineLabels tag
meta_description: Classify video frames using TimelineLabels.
---

Use the TimelineLabels tag to classify video frames. This can be a single frame or a span of frames.

First, select a label and then click once to annotate a single frame. Click and drag to annotate multiple frames.

To move forward and backward in the timeline without labeling, ensure that no labels are selected before you click.

![Screenshot of video with frame classification](../images/timelinelabels.png)

Use with the `<Video>` control tag.

!!! info Tip
You can increase the height of the timeline using the `timelineHeight` parameter on the `<Video>` tag.

### Parameters

| Param | Type | Description |
| --- | --- | --- |
| name | <code>string</code> | Name of the element |
| toName | <code>string</code> | Name of the video element |

### Sample Results JSON

| Name | Type | Description |
| --- | --- | --- |
| value | <code>Object</code> | |
| value.ranges | <code>Array.&lt;object&gt;</code> | Array of ranges, each range is an object with `start` and `end` properties. One range per region. |
| [value.timelinelabels] | <code>Array.&lt;string&gt;</code> | Regions are created by `TimelineLabels`, and the corresponding label is listed here. |

### Example JSON
```json
{
"value": {
"ranges": [{"start": 3, "end": 5}],
"timelinelabels": ["Moving"]
}
}
```

### Example
```html
<View>
<Header>Label timeline spans:</Header>
<Video name="video" value="$video" />
<TimelineLabels name="timelineLabels" toName="video">
<Label value="Nothing" background="#944BFF"/>
<Label value="Movement" background="#98C84E"/>
</TimelineLabels>
</View>
```
3 changes: 2 additions & 1 deletion docs/source/tags/video.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Use with the following data types: video
| [frameRate] | <code>number</code> | <code>24</code> | video frame rate per second; default is 24; can use task data like `$fps` |
| [sync] | <code>string</code> | | object name to sync with |
| [muted] | <code>boolean</code> | <code>false</code> | muted video |
| [height] | <code>number</code> | <code>600</code> | height of the video |
| [height] | <code>number</code> | <code>600</code> | height of the video player |
| [timelineHeight] | <code>number</code> | <code>64</code> | height of the timeline with regions |

### Example

Expand Down
2 changes: 1 addition & 1 deletion docs/source/tags/videorectangle.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: VideoRectangle
type: tags
order: 429
order: 430
meta_title: Video Tag for Video Labeling
meta_description: Customize Label Studio with the Video tag for basic video annotation tasks for machine learning and data science projects.
---
Expand Down
Binary file added docs/themes/v2/source/images/timelinelabels.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion web/libs/editor/src/components/Timeline/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type FC, useEffect, useMemo, useRef, useState } from "react";
import { useLocalStorageState } from "../../hooks/useLocalStorageState";
import { useMemoizedHandlers } from "../../hooks/useMemoizedHandlers";
import { Block, Elem } from "../../utils/bem";
import { clamp, isDefined } from "../../utils/utilities";
import { clamp, fixMobxObserve, isDefined } from "../../utils/utilities";
import { TimelineContextProvider } from "./Context";
import { Controls } from "./Controls";
import { Seeker } from "./Seeker";
Expand Down Expand Up @@ -62,6 +62,8 @@ const TimelineComponent: FC<TimelineProps> = ({
onAddRegion: props.onAddRegion,
onDeleteRegion: props.onDeleteRegion,
onSelectRegion: props.onSelectRegion,
onStartDrawing: props.onStartDrawing,
onFinishDrawing: props.onFinishDrawing,
onAction: props.onAction,
onFullscreenToggle: props.onFullscreenToggle,
onSpeedChange: props.onSpeedChange,
Expand Down Expand Up @@ -172,6 +174,8 @@ const TimelineComponent: FC<TimelineProps> = ({
</Elem>
);

regions.map((reg) => fixMobxObserve(reg.sequence));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regions.map call on this line is not used and can be removed to improve performance.


const view = !viewCollapsed && !disableView && (
<Elem name="view">
<View.View
Expand All @@ -183,6 +187,7 @@ const TimelineComponent: FC<TimelineProps> = ({
speed={speed}
volume={props.volume}
controls={props.controls}
height={props.height}
position={currentPosition}
offset={seekOffset}
leftOffset={View.settings?.leftOffset}
Expand All @@ -197,6 +202,8 @@ const TimelineComponent: FC<TimelineProps> = ({
onAddRegion={(reg) => handlers.onAddRegion?.(reg)}
onDeleteRegion={(id) => handlers.onDeleteRegion?.(id)}
onSelectRegion={(e, id, select) => handlers.onSelectRegion?.(e, id, select)}
onStartDrawing={(frame) => handlers.onStartDrawing?.(frame)}
onFinishDrawing={() => handlers.onFinishDrawing?.()}
onSpeedChange={(speed) => handlers.onSpeedChange?.(speed)}
onZoom={props.onZoom}
/>
Expand Down
15 changes: 12 additions & 3 deletions web/libs/editor/src/components/Timeline/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface TimelineProps<D extends ViewTypes = "frames"> {
regions: any[];
length: number;
position: number;
height?: number;
mode: D;
framerate: number;
playing: boolean;
Expand Down Expand Up @@ -39,6 +40,8 @@ export interface TimelineProps<D extends ViewTypes = "frames"> {
onToggleVisibility?: (id: string, visibility: boolean) => void;
onAddRegion?: (region: Record<string, any>) => any;
onDeleteRegion?: (id: string) => void;
onStartDrawing?: (frame: number) => void;
onFinishDrawing?: () => void;
onZoom?: (zoom: number) => void;
onSelectRegion?: (event: MouseEvent<HTMLDivElement>, id: string, select?: boolean) => void;
onAction?: (event: MouseEvent, action: string, data?: any) => void;
Expand All @@ -58,6 +61,7 @@ export interface TimelineViewProps {
speed?: number;
volume?: number;
regions: TimelineRegion[];
height?: number;
leftOffset?: number;
controls?: TimelineControls;
onScroll: (position: number) => void;
Expand All @@ -73,17 +77,22 @@ export interface TimelineViewProps {
onAddRegion?: TimelineProps["onAddRegion"];
onDeleteRegion?: TimelineProps["onDeleteRegion"];
onSelectRegion?: TimelineProps["onSelectRegion"];
onStartDrawing?: TimelineProps["onStartDrawing"];
onFinishDrawing?: TimelineProps["onFinishDrawing"];
onVolumeChange?: TimelineProps["onVolumeChange"];
onSpeedChange?: TimelineProps["onSpeedChange"];
}

export interface TimelineRegion {
id: string;
index?: number;
label: string;
color: string;
visible: boolean;
selected: boolean;
sequence: TimelineRegionKeyframe[];
/** is this timeline region with spans */
timeline?: boolean;
hlomzik marked this conversation as resolved.
Show resolved Hide resolved
}

export interface TimelineRegionKeyframe {
Expand Down Expand Up @@ -176,9 +185,9 @@ export interface TimelineControlsProps {
onPause?: TimelineProps["onPause"];
onFullScreenToggle: TimelineProps["onFullscreenToggle"];
onVolumeChange: TimelineProps["onVolumeChange"];
onSpeedChange: TimelineProps["onSpeedChange"];
onZoom: TimelineProps["onZoom"];
onAmpChange: (amp: number) => void;
onSpeedChange?: TimelineProps["onSpeedChange"];
onZoom?: TimelineProps["onZoom"];
onAmpChange?: (amp: number) => void;
toggleVisibility?: (layerName: string, isVisible: boolean) => void;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ type DataType = {

export const Controls: FC<TimelineExtraControls<Actions, DataType>> = ({ onAction }) => {
const { position, regions } = useContext(TimelineContext);
const hasSelectedRegion = regions.some(({ selected }) => selected);
const hasSelectedRegion = regions.some(({ selected, timeline }) => selected && !timeline);
const closestKeypoint = useMemo(() => {
const region = regions.find((r) => r.selected);
const region = regions.find((r) => r.selected && !r.timeline);

return region?.sequence.filter(({ frame }) => frame <= position).slice(-1)[0];
}, [regions, position]);
Expand Down
60 changes: 43 additions & 17 deletions web/libs/editor/src/components/Timeline/Views/Frames/Frames.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { type FC, type MouseEvent, useCallback, useEffect, useMemo, useRef, useS
import { useMemoizedHandlers } from "../../../../hooks/useMemoizedHandlers";
import { Block, Elem } from "../../../../utils/bem";
import { isDefined } from "../../../../utils/utilities";
import type { TimelineViewProps } from "../../Types";
import "./Frames.scss";
import type { TimelineRegion, TimelineViewProps } from "../../Types";
import { Keypoints } from "./Keypoints";
import "./Frames.scss";

const toSteps = (num: number, step: number) => {
return Math.floor(num / step);
Expand Down Expand Up @@ -71,7 +71,7 @@ export const Frames: FC<TimelineViewProps> = ({
}, [step]);

const setScroll = useCallback(
({ left, top }) => {
({ left, top }: { left?: number; top?: number }) => {
if (!length) return;

setHoverOffset(null);
Expand Down Expand Up @@ -196,6 +196,14 @@ export const Frames: FC<TimelineViewProps> = ({
const dimensions = scrollable.current!.getBoundingClientRect();
const offsetLeft = dimensions.left;
const rightLimit = dimensions.width - timelineStartOffset;
const target = e.target as Element;
// every region has `data-id` attribute, so looking for them
const regionRow = target.closest("[data-id]") as HTMLElement | null;
// not clicking on labels area
const onKeyframes = e.pageX - offsetLeft > timelineStartOffset;
// don't draw on region lines, only on the empty space or special new line
const isDrawing = onKeyframes && (!regionRow || regionRow.dataset?.id === "new");
let region: any;

const getMouseToFrame = (e: MouseEvent | globalThis.MouseEvent) => {
const mouseOffset = e.pageX - offsetLeft - timelineStartOffset;
Expand All @@ -204,22 +212,35 @@ export const Frames: FC<TimelineViewProps> = ({
};

const offset = getMouseToFrame(e);
const baseFrame = toSteps(offset, step) + 1;

setIndicatorOffset(offset);

if (isDrawing) {
// always a timeline region
region = props.onStartDrawing?.(baseFrame);
}

const onMouseMove = (e: globalThis.MouseEvent) => {
const offset = getMouseToFrame(e);
const frame = toSteps(offset, step) + 1;

if (offset >= 0 && offset <= rightLimit) {
setHoverEnabled(false);
setRegionSelectionDisabled(true);
setIndicatorOffset(offset);
}

if (region) {
const [start, end] = frame > baseFrame ? [baseFrame, frame] : [frame, baseFrame];
region.setRanges([start, end]);
}
};

const onMouseUp = () => {
setHoverEnabled(true);
setRegionSelectionDisabled(false);
props.onFinishDrawing?.();
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
Expand Down Expand Up @@ -282,8 +303,9 @@ export const Frames: FC<TimelineViewProps> = ({

if (!isDefined(scroll) || framesInView < 1) return;

const firstFrame = toSteps(roundToStep(lastOffsetX.current, step), step);
const lastFrame = firstFrame + framesInView;
// offsets are zero based, but position is 1 based
const firstFrame = toSteps(roundToStep(lastOffsetX.current, step), step) + 1;
const lastFrame = firstFrame + framesInView - 1;

const positionDelta = Math.abs(position - lastPosition.current);

Expand All @@ -293,19 +315,19 @@ export const Frames: FC<TimelineViewProps> = ({
// this ensures the calculation of offset is kept correct.
// This is needed because the position is not always a multiple of the step
// and the offset used to calculate the position is always a multiple of the step.
if (positionDelta === 1 && position >= firstFrame && position <= lastFrame) {
// set to previous frame scroll
// if position is 0, then it will be set to 0
if (position <= firstFrame) {
if (positionDelta === 1 && (position < firstFrame || position > lastFrame)) {
// scroll to previous page if we are going outside of the current one
if (position < firstFrame) {
const prevLeft = clamp((firstFrame - 1 - framesInView) * step, 0, scroll.scrollWidth - scroll.clientWidth);

lastScrollPosition.current = roundToStep(prevLeft, step);

setScroll({ left: prevLeft });

// set to next frame scroll
// if position is last frame, then it will be set to last frame scroll
// scroll to the next page if we are going outside of the current one
} else if (position > lastFrame) {
// offsets are zero based, but position is 1 based,
// so technically that's +1 to go to the next page, but -1 to switch to offsets
const nextLeft = clamp(lastFrame * step, 0, scroll.scrollWidth - scroll.clientWidth);

lastScrollPosition.current = roundToStep(nextLeft, step);
Expand All @@ -319,15 +341,19 @@ export const Frames: FC<TimelineViewProps> = ({
// Handle position change outside of the current scroll
// This updates when the user clicks within the track to change the position
// or when keyframe hops are used and the position is changed more than 1 frame
const scrollTo = roundToStep(position, framesInView);
const scrollTo = roundToStep(position - 1, framesInView);
// how far are we from the start of currently visible window
const diff = (position - 1) * step - lastScrollPosition.current;

if (lastScrollPosition.current !== scrollTo) {
if (diff > (framesInView - 1) * step || diff < 0) {
setScroll({ left: scrollTo * step });
// frames
lastScrollPosition.current = scrollTo * step;
}
lastScrollPosition.current = scrollTo;
}, [position, framesInView, step]);

const styles = {
"--view-height": props.height ? `${props.height}px` : null,
"--frame-size": `${step}px`,
"--view-size": `${viewWidth}px`,
"--offset": `${timelineStartOffset}px`,
Expand Down Expand Up @@ -379,7 +405,7 @@ export const Frames: FC<TimelineViewProps> = ({
};

interface KeypointsVirtualProps {
regions: any[];
regions: TimelineRegion[];
startOffset: number;
scrollTop: number;
disabled?: boolean;
Expand All @@ -399,10 +425,10 @@ const KeypointsVirtual: FC<KeypointsVirtualProps> = ({ regions, startOffset, scr
return (
<Elem name="keypoints" style={{ height: regions.length * height }}>
{regions.map((region, i) => {
return region.sequence.length > 0 ? (
return region.sequence.length > 0 || region.timeline ? (
<Keypoints
key={region.id}
idx={i + 1}
idx={region.index}
region={region}
startOffset={startOffset}
onSelectRegion={disabled ? undefined : onSelectRegion}
Expand Down
Loading
Loading