lost-sia 3.2.0-alpha1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Canvas/Canvas.js +1 -1
- package/dist/Canvas/LabelInput.js +1 -1
- package/dist/Sia.js +1 -1
- package/dist/Toolbar/ToolbarItems/ImageToolItems/ImageLabelInput.d.ts +2 -1
- package/dist/Toolbar/ToolbarItems/ImageToolItems/ImageLabelInput.js +1 -1
- package/dist/Toolbar/ToolbarItems/ImageToolItems/TagLabel.d.ts +2 -1
- package/dist/Toolbar/ToolbarItems/ImageToolItems/TagLabel.js +1 -1
- package/dist/Toolbar/ToolbarItems/ImageTools.js +1 -1
- package/dist/assets/SIA-CoOqcT1a.css +1 -0
- package/dist/assets/brand-icons-Cu_C0hZ4.svg +1008 -0
- package/dist/assets/brand-icons-F3SPCeH1.woff +0 -0
- package/dist/assets/brand-icons-XL9sxUpA.woff2 +0 -0
- package/dist/assets/brand-icons-sqJ2Pg7a.eot +0 -0
- package/dist/assets/brand-icons-ubhWoxly.ttf +0 -0
- package/dist/assets/flags-DOLqOU7Y.png +0 -0
- package/dist/assets/icons-BOCtAERH.woff +0 -0
- package/dist/assets/icons-CHzK1VD9.eot +0 -0
- package/dist/assets/icons-D29ZQHHw.ttf +0 -0
- package/dist/assets/icons-Du6TOHnR.woff2 +0 -0
- package/dist/assets/icons-RwhydX30.svg +1518 -0
- package/dist/assets/node_modules/semantic-ui-css/semantic.min-09YPtVE6.css +1 -0
- package/dist/assets/outline-icons-BfdLr8tr.svg +366 -0
- package/dist/assets/outline-icons-DD8jm0uy.ttf +0 -0
- package/dist/assets/outline-icons-DInHoiqI.woff2 +0 -0
- package/dist/assets/outline-icons-LX8adJ4n.eot +0 -0
- package/dist/assets/outline-icons-aQ88nltS.woff +0 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -1
- package/dist/stories/Canvas/Canvas.stories.d.ts +3 -3
- package/dist/stories/Canvas/CanvasWithOffset.stories.d.ts +6 -6
- package/dist/stories/Toolbar/ImageTools/TagLabel.stories.d.ts +2 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.js +1 -1
- package/package.json +4 -3
- package/src/AnnoExampleViewer.jsx +69 -0
- package/src/Canvas/Canvas.tsx +45 -48
- package/src/Canvas/LabelInput.tsx +11 -9
- package/src/InfoBoxes/AnnoDetails.jsx +165 -0
- package/src/InfoBoxes/AnnoStats.jsx +106 -0
- package/src/InfoBoxes/InfoBox.jsx +76 -0
- package/src/InfoBoxes/InfoBoxArea.jsx +152 -0
- package/src/InfoBoxes/LabelInfo.jsx +107 -0
- package/src/SIA.scss +13 -0
- package/src/SIASettingButton.jsx +126 -0
- package/src/Sia.tsx +70 -54
- package/src/Toolbar/ToolbarItems/ImageToolItems/ImageLabelInput.tsx +74 -24
- package/src/Toolbar/ToolbarItems/ImageToolItems/TagLabel.tsx +33 -0
- package/src/Toolbar/ToolbarItems/ImageTools.tsx +1 -0
- package/src/index.ts +1 -1
- package/src/stories/exampleData/exampleExternalAnnotations.ts +22 -22
- package/src/utils/index.ts +0 -1
- package/dist/SiaViewer.d.ts +0 -19
- package/dist/SiaViewer.js +0 -1
- package/dist/assets/SIA-BV1tYu3P.css +0 -1
- package/dist/stories/SIA/SiaViewer.stories.d.ts +0 -52
- package/src/SiaViewer.tsx +0 -125
- package/src/stories/SIA/SiaViewer.stories.tsx +0 -105
|
@@ -2,9 +2,9 @@ import { StoryObj } from '@storybook/react';
|
|
|
2
2
|
import { default as AnnotationTool } from '../../models/AnnotationTool';
|
|
3
3
|
import { UiConfig } from '../../types';
|
|
4
4
|
export declare const ActionsData: {
|
|
5
|
-
onAnnoEvent:
|
|
6
|
-
onKeyDown:
|
|
7
|
-
onKeyUp:
|
|
5
|
+
onAnnoEvent: any;
|
|
6
|
+
onKeyDown: any;
|
|
7
|
+
onKeyUp: any;
|
|
8
8
|
};
|
|
9
9
|
declare const meta: {
|
|
10
10
|
title: string;
|
|
@@ -22,9 +22,9 @@ declare const meta: {
|
|
|
22
22
|
tags: string[];
|
|
23
23
|
excludeStories: RegExp;
|
|
24
24
|
args: {
|
|
25
|
-
onAnnoEvent:
|
|
26
|
-
onKeyDown:
|
|
27
|
-
onKeyUp:
|
|
25
|
+
onAnnoEvent: any;
|
|
26
|
+
onKeyDown: any;
|
|
27
|
+
onKeyUp: any;
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
30
|
export default meta;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { StoryObj } from '@storybook/react';
|
|
2
2
|
declare const meta: {
|
|
3
3
|
title: string;
|
|
4
|
-
component: ({ name, color, size: bodySize, style, triangleSize, onClick, }: {
|
|
4
|
+
component: ({ name, color, size: bodySize, style, triangleSize, onClick, onRemove, }: {
|
|
5
5
|
name: string;
|
|
6
6
|
color?: string;
|
|
7
7
|
size?: number;
|
|
8
8
|
style?: import('react').CSSProperties;
|
|
9
9
|
triangleSize?: number;
|
|
10
10
|
onClick?: () => void;
|
|
11
|
+
onRemove?: () => void;
|
|
11
12
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
12
13
|
argTypes: {};
|
|
13
14
|
parameters: {
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{default as
|
|
1
|
+
import{default as e}from"./TimeUtils.js";import{SIA_INITIAL_UI_CONFIG as i,uiConfig as r}from"./uiConfig.js";export{i as SIA_INITIAL_UI_CONFIG,e as TimeUtils,r as uiConfig};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lost-sia",
|
|
3
|
-
"version": "3.2.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Single Image Annotation Tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "l3p-cv/lost-sia",
|
|
@@ -63,7 +63,9 @@
|
|
|
63
63
|
"react": "^19.2.1",
|
|
64
64
|
"react-dom": "^19.2.1",
|
|
65
65
|
"react-draggable": "^4.5.0",
|
|
66
|
-
"sass": "^1.95.0"
|
|
66
|
+
"sass": "^1.95.0",
|
|
67
|
+
"semantic-ui-css": "2.5.0",
|
|
68
|
+
"semantic-ui-react": "^2.0.3"
|
|
67
69
|
},
|
|
68
70
|
"peerDependencies": {
|
|
69
71
|
"@coreui/react": "^5.9.1",
|
|
@@ -79,7 +81,6 @@
|
|
|
79
81
|
"@storybook/addon-links": "^10.1.6",
|
|
80
82
|
"@storybook/react": "^10.1.6",
|
|
81
83
|
"@storybook/react-vite": "^10.1.6",
|
|
82
|
-
"@storybook/test": "^8.6.15",
|
|
83
84
|
"@types/react": "^19.2.7",
|
|
84
85
|
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
|
85
86
|
"@typescript-eslint/parser": "^8.46.1",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import Prompt from './Prompt'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { Card, Image } from 'semantic-ui-react'
|
|
4
|
+
|
|
5
|
+
const LabelExampleViewer = (props) => {
|
|
6
|
+
const requestExample = (e) => {
|
|
7
|
+
e.stopPropagation()
|
|
8
|
+
if (props.onRequestExample) {
|
|
9
|
+
props.onRequestExample()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const renderContent = () => {
|
|
13
|
+
if (!props.lbl) return null
|
|
14
|
+
if (!props.exampleImg) return null
|
|
15
|
+
const description = (
|
|
16
|
+
<div>
|
|
17
|
+
{props.lbl.description}
|
|
18
|
+
<h4>Comment:</h4>
|
|
19
|
+
{props.exampleImg.anno ? props.exampleImg.anno.comment : 'no comment'}
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
<Card
|
|
25
|
+
|
|
26
|
+
// image={props.exampleImg.img}
|
|
27
|
+
// description={props.lbl.description}
|
|
28
|
+
// header={props.lbl.label}
|
|
29
|
+
// // description={props.exampleImg.anno.comment}
|
|
30
|
+
// extra={props.exampleImg.anno.comment}
|
|
31
|
+
>
|
|
32
|
+
<Image
|
|
33
|
+
onClick={(e) => requestExample(e)}
|
|
34
|
+
src={props.exampleImg.img}
|
|
35
|
+
wrapped
|
|
36
|
+
ui={false}
|
|
37
|
+
/>
|
|
38
|
+
<Card.Content>
|
|
39
|
+
<Card.Header>{props.lbl.label}</Card.Header>
|
|
40
|
+
{/* <Card.Description>{props.lbl.description}</Card.Description> */}
|
|
41
|
+
<Card.Description>{description}</Card.Description>
|
|
42
|
+
{/* <Card.Description>{props.exampleImg.anno.comment}</Card.Description> */}
|
|
43
|
+
</Card.Content>
|
|
44
|
+
</Card>
|
|
45
|
+
{/* <Header inverted>{props.lbl.label}</Header>
|
|
46
|
+
{props.lbl.description}
|
|
47
|
+
<Image src={props.exampleImg.img} centered size='medium'></Image> */}
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handlePromptClick = () => {
|
|
53
|
+
if (props.onClose) {
|
|
54
|
+
props.onClose()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Prompt
|
|
60
|
+
onClick={() => {
|
|
61
|
+
handlePromptClick()
|
|
62
|
+
}}
|
|
63
|
+
active={props.active}
|
|
64
|
+
content={renderContent()}
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default LabelExampleViewer
|
package/src/Canvas/Canvas.tsx
CHANGED
|
@@ -133,8 +133,12 @@ const Canvas = ({
|
|
|
133
133
|
y: svgTranslation.y,
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
//
|
|
137
|
-
|
|
136
|
+
// stage coordinates of the annotation whose label popup is open.
|
|
137
|
+
// stored as Point[] so the pixel position can be recomputed on every render
|
|
138
|
+
// (zoom changes svgScale/svgTranslation, so we need a live recalculation each frame)
|
|
139
|
+
const [labelInputPosition, setLabelInputPosition] = useState<Point[] | undefined>(
|
|
140
|
+
undefined,
|
|
141
|
+
)
|
|
138
142
|
const [isLabelInputVisible, setIsLabelInputVisible] = useState<boolean>(false)
|
|
139
143
|
|
|
140
144
|
// svg element - used to trap focus
|
|
@@ -347,10 +351,31 @@ const Canvas = ({
|
|
|
347
351
|
topLeftPoint,
|
|
348
352
|
pageToStageOffset,
|
|
349
353
|
svgScale,
|
|
350
|
-
|
|
354
|
+
centeredSvgTranslation,
|
|
351
355
|
)
|
|
352
356
|
}
|
|
353
357
|
|
|
358
|
+
// Estimated max dimensions of the LabelInput dropdown (filter bar + ~8 label items).
|
|
359
|
+
// Used to clamp the popup anchor so it never escapes the canvas bounds.
|
|
360
|
+
const LABEL_POPUP_WIDTH = 250
|
|
361
|
+
const LABEL_POPUP_HEIGHT = 350
|
|
362
|
+
|
|
363
|
+
/** Clamps a viewport-space popup position so the dropdown stays inside the visible canvas area.
|
|
364
|
+
* Uses the canvas bounding rect so the popup is always reachable even when the annotation
|
|
365
|
+
* is outside the visible viewport (e.g. after zooming in while drawing).
|
|
366
|
+
*/
|
|
367
|
+
const clampToViewport = (pos: Point): Point => {
|
|
368
|
+
const canvasRect = canvasRef.current?.getBoundingClientRect()
|
|
369
|
+
const minX = canvasRect?.left ?? 0
|
|
370
|
+
const minY = canvasRect?.top ?? 0
|
|
371
|
+
const maxX = (canvasRect?.right ?? window.innerWidth) - LABEL_POPUP_WIDTH
|
|
372
|
+
const maxY = (canvasRect?.bottom ?? window.innerHeight) - LABEL_POPUP_HEIGHT
|
|
373
|
+
return {
|
|
374
|
+
x: Math.min(Math.max(pos.x, minX), maxX),
|
|
375
|
+
y: Math.min(Math.max(pos.y, minY), maxY),
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
354
379
|
const handleKeyAction = (keyAction: KeyAction) => {
|
|
355
380
|
switch (keyAction) {
|
|
356
381
|
case KeyAction.EDIT_LABEL:
|
|
@@ -361,7 +386,7 @@ const Canvas = ({
|
|
|
361
386
|
imgSize,
|
|
362
387
|
stageSize,
|
|
363
388
|
)
|
|
364
|
-
setLabelInputPosition(
|
|
389
|
+
setLabelInputPosition(stageCoords)
|
|
365
390
|
setIsLabelInputVisible(true)
|
|
366
391
|
}
|
|
367
392
|
break
|
|
@@ -572,37 +597,6 @@ const Canvas = ({
|
|
|
572
597
|
setImgSize({ x: width, y: height })
|
|
573
598
|
}, [image, canvasSize])
|
|
574
599
|
|
|
575
|
-
// Source of truth for the image's natural size.
|
|
576
|
-
// The SVG <image> element above loads asynchronously, so reading
|
|
577
|
-
// imageRef.getBoundingClientRect() (the effect above) runs before the
|
|
578
|
-
// remote image has decoded and resolves to {0, 0}. That left imgSize at
|
|
579
|
-
// 0, which trips the getFittedImageScale guard (returns 0), so stageSize
|
|
580
|
-
// never got set and the <image> rendered at its natural (huge) pixel size.
|
|
581
|
-
// Preloading via an HTMLImageElement gives us naturalWidth/Height as soon
|
|
582
|
-
// as the bytes are decoded, regardless of the SVG <image> lifecycle.
|
|
583
|
-
useEffect(() => {
|
|
584
|
-
if (!image) {
|
|
585
|
-
setImgSize({ x: -1, y: -1 })
|
|
586
|
-
return
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
let cancelled = false
|
|
590
|
-
const probe = new Image()
|
|
591
|
-
probe.onload = () => {
|
|
592
|
-
if (cancelled) return
|
|
593
|
-
const { naturalWidth, naturalHeight } = probe
|
|
594
|
-
if (naturalWidth > 0 && naturalHeight > 0) {
|
|
595
|
-
setImgSize({ x: naturalWidth, y: naturalHeight })
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
probe.src = image
|
|
599
|
-
|
|
600
|
-
return () => {
|
|
601
|
-
cancelled = true
|
|
602
|
-
probe.onload = null
|
|
603
|
-
}
|
|
604
|
-
}, [image])
|
|
605
|
-
|
|
606
600
|
useEffect(() => {
|
|
607
601
|
if (imageToStageFactor === 0) return
|
|
608
602
|
|
|
@@ -810,10 +804,8 @@ const Canvas = ({
|
|
|
810
804
|
|
|
811
805
|
onSelectAnnotation(percentagedAnnotation)
|
|
812
806
|
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
setLabelInputPosition(getAnnoTopLeftPagePosition(annotation.coordinates))
|
|
816
|
-
}
|
|
807
|
+
// store stage coords so popup position is recomputed live on every render
|
|
808
|
+
setLabelInputPosition(annotation.coordinates)
|
|
817
809
|
}
|
|
818
810
|
|
|
819
811
|
const handleOnAnnoChanged = (annotation: Annotation) => {
|
|
@@ -952,15 +944,21 @@ const Canvas = ({
|
|
|
952
944
|
flexDirection: 'column',
|
|
953
945
|
}}
|
|
954
946
|
>
|
|
955
|
-
{annotationSettings.canLabel && (
|
|
956
947
|
<div
|
|
957
|
-
style={{
|
|
958
|
-
position
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
948
|
+
style={(() => {
|
|
949
|
+
// recompute pixel position on every render so the popup tracks the annotation
|
|
950
|
+
const pagePos = labelInputPosition
|
|
951
|
+
? clampToViewport(getAnnoTopLeftPagePosition(labelInputPosition))
|
|
952
|
+
: undefined
|
|
953
|
+
return {
|
|
954
|
+
// position: fixed resolves left/top against the viewport.
|
|
955
|
+
position: 'fixed' as const,
|
|
956
|
+
left: pagePos ? pagePos.x - window.scrollX : 0,
|
|
957
|
+
top: pagePos ? pagePos.y - window.scrollY : 0,
|
|
958
|
+
display: pagePos === undefined ? 'none' : 'inherit',
|
|
959
|
+
zIndex: isLabelInputVisible ? 7000 : -1,
|
|
960
|
+
}
|
|
961
|
+
})()}
|
|
964
962
|
>
|
|
965
963
|
<LabelInput
|
|
966
964
|
defaultLabelId={defaultLabelId}
|
|
@@ -1007,7 +1005,6 @@ const Canvas = ({
|
|
|
1007
1005
|
}}
|
|
1008
1006
|
/>
|
|
1009
1007
|
</div>
|
|
1010
|
-
)}
|
|
1011
1008
|
|
|
1012
1009
|
{isImageJunk && (
|
|
1013
1010
|
<div
|
|
@@ -79,15 +79,17 @@ const LabelInput = ({
|
|
|
79
79
|
/>
|
|
80
80
|
</div>
|
|
81
81
|
<CDropdownDivider />
|
|
82
|
-
{
|
|
83
|
-
filteredLabels.
|
|
84
|
-
|
|
85
|
-
{label.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
|
83
|
+
{filteredLabels.length > 0 ? (
|
|
84
|
+
filteredLabels.map((label: Label) => (
|
|
85
|
+
<CDropdownItem key={label.id} onClick={() => updateSelectedLabels(label)}>
|
|
86
|
+
{label.name}
|
|
87
|
+
</CDropdownItem>
|
|
88
|
+
))
|
|
89
|
+
) : (
|
|
90
|
+
<CDropdownItem disabled>No results</CDropdownItem>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
91
93
|
</CDropdownMenu>
|
|
92
94
|
</CDropdown>
|
|
93
95
|
</>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { List, TextArea, Form, Label, Icon } from 'semantic-ui-react'
|
|
3
|
+
import InfoBox from './InfoBox'
|
|
4
|
+
import PopUp from '../SiaPopup'
|
|
5
|
+
|
|
6
|
+
const AnnoDetails = (props) => {
|
|
7
|
+
const [comment, setComment] = useState('')
|
|
8
|
+
const [example, setExample] = useState(false)
|
|
9
|
+
const [showSaveBtn, setShowSaveBtn] = useState(false)
|
|
10
|
+
const tARef = useRef()
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (props.anno) {
|
|
13
|
+
if (props.anno.comment) {
|
|
14
|
+
setComment(props.anno.comment)
|
|
15
|
+
} else {
|
|
16
|
+
setComment('')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}, [props.anno])
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (tARef.current) {
|
|
23
|
+
tARef.current.focus()
|
|
24
|
+
}
|
|
25
|
+
}, [props.commentInputTrigger])
|
|
26
|
+
|
|
27
|
+
const onDismiss = () => {
|
|
28
|
+
if (props.onDismiss) {
|
|
29
|
+
props.onDismiss()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const onCommentUpdate = () => {
|
|
34
|
+
if (props.onCommentUpdate) {
|
|
35
|
+
props.onCommentUpdate(comment)
|
|
36
|
+
}
|
|
37
|
+
setShowSaveBtn(false)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const onMarkExampleClick = () => {
|
|
41
|
+
// setExample(!example)
|
|
42
|
+
if (props.onMarkExample) {
|
|
43
|
+
props.onMarkExample(props.anno)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const renderSaveBtn = () => {
|
|
48
|
+
if (showSaveBtn) {
|
|
49
|
+
return <Label as="a" corner="right" icon="save" color="red"></Label>
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const renderComment = () => {
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
<Form onClick={() => {}}>
|
|
57
|
+
{renderSaveBtn()}
|
|
58
|
+
<TextArea
|
|
59
|
+
placeholder="Write a comment"
|
|
60
|
+
ref={tARef}
|
|
61
|
+
value={comment}
|
|
62
|
+
rows={2}
|
|
63
|
+
onBlur={() => onCommentUpdate()}
|
|
64
|
+
onFocus={() => setShowSaveBtn(true)}
|
|
65
|
+
onChange={(e) => setComment(e.target.value)}
|
|
66
|
+
></TextArea>
|
|
67
|
+
</Form>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
const renderLabels = () => {
|
|
72
|
+
if (props.anno) {
|
|
73
|
+
const selectedLabelIds = props.anno.labelIds
|
|
74
|
+
if (!selectedLabelIds) return 'No Label'
|
|
75
|
+
|
|
76
|
+
let lbls = ''
|
|
77
|
+
props.anno.labelIds.forEach((lbl, idx) => {
|
|
78
|
+
const labelObject = props.possibleLabels.find((el) => {
|
|
79
|
+
return el.id === lbl
|
|
80
|
+
})
|
|
81
|
+
if (idx > 0) lbls += ', '
|
|
82
|
+
lbls += labelObject.label
|
|
83
|
+
})
|
|
84
|
+
if (!lbls) return 'No Label'
|
|
85
|
+
return lbls
|
|
86
|
+
} else {
|
|
87
|
+
return 'No Label'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const renderExampleMark = () => {
|
|
92
|
+
if (!props.allowedToMarkExample) return null
|
|
93
|
+
let color = 'grey'
|
|
94
|
+
let iconName = 'bookmark outline'
|
|
95
|
+
if (props.anno) {
|
|
96
|
+
if (props.anno.isExample) {
|
|
97
|
+
color = 'yellow'
|
|
98
|
+
iconName = 'bookmark'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const mark = (
|
|
102
|
+
<Label
|
|
103
|
+
as="a"
|
|
104
|
+
color={color}
|
|
105
|
+
style={{ opacity: 0.8 }}
|
|
106
|
+
size="medium"
|
|
107
|
+
corner="left"
|
|
108
|
+
onClick={() => {
|
|
109
|
+
onMarkExampleClick()
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<Icon name={iconName} />
|
|
113
|
+
</Label>
|
|
114
|
+
)
|
|
115
|
+
return (
|
|
116
|
+
<PopUp
|
|
117
|
+
content="Mark this annotation as example for other annotators"
|
|
118
|
+
trigger={mark}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const renderDescription = () => {
|
|
124
|
+
if (props.anno) {
|
|
125
|
+
return (
|
|
126
|
+
<div>
|
|
127
|
+
{renderExampleMark()}
|
|
128
|
+
{/* <Header>
|
|
129
|
+
Labels
|
|
130
|
+
</Header>
|
|
131
|
+
<div>
|
|
132
|
+
{renderLabels()}
|
|
133
|
+
</div> */}
|
|
134
|
+
<List>
|
|
135
|
+
<List.Item icon="at" content={props.anno.id} />
|
|
136
|
+
<List.Item icon="tag" content={renderLabels()} />
|
|
137
|
+
<List.Item
|
|
138
|
+
icon="time"
|
|
139
|
+
content={`${props.anno.annoTime.toLocaleString('us', {
|
|
140
|
+
minimumFractionDigits: 2,
|
|
141
|
+
maximumFractionDigits: 2,
|
|
142
|
+
})} seconds`}
|
|
143
|
+
/>
|
|
144
|
+
{/* <List.Item icon='time' content={props.anno.annoTime} /> */}
|
|
145
|
+
</List>
|
|
146
|
+
{renderComment()}
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
} else {
|
|
150
|
+
return 'No annotation selected!'
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<InfoBox
|
|
156
|
+
header={'Annotation Details'}
|
|
157
|
+
content={renderDescription()}
|
|
158
|
+
visible={props.visible}
|
|
159
|
+
defaultPos={props.defaultPos}
|
|
160
|
+
onDismiss={(e) => onDismiss()}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export default AnnoDetails
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { List, Label } from 'semantic-ui-react'
|
|
3
|
+
import InfoBox from './InfoBox'
|
|
4
|
+
import * as colorlut from '../utils/color'
|
|
5
|
+
// import actions from '../../../../actions'
|
|
6
|
+
// import * as transform from '../utils/transform'
|
|
7
|
+
// const { siaShowImgBar } = actions
|
|
8
|
+
|
|
9
|
+
const AnnoStats = (props) => {
|
|
10
|
+
const [hideList, setHideList] = useState([])
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setHideList([])
|
|
14
|
+
}, [props.imgLoadCount])
|
|
15
|
+
|
|
16
|
+
const onDismiss = () => {
|
|
17
|
+
if (props.onDismiss) {
|
|
18
|
+
props.onDismiss()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const onLblClick = (lbl) => {
|
|
23
|
+
let hideLbl = false
|
|
24
|
+
if (hideList.includes(lbl.id)) {
|
|
25
|
+
setHideList(hideList.filter((e) => e !== lbl.id))
|
|
26
|
+
hideLbl = false
|
|
27
|
+
} else {
|
|
28
|
+
// hideList.push(lbl.id)
|
|
29
|
+
setHideList([...hideList, lbl.id])
|
|
30
|
+
hideLbl = true
|
|
31
|
+
}
|
|
32
|
+
if (props.onHideLbl) {
|
|
33
|
+
props.onHideLbl(lbl, hideLbl)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const renderRow = (s) => {
|
|
38
|
+
const opacity = hideList.includes(s.id) ? 0.5 : 1.0
|
|
39
|
+
return (
|
|
40
|
+
<List.Item key={s.id}>
|
|
41
|
+
<List.Content>
|
|
42
|
+
<Label
|
|
43
|
+
as="a"
|
|
44
|
+
tag
|
|
45
|
+
style={{ background: s.color, opacity: opacity }}
|
|
46
|
+
onClick={() => onLblClick(s)}
|
|
47
|
+
>
|
|
48
|
+
{s.label}
|
|
49
|
+
<Label.Detail>{s.amount}</Label.Detail>
|
|
50
|
+
</Label>
|
|
51
|
+
</List.Content>
|
|
52
|
+
</List.Item>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const renderRows = () => {
|
|
57
|
+
let stats = {}
|
|
58
|
+
props.annos.forEach((a) => {
|
|
59
|
+
// // console.log('render rows', a)
|
|
60
|
+
if (a.status !== 'deleted') {
|
|
61
|
+
a.labelIds.forEach((lblId) => {
|
|
62
|
+
if (lblId in stats) {
|
|
63
|
+
stats[lblId] += 1
|
|
64
|
+
} else {
|
|
65
|
+
stats[lblId] = 1
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
if (a.labelIds.length === 0) {
|
|
69
|
+
if ((-1) in stats) {
|
|
70
|
+
stats[-1] += 1
|
|
71
|
+
} else {
|
|
72
|
+
stats[-1] = 1
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
const res = Object.entries(stats).map(([key, value]) => {
|
|
78
|
+
let lbl = props.possibleLabels.find((e) => {
|
|
79
|
+
return e.id === parseInt(key)
|
|
80
|
+
})
|
|
81
|
+
if (!lbl) {
|
|
82
|
+
lbl = { id: -1, label: 'No Label', color: colorlut.getDefaultColor() }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
lbl.amount = value
|
|
86
|
+
// return renderRow({class:idToLbl[key], amount:value, color:idToColor[key]})
|
|
87
|
+
return renderRow(lbl)
|
|
88
|
+
})
|
|
89
|
+
return res
|
|
90
|
+
}
|
|
91
|
+
const renderDescription = () => {
|
|
92
|
+
return <List>{renderRows()}</List>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<InfoBox
|
|
97
|
+
header={'Annotations per Label'}
|
|
98
|
+
content={renderDescription()}
|
|
99
|
+
visible={props.visible}
|
|
100
|
+
defaultPos={props.defaultPos}
|
|
101
|
+
onDismiss={(e) => onDismiss()}
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default AnnoStats
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { Component } from 'react'
|
|
2
|
+
import { Header, Message } from 'semantic-ui-react'
|
|
3
|
+
import Draggable from 'react-draggable'
|
|
4
|
+
|
|
5
|
+
class InfoBox extends Component {
|
|
6
|
+
constructor(props) {
|
|
7
|
+
super(props)
|
|
8
|
+
this.state = {
|
|
9
|
+
style: {
|
|
10
|
+
position: 'fixed',
|
|
11
|
+
top: 200,
|
|
12
|
+
left: 200,
|
|
13
|
+
width: 250,
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
componentDidMount() {
|
|
19
|
+
this.updateStyle()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
componentDidUpdate(prevProps) {
|
|
23
|
+
if (this.props.defaultPos !== prevProps.defaultPos) {
|
|
24
|
+
this.updateStyle()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
handleOnStop(e) {
|
|
29
|
+
if (this.props.onStop) {
|
|
30
|
+
this.props.onStop(e)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
updateStyle() {
|
|
35
|
+
this.setState({
|
|
36
|
+
style: { ...this.state.style, ...this.props.defaultPos },
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onDismiss(e) {
|
|
41
|
+
if (this.props.onDismiss) {
|
|
42
|
+
this.props.onDismiss(e)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
render() {
|
|
47
|
+
if (!this.props.visible) return null
|
|
48
|
+
return (
|
|
49
|
+
<Draggable handle=".handle" onStop={(e) => this.handleOnStop(e)}>
|
|
50
|
+
<div style={this.state.style}>
|
|
51
|
+
<Message
|
|
52
|
+
style={{ opacity: 0.98 }}
|
|
53
|
+
onDismiss={(e) => {
|
|
54
|
+
this.onDismiss(e)
|
|
55
|
+
}}
|
|
56
|
+
size="small"
|
|
57
|
+
>
|
|
58
|
+
<Header
|
|
59
|
+
textAlign="center"
|
|
60
|
+
as="h5"
|
|
61
|
+
className="handle"
|
|
62
|
+
style={{ cursor: 'grab' }}
|
|
63
|
+
>
|
|
64
|
+
{this.props.header}
|
|
65
|
+
</Header>
|
|
66
|
+
{/* <Message.Header textAlign='center' className="handle" style={{cursor: 'grab'}}>{this.props.header}</Message.Header> */}
|
|
67
|
+
<hr />
|
|
68
|
+
<Message.Content>{this.props.content}</Message.Content>
|
|
69
|
+
</Message>
|
|
70
|
+
</div>
|
|
71
|
+
</Draggable>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default InfoBox
|