Skip to content

Commit

Permalink
Merge branch 'feature/annotation-visualization' into feature/annotati…
Browse files Browse the repository at this point in the history
…on-prototype
  • Loading branch information
ShrimpCryptid committed Dec 23, 2024
2 parents 119917d + 622762c commit cf94f4f
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 75 deletions.
7 changes: 2 additions & 5 deletions src/assets/images/icon-tag-slash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions src/assets/images/icon-tag.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 61 additions & 24 deletions src/colorizer/canvas/elements/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,55 @@ export type AnnotationStyle = {
markerSizePx: number;
borderColor: string;
additionalItemsOffsetPx: number;
/** Percentage, as a number from 0 to 1, of how much to scale the annotation
* size with the zoom level. 0 means no scaling (the marker size is fixed in
* onscreen pixels), 1 means the annotation markers will scale linearly with
* the zoom level (at 2x zoom the markers will be 2x bigger).
*/
scaleWithZoomPct: number;
};

export const defaultAnnotationStyle = {
markerSizePx: 5,
borderColor: "white",
additionalItemsOffsetPx: 2,
scaleWithZoomPct: 0.25,
};

function drawAnnotation(
/** Transforms a 2D frame pixel coordinate into a 2D canvas pixel coordinate,
* accounting for panning and zooming. For both, (0,0) is the top left corner.
*/
function framePixelCoordsToCanvasPixelCoords(pos: Vector2, params: AnnotationParams): Vector2 {
// Position is in pixel coordinates of the frame. Transform to relative frame coordinates,
// then to relative canvas coordinates, and finally into canvas pixel coordinates.
const frameResolution = params.dataset?.frameResolution;
if (!frameResolution) {
return new Vector2(0, 0);
}
pos = pos.clone();
pos.divide(frameResolution); // to relative frame coordinates
pos.sub(new Vector2(0.5, 0.5)); // Center (0,0) at center of frame
pos.add(params.panOffset.clone().multiply(new Vector2(1, -1))); // apply panning offset
pos.multiply(params.frameToCanvasCoordinates); // to relative canvas coordinates
pos.multiply(params.canvasSize); // to canvas pixel coordinates
pos.add(params.canvasSize.clone().multiplyScalar(0.5)); // Move origin to top left corner
return pos;
}

/**
* Draws an annotation marker over the given object ID, handling zooming,
* panning, and multiple labels.
* @param origin Origin of the parent annotation component (should be the top
* left corner of the canvas viewport).
* @param ctx 2D canvas rendering context.
* @param params The annotation parameters.
* @param style The annotation styling.
* @param id The object ID to render. The object's centroid will be used as the
* marker position.
* @param labelIdx The indices of all labels to render for this object ID. The
* first label index in the list will be rendered as the main marker.
*/
function drawAnnotationMarker(
origin: Vector2,
ctx: CanvasRenderingContext2D,
params: AnnotationParams,
Expand All @@ -37,38 +77,34 @@ function drawAnnotation(
): void {
const labelData = params.labelData[labelIdx[0]];
const centroid = params.dataset?.getCentroid(id);
const frameResolution = params.dataset?.frameResolution;
if (!centroid || !frameResolution) {
if (!centroid || !params.dataset) {
return;
}

// Position is in pixel coordinates of the frame. Transform to relative frame coordinates,
// then to relative canvas coordinates, and finally into canvas pixel coordinates.
const pos = new Vector2(centroid[0], centroid[1]);
pos.divide(frameResolution); // to relative frame coordinates
pos.sub(new Vector2(0.5, 0.5)); // Center (0,0) at center of frame
pos.add(params.panOffset.clone().multiply(new Vector2(1, -1))); // apply panning offset
pos.multiply(params.frameToCanvasCoordinates); // to relative canvas coordinates
pos.multiply(params.canvasSize); // to canvas pixel coordinates
pos.add(params.canvasSize.clone().multiplyScalar(0.5)); // Move origin to top left corner

const renderPos = new Vector2(pos.x + origin.x, pos.y + origin.y);

ctx.fillStyle = "#" + labelData.color.getHexString();
const pos = framePixelCoordsToCanvasPixelCoords(new Vector2(centroid[0], centroid[1]), params);
pos.add(origin);
ctx.strokeStyle = style.borderColor;

// Render an extra outline if multiple labels are present.
// Scale markers by the zoom level.
const zoomScale = Math.max(params.frameToCanvasCoordinates.x, params.frameToCanvasCoordinates.y);
const dampenedZoomScale = zoomScale * style.scaleWithZoomPct + (1 - style.scaleWithZoomPct);
const scaledMarkerSizePx = style.markerSizePx * dampenedZoomScale;

// Draw an additional marker behind the main one if there are multiple labels.
if (labelIdx.length > 1) {
const offsetRenderPos = renderPos.clone().addScalar(style.additionalItemsOffsetPx);
ctx.fillStyle = "#" + params.labelData[labelIdx[1]].color.getHexString();
const offsetPos = pos.clone().addScalar(style.additionalItemsOffsetPx * dampenedZoomScale);
ctx.beginPath();
ctx.arc(offsetRenderPos.x, offsetRenderPos.y, style.markerSizePx, 0, 2 * Math.PI);
ctx.arc(offsetPos.x, offsetPos.y, scaledMarkerSizePx, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
ctx.stroke();
}

// Draw the marker at the new position.
// Draw the main marker.
ctx.fillStyle = "#" + labelData.color.getHexString();
ctx.beginPath();
ctx.arc(renderPos.x, renderPos.y, style.markerSizePx, 0, 2 * Math.PI);
ctx.arc(pos.x, pos.y, scaledMarkerSizePx, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
ctx.stroke();
Expand All @@ -88,7 +124,8 @@ export function getAnnotationRenderer(
render: (origin: Vector2) => {
const currentLabelToIds = params.timeToLabelIds.get(params.frame) || {};

// Remap from ids to labels.
// Remap from labels->ids to ids->labels. Adjust ordering to render the
// currently selected label first if there is one.
const idsToLabels = new Map<number, number[]>();
for (const labelIdString in currentLabelToIds) {
const labelId = parseInt(labelIdString, 10);
Expand All @@ -106,8 +143,8 @@ export function getAnnotationRenderer(
}
}

for (const [id, labelIdx] of idsToLabels) {
drawAnnotation(origin, ctx, params, style, id, labelIdx);
for (const [id, labelIdxs] of idsToLabels) {
drawAnnotationMarker(origin, ctx, params, style, id, labelIdxs);
}
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/colorizer/utils/react_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ export const useAnnotations = (): AnnotationState => {

const [currentLabelIdx, setCurrentLabelIdx] = useState<number | null>(null);
const [isAnnotationEnabled, _setIsAnnotationEnabled] = useState<boolean>(false);
const [visible, _setVisibility] = useState<boolean>(true);
const [visible, _setVisibility] = useState<boolean>(false);

// Annotation mode can only be enabled if there is at least one label, so create
// one if necessary.
Expand Down
37 changes: 14 additions & 23 deletions src/components/CanvasWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,25 +347,16 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem
canv.setOutlineColor(props.config.outlineColor);
}, [props.config.outlineColor]);

// Memoize because mapping from time to annotation labels is potentially expensive
const timeToAnnotationLabelIds = useMemo(() => {
if (props.dataset) {
return props.annotationState.getTimeToLabelIdMap(props.dataset);
} else {
return new Map<number, Record<number, number[]>>();
}
}, [props.dataset, props.annotationState.annotationDataVersion]);
const annotationLabels = useMemo(
() => props.annotationState.getLabels(),
[props.annotationState.annotationDataVersion]
);

useMemo(() => {
const annotationLabels = props.annotationState.getLabels();
const timeToAnnotationLabelIds = props.dataset
? props.annotationState.getTimeToLabelIdMap(props.dataset)
: new Map();
canv.setAnnotationData(annotationLabels, timeToAnnotationLabelIds, props.annotationState.currentLabelIdx);
canv.isAnnotationVisible = props.annotationState.visible;
}, [
timeToAnnotationLabelIds,
annotationLabels,
props.dataset,
props.annotationState.annotationDataVersion,
props.annotationState.currentLabelIdx,
props.annotationState.visible,
]);
Expand Down Expand Up @@ -650,7 +641,7 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem
};
}, [props.dataset, canv]);

const makeTooltipLinkStyleButton = (key: string, content: ReactNode, onClick: () => void): ReactNode => {
const makeLinkStyleButton = (key: string, onClick: () => void, content: ReactNode): ReactNode => {
return (
<LinkStyleButton
key={key}
Expand Down Expand Up @@ -686,12 +677,12 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem
);
// Link to viewer settings
backdropTooltipContents.push(
makeTooltipLinkStyleButton(
makeLinkStyleButton(
"backdrop-viewer-settings-link",
onViewerSettingsLinkClicked,
<span>
{"Viewer settings > Backdrop"} <VisuallyHidden>(opens settings tab)</VisuallyHidden>
</span>,
onViewerSettingsLinkClicked
</span>
)
);

Expand All @@ -703,19 +694,19 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem
</span>
);
annotationTooltipContents.push(
makeTooltipLinkStyleButton(
makeLinkStyleButton(
"annotation-link",
onAnnotationLinkClicked,
<span>
View and edit annotations <VisuallyHidden>(opens annotations tab)</VisuallyHidden>
</span>,
onAnnotationLinkClicked
</span>
)
);

return (
<CanvasContainer ref={containerRef} $annotationModeEnabled={props.annotationState.isAnnotationModeEnabled}>
{props.annotationState.isAnnotationModeEnabled && (
<AnnotationModeContainer>Annotation edit mode enabled</AnnotationModeContainer>
<AnnotationModeContainer>Annotation editing in progress...</AnnotationModeContainer>
)}
<LoadingSpinner loading={props.loading} progress={props.loadingProgress}>
<div ref={canvasPlaceholderRef}></div>
Expand Down
24 changes: 9 additions & 15 deletions src/components/Tabs/AnnotationTab.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import {
CheckOutlined,
CloseOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
TagOutlined,
} from "@ant-design/icons";
import { CheckOutlined, CloseOutlined, DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons";
import { Color as AntdColor } from "@rc-component/color-picker";
import { Button, ColorPicker, Input, InputRef, Popover, Table, TableProps, Tooltip } from "antd";
import { ItemType } from "antd/es/menu/hooks/useItems";
import React, { ReactElement, useContext, useEffect, useMemo, useRef, useState } from "react";
import styled, { css } from "styled-components";
import { Color, HexColorString } from "three";

import { TagIconSVG } from "../../assets";
import { Dataset } from "../../colorizer";
import { AnnotationState } from "../../colorizer/utils/react_utils";
import { FlexColumn, FlexColumnAlignCenter, FlexRow, VisuallyHidden } from "../../styles/utils";
import { FlexColumn, FlexColumnAlignCenter, FlexRow, FlexRowAlignCenter, VisuallyHidden } from "../../styles/utils";

import { LabelData } from "../../colorizer/AnnotationData";
import { AppThemeContext } from "../AppStyle";
Expand Down Expand Up @@ -247,8 +241,10 @@ export default function AnnotationTab(props: AnnotationTabProps): ReactElement {
style={{ paddingLeft: "10px" }}
onClick={() => setIsAnnotationModeEnabled(!isAnnotationModeEnabled)}
>
{isAnnotationModeEnabled ? <CheckOutlined /> : <TagOutlined />}
{isAnnotationModeEnabled ? "Done editing" : "Create and edit"}
<FlexRowAlignCenter $gap={6}>
{isAnnotationModeEnabled ? <CheckOutlined /> : <TagIconSVG />}
{isAnnotationModeEnabled ? "Done editing" : "Create and edit"}
</FlexRowAlignCenter>
</AnnotationModeButton>
{isAnnotationModeEnabled && (
<p style={{ color: theme.color.text.hint }}>
Expand Down Expand Up @@ -346,10 +342,8 @@ export default function AnnotationTab(props: AnnotationTabProps): ReactElement {
}}
locale={{
emptyText: (
<FlexColumnAlignCenter>
<span style={{ fontSize: "24px", marginBottom: 0 }}>
<TagOutlined />
</span>
<FlexColumnAlignCenter style={{ margin: "16px 0 10px 0" }}>
<TagIconSVG style={{ width: "24px", height: "24px", marginBottom: 0 }} />
<p>No annotated IDs</p>
</FlexColumnAlignCenter>
),
Expand Down
7 changes: 5 additions & 2 deletions src/components/Tooltips/CanvasHoverTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export default function CanvasHoverTooltip(props: PropsWithChildren<CanvasHoverT
objectInfoContent.push(<p key="vector">{vectorTooltipText}</p>);
}

// Show all current labels applied to the hovered object
const labels = props.annotationState.getLabelsAppliedToId(lastHoveredId);
const labelData = props.annotationState.getLabels();
if (labels.length > 0 && props.annotationState.visible) {
Expand All @@ -122,11 +123,13 @@ export default function CanvasHoverTooltip(props: PropsWithChildren<CanvasHoverT
{labels.map((labelIdx) => {
const label = labelData[labelIdx];
return (
// TODO: Tags do not change their text color based on the background color.
// Make a custom wrapper for Tag that does this; see
// https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
<Tag
key={labelIdx}
style={{ width: "fit-content", margin: "0 2px" }}
color={"#" + label.color.getHexString()}
bordered={true}
>
{label.name}
</Tag>
Expand All @@ -136,6 +139,7 @@ export default function CanvasHoverTooltip(props: PropsWithChildren<CanvasHoverT
);
}

// If editing annotations, also show the current label being applied
let annotationLabel: React.ReactNode;
const currentLabelIdx = props.annotationState.currentLabelIdx;
if (props.annotationState.isAnnotationModeEnabled && currentLabelIdx !== null) {
Expand All @@ -147,7 +151,6 @@ export default function CanvasHoverTooltip(props: PropsWithChildren<CanvasHoverT
);
}

// TODO: Eventually this will also show the current annotation tag when annotation mode is enabled.
const tooltipContent = (
<FlexColumn $gap={6}>
{annotationLabel}
Expand Down

0 comments on commit cf94f4f

Please sign in to comment.