lost-sia 3.2.0-alpha0 → 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/package.json +4 -3
- package/src/AnnoExampleViewer.jsx +69 -0
- package/src/Canvas/Canvas.tsx +45 -17
- 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/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/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
|
|
@@ -779,10 +804,8 @@ const Canvas = ({
|
|
|
779
804
|
|
|
780
805
|
onSelectAnnotation(percentagedAnnotation)
|
|
781
806
|
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
setLabelInputPosition(getAnnoTopLeftPagePosition(annotation.coordinates))
|
|
785
|
-
}
|
|
807
|
+
// store stage coords so popup position is recomputed live on every render
|
|
808
|
+
setLabelInputPosition(annotation.coordinates)
|
|
786
809
|
}
|
|
787
810
|
|
|
788
811
|
const handleOnAnnoChanged = (annotation: Annotation) => {
|
|
@@ -921,15 +944,21 @@ const Canvas = ({
|
|
|
921
944
|
flexDirection: 'column',
|
|
922
945
|
}}
|
|
923
946
|
>
|
|
924
|
-
{annotationSettings.canLabel && (
|
|
925
947
|
<div
|
|
926
|
-
style={{
|
|
927
|
-
position
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
+
})()}
|
|
933
962
|
>
|
|
934
963
|
<LabelInput
|
|
935
964
|
defaultLabelId={defaultLabelId}
|
|
@@ -976,7 +1005,6 @@ const Canvas = ({
|
|
|
976
1005
|
}}
|
|
977
1006
|
/>
|
|
978
1007
|
</div>
|
|
979
|
-
)}
|
|
980
1008
|
|
|
981
1009
|
{isImageJunk && (
|
|
982
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
|