Skip to content

Commit 85b77ad

Browse files
authored
Move segment-anything tool (#4735)
1 parent 8f488f8 commit 85b77ad

22 files changed

+855
-265
lines changed

ui/package-lock.json

Lines changed: 126 additions & 173 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/rsbuild.config.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export default defineConfig({
2929
},
3030
}),
3131
],
32-
3332
source: {
3433
define: {
3534
...publicVars,
@@ -54,4 +53,17 @@ export default defineConfig({
5453
},
5554
},
5655
},
56+
server: {
57+
headers: {
58+
'Cross-Origin-Embedder-Policy': 'credentialless',
59+
'Cross-Origin-Opener-Policy': 'same-origin',
60+
'Content-Security-Policy':
61+
"default-src 'self'; " +
62+
"script-src 'self' 'unsafe-eval' blob:; " +
63+
"worker-src 'self' blob:; " +
64+
"connect-src 'self' http://localhost:7860 data:; " +
65+
"img-src 'self' http://localhost:7860 data: blob:; " +
66+
"style-src 'self' 'unsafe-inline';",
67+
},
68+
},
5769
});

ui/src/features/annotator/annotations/annotations.component.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (C) 2025 Intel Corporation
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { CSSProperties } from 'react';
5+
6+
import { isEmpty } from 'lodash-es';
7+
48
import { useAnnotator } from '../annotator-provider.component';
59
import { useSelectedAnnotations } from '../select-annotation-provider.component';
610
import { Annotation } from './annotation.component';
@@ -12,6 +16,17 @@ type AnnotationsProps = {
1216
isFocussed: boolean;
1317
};
1418

19+
const DEFAULT_ANNOTATION_STYLES = {
20+
fillOpacity: 0.4,
21+
fill: 'var(--annotation-fill)',
22+
stroke: 'var(--annotation-stroke)',
23+
strokeLinecap: 'round',
24+
strokeWidth: 'calc(1px / var(--zoom-scale))',
25+
strokeDashoffset: 0,
26+
strokeDasharray: 0,
27+
strokeOpacity: 'var(--annotation-border-opacity, 1)',
28+
} satisfies CSSProperties;
29+
1530
export const Annotations = ({ width, height, isFocussed }: AnnotationsProps) => {
1631
const { annotations } = useAnnotator();
1732
const { selectedAnnotations } = useSelectedAnnotations();
@@ -22,11 +37,17 @@ export const Annotations = ({ width, height, isFocussed }: AnnotationsProps) =>
2237
...annotations.filter((a) => selectedAnnotations.has(a.id)),
2338
];
2439

40+
if (isEmpty(annotations)) {
41+
return <></>;
42+
}
43+
2544
return (
26-
<MaskAnnotations annotations={orderedAnnotations} width={width} height={height} isEnabled={isFocussed}>
27-
{orderedAnnotations.map((annotation) => (
28-
<Annotation annotation={annotation} key={annotation.id} />
29-
))}
30-
</MaskAnnotations>
45+
<svg width={width} height={height} style={DEFAULT_ANNOTATION_STYLES}>
46+
<MaskAnnotations annotations={orderedAnnotations} width={width} height={height} isEnabled={isFocussed}>
47+
{orderedAnnotations.map((annotation) => (
48+
<Annotation annotation={annotation} key={annotation.id} />
49+
))}
50+
</MaskAnnotations>
51+
</svg>
3152
);
3253
};

ui/src/features/annotator/annotator-canvas.tsx

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
// Copyright (C) 2025 Intel Corporation
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { CSSProperties, MouseEvent } from 'react';
4+
import { PointerEvent } from 'react';
55

66
import { Grid, View } from '@geti/ui';
7-
import { isEmpty } from 'lodash-es';
87

98
import { ZoomProvider } from '../../components/zoom/zoom';
109
import { ZoomTransform } from '../../components/zoom/zoom-transform';
@@ -15,29 +14,20 @@ import { useSelectedAnnotations } from './select-annotation-provider.component';
1514
import { ToolManager } from './tools/tool-manager.component';
1615
import { Annotation, DatasetItem } from './types';
1716

18-
const DEFAULT_ANNOTATION_STYLES = {
19-
fillOpacity: 0.4,
20-
fill: 'var(--annotation-fill)',
21-
stroke: 'var(--annotation-stroke)',
22-
strokeLinecap: 'round',
23-
strokeWidth: 'calc(1px / var(--zoom-scale))',
24-
strokeDashoffset: 0,
25-
strokeDasharray: 0,
26-
strokeOpacity: 'var(--annotation-border-opacity, 1)',
27-
} satisfies CSSProperties;
28-
2917
type AnnotatorCanvasProps = {
3018
mediaItem: DatasetItem;
3119
isFocussed: boolean;
3220
};
3321
export const AnnotatorCanvas = ({ mediaItem, isFocussed }: AnnotatorCanvasProps) => {
34-
const { setSelectedAnnotations } = useSelectedAnnotations();
3522
const project_id = useProjectIdentifier();
23+
const { setSelectedAnnotations } = useSelectedAnnotations();
24+
3625
const size = { width: mediaItem.width, height: mediaItem.height };
37-
// todo: pass media annotations
26+
// TODO: pass media annotations
27+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3828
const annotations: Annotation[] = [];
3929

40-
const handleClickOutside = (e: MouseEvent<SVGSVGElement>): void => {
30+
const handleClickOutside = (e: PointerEvent<HTMLDivElement>): void => {
4131
if (e.target === e.currentTarget) {
4232
setSelectedAnnotations(new Set());
4333
}
@@ -51,20 +41,14 @@ export const AnnotatorCanvas = ({ mediaItem, isFocussed }: AnnotatorCanvasProps)
5141
<img src={getImageUrl(project_id, String(mediaItem.id))} alt='Collected data' />
5242
</View>
5343

54-
{!isEmpty(annotations) && (
55-
<View gridArea={'innercanvas'}>
56-
<svg
57-
width={size.width}
58-
height={size.height}
59-
style={DEFAULT_ANNOTATION_STYLES}
60-
onClick={handleClickOutside}
61-
>
44+
<View gridArea={'innercanvas'}>
45+
<>
46+
<div onPointerDown={handleClickOutside}>
6247
<Annotations width={size.width} height={size.height} isFocussed={isFocussed} />
63-
64-
<ToolManager />
65-
</svg>
66-
</View>
67-
)}
48+
</div>
49+
<ToolManager />
50+
</>
51+
</View>
6852
</Grid>
6953
</ZoomTransform>
7054
</ZoomProvider>

ui/src/features/annotator/annotator-provider.component.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,23 @@ import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useStat
66
import { v4 as uuid } from 'uuid';
77

88
import { ToolType } from '../../components/tool-selection-bar/tools/interface';
9-
import { Annotation, DatasetItem, Shape } from './types';
9+
import { useLoadImageQuery } from './hooks/use-load-image-query.hook';
10+
import { Annotation, DatasetItem, RegionOfInterest, Shape } from './types';
1011

1112
type AnnotatorContext = {
13+
// Tools
1214
activeTool: ToolType | null;
1315
setActiveTool: Dispatch<SetStateAction<ToolType>>;
1416

17+
// Annotations
18+
annotations: Annotation[];
1519
addAnnotation: (shape: Shape) => void;
1620
updateAnnotation: (updatedAnnotation: Annotation) => void;
1721

22+
// Media item
1823
mediaItem: DatasetItem;
19-
annotations: Annotation[];
24+
image: ImageData;
25+
roi: RegionOfInterest;
2026
};
2127

2228
export const AnnotatorProviderContext = createContext<AnnotatorContext | null>(null);
@@ -26,6 +32,8 @@ export const AnnotatorProvider = ({ mediaItem, children }: { mediaItem: DatasetI
2632
// todo: pass media annotations
2733
const [annotations, setAnnotations] = useState<Annotation[]>([]);
2834

35+
const imageQuery = useLoadImageQuery(mediaItem);
36+
2937
const updateAnnotation = (updatedAnnotation: Annotation) => {
3038
const { id } = updatedAnnotation;
3139

@@ -57,6 +65,8 @@ export const AnnotatorProvider = ({ mediaItem, children }: { mediaItem: DatasetI
5765
annotations,
5866

5967
mediaItem,
68+
image: imageQuery.data,
69+
roi: { x: 0, y: 0, width: mediaItem.width, height: mediaItem.height },
6070
}}
6171
>
6272
{children}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query';
5+
6+
import { API_BASE_URL } from '../../../api/client';
7+
import { useProjectIdentifier } from '../../../hooks/use-project-identifier.hook';
8+
import { getImageData, loadImage } from '../tools/utils';
9+
import { DatasetItem } from '../types';
10+
11+
export const useLoadImageQuery = (mediaItem: DatasetItem | undefined): UseSuspenseQueryResult<ImageData, unknown> => {
12+
const projectId = useProjectIdentifier();
13+
14+
return useSuspenseQuery({
15+
queryKey: ['mediaItem', mediaItem?.id, projectId],
16+
queryFn: async () => {
17+
if (mediaItem === undefined) {
18+
throw new Error("Can't fetch undefined media item");
19+
}
20+
21+
const imageUrl = `${API_BASE_URL}/api/projects/${projectId}/dataset/items/${mediaItem.id}/binary`;
22+
const image = await loadImage(imageUrl);
23+
24+
return getImageData(image);
25+
},
26+
// The image of a media item never changes so we don't want to refetch stale data
27+
staleTime: Infinity,
28+
retry: 0,
29+
});
30+
};

ui/src/features/annotator/hooks/use-segment-anything.hook.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Flex, Heading, View } from '@geti/ui';
5+
6+
import IntelBrandedLoadingGif from '../../assets/intel-loading.webp';
7+
8+
export const AnnotatorLoading = ({ isLoading }: { isLoading: boolean }) => {
9+
return (
10+
<View
11+
position={'absolute'}
12+
left={0}
13+
top={0}
14+
right={0}
15+
bottom={0}
16+
UNSAFE_style={{
17+
backgroundColor: 'var(--spectrum-alias-background-color-modal-overlay)',
18+
zIndex: 10,
19+
}}
20+
>
21+
<Flex direction={'column'} alignItems={'center'} justifyContent={'center'} height='100%' gap='size-100'>
22+
{/* eslint-disable-next-line jsx-a11y/img-redundant-alt */}
23+
<img
24+
src={IntelBrandedLoadingGif}
25+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
26+
role='progressbar'
27+
alt='Processing image'
28+
style={{
29+
width: 300,
30+
height: 300,
31+
}}
32+
/>
33+
<Heading
34+
level={1}
35+
UNSAFE_style={{
36+
textShadow: '1px 1px 2px black, 1px 1px 2px white',
37+
}}
38+
>
39+
{isLoading && 'Processing image, please wait...'}
40+
</Heading>
41+
</Flex>
42+
</View>
43+
);
44+
};

ui/src/features/annotator/tools/annotator-tools.component.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { Divider } from '@geti/ui';
5-
import { BoundingBox, Selector } from '@geti/ui/icons';
5+
import { BoundingBox, SegmentAnythingIcon, Selector } from '@geti/ui/icons';
66

77
import { ToolConfig } from '../../../components/tool-selection-bar/tools/interface';
88
import { Tools } from '../../../components/tool-selection-bar/tools/tools.component';
@@ -17,7 +17,8 @@ const TASK_TOOL_CONFIG: Record<string, ToolConfig[]> = {
1717
],
1818
segmentation: [
1919
{ type: 'selection', icon: Selector },
20-
// TODO: Add 'polygon' and 'sam' tools later
20+
{ type: 'sam', icon: SegmentAnythingIcon },
21+
// TODO: Add 'polygon' tool later
2122
],
2223
};
2324

ui/src/features/annotator/tools/bounding-box-tool/bounding-box-tool.component.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { useAnnotator } from '../../annotator-provider.component';
66
import { DrawingBox } from '../drawing-box-tool/drawing-box.component';
77

88
export const BoundingBoxTool = () => {
9-
const { mediaItem, addAnnotation } = useAnnotator();
9+
const { mediaItem, addAnnotation, image } = useAnnotator();
1010
const { scale: zoom } = useZoom();
1111

1212
return (
1313
<DrawingBox
1414
roi={{ x: 0, y: 0, width: mediaItem.width, height: mediaItem.height }}
15-
image={new ImageData(mediaItem.width, mediaItem.height)}
15+
image={image}
1616
zoom={zoom}
1717
onComplete={addAnnotation}
1818
/>

0 commit comments

Comments
 (0)