@searpent/react-image-annotate 2.0.1 → 2.0.4
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/package.json +4 -1
- package/src/Annotator/examplePhotos.js +7601 -0
- package/src/Annotator/index.js +83 -3
- package/src/Annotator/index.story.js +74 -25
- package/src/Annotator/reducers/general-reducer.js +99 -17
- package/src/Editor/annotation-plugin/annotation.css +46 -0
- package/src/Editor/annotation-plugin/annotation.js +515 -0
- package/src/Editor/index.js +24 -0
- package/src/Editor/tools.js +6 -0
- package/src/GroupSelectorSidebarBox/index.js +48 -0
- package/src/ImageCanvas/index.js +31 -25
- package/src/ImageCanvas/index.story.js +100 -0
- package/src/MainLayout/index.js +250 -160
- package/src/MainLayout/types.js +64 -56
- package/src/MetadataEditorSidebarBox/index.js +98 -0
- package/src/PageSelector/index.js +76 -0
- package/src/PageSelector/page-selector.css +69 -0
- package/src/RegionLabel/index.js +21 -0
- package/src/RegionShapes/index.js +34 -16
- package/src/RegionTags/index.js +7 -3
- package/src/SettingsProvider/index.js +7 -3
- package/src/hooks/use-colors.js +74 -0
- package/src/utils/filter-only-unique.js +5 -0
- package/src/utils/photosToImages.js +40 -0
- package/src/utils/regions-to-blocks.js +14 -0
package/src/MainLayout/types.js
CHANGED
|
@@ -23,6 +23,11 @@ export type ToolEnum =
|
|
|
23
23
|
| "create-expanding-line"
|
|
24
24
|
| "create-keypoints"
|
|
25
25
|
|
|
26
|
+
export type Metadata = {
|
|
27
|
+
key: string,
|
|
28
|
+
value: string
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
export type Image = {
|
|
27
32
|
src: string,
|
|
28
33
|
thumbnailSrc?: string,
|
|
@@ -31,6 +36,7 @@ export type Image = {
|
|
|
31
36
|
pixelSize?: { w: number, h: number },
|
|
32
37
|
realSize?: { w: number, h: number, unitName: string },
|
|
33
38
|
frameTime?: number,
|
|
39
|
+
metadata?: Array<Metadata>
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export type Mode =
|
|
@@ -38,79 +44,79 @@ export type Mode =
|
|
|
38
44
|
| {| mode: "DRAW_POLYGON", regionId: string |}
|
|
39
45
|
| {| mode: "MOVE_POLYGON_POINT", regionId: string, pointIndex: number |}
|
|
40
46
|
| {|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
mode: "RESIZE_BOX",
|
|
48
|
+
editLabelEditorAfter ?: boolean,
|
|
49
|
+
regionId: string,
|
|
44
50
|
freedom: [number, number],
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
original: { x: number, y: number, w: number, h: number },
|
|
52
|
+
isNew ?: boolean,
|
|
47
53
|
|}
|
|
48
54
|
| {| mode: "MOVE_REGION" |}
|
|
49
55
|
| {| mode: "MOVE_KEYPOINT", regionId: string, keypointId: string |}
|
|
50
56
|
| {|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
mode: "RESIZE_KEYPOINTS",
|
|
58
|
+
landmarks: {
|
|
59
|
+
[string]: KeypointDefinition,
|
|
54
60
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
centerX: number,
|
|
62
|
+
centerY: number,
|
|
63
|
+
regionId: string,
|
|
58
64
|
isNew: boolean,
|
|
59
65
|
|}
|
|
60
66
|
|
|
61
67
|
export type MainLayoutStateBase = {|
|
|
62
68
|
annotationType: "video" | "image",
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
69
|
+
mouseDownAt ?: ? { x: number, y: number },
|
|
70
|
+
fullScreen ?: boolean,
|
|
71
|
+
settingsOpen ?: boolean,
|
|
72
|
+
minRegionSize ?: number,
|
|
73
|
+
showTags: boolean,
|
|
74
|
+
showMask: boolean,
|
|
75
|
+
showPointDistances ?: boolean,
|
|
76
|
+
pointDistancePrecision ?: number,
|
|
77
|
+
selectedTool: ToolEnum,
|
|
78
|
+
selectedCls ?: string,
|
|
79
|
+
mode: Mode,
|
|
80
|
+
taskDescription: string,
|
|
81
|
+
allowedArea ?: { x: number, y: number, w: number, h: number },
|
|
82
|
+
regionClsList ?: Array < string >,
|
|
83
|
+
regionTagList ?: Array < string >,
|
|
84
|
+
imageClsList ?: Array < string >,
|
|
85
|
+
imageTagList ?: Array < string >,
|
|
86
|
+
enabledTools: Array < string >,
|
|
87
|
+
history: Array < { time: Date, state: MainLayoutState, name: string } >,
|
|
88
|
+
keypointDefinitions: KeypointsDefinition,
|
|
83
89
|
|}
|
|
84
90
|
|
|
85
91
|
export type MainLayoutImageAnnotationState = {|
|
|
86
92
|
...MainLayoutStateBase,
|
|
87
93
|
annotationType: "image",
|
|
88
94
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
selectedImage ?: string,
|
|
96
|
+
images: Array < Image >,
|
|
97
|
+
labelImages ?: boolean,
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
// If the selectedImage corresponds to a frame of a video
|
|
100
|
+
selectedImageFrameTime ?: number,
|
|
95
101
|
|}
|
|
96
102
|
|
|
97
103
|
export type MainLayoutVideoAnnotationState = {|
|
|
98
104
|
...MainLayoutStateBase,
|
|
99
105
|
annotationType: "video",
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
regions: Array<Region>,
|
|
107
|
+
videoSrc: string,
|
|
108
|
+
currentVideoTime: number,
|
|
109
|
+
videoName ?: string,
|
|
110
|
+
videoPlaying: boolean,
|
|
111
|
+
videoDuration ?: number,
|
|
112
|
+
keyframes: {
|
|
113
|
+
[time: number]: {|
|
|
114
|
+
time: number,
|
|
115
|
+
regions: Array < Region >,
|
|
110
116
|
|},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
realSize?: { w: number, h: number, unitName: string },
|
|
117
|
+
},
|
|
118
|
+
pixelSize ?: { w: number, h: number },
|
|
119
|
+
realSize ?: { w: number, h: number, unitName: string },
|
|
114
120
|
|}
|
|
115
121
|
|
|
116
122
|
export type MainLayoutState =
|
|
@@ -121,11 +127,11 @@ export type Action =
|
|
|
121
127
|
| {| type: "@@INIT" |}
|
|
122
128
|
| {| type: "SELECT_IMAGE", image: Image, imageIndex: number |}
|
|
123
129
|
| {|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
type: "IMAGE_OR_VIDEO_LOADED",
|
|
131
|
+
metadata: {
|
|
132
|
+
naturalWidth: number,
|
|
133
|
+
naturalHeight: number,
|
|
134
|
+
duration ?: number,
|
|
129
135
|
},
|
|
130
136
|
|}
|
|
131
137
|
| {| type: "CHANGE_REGION", region: Region |}
|
|
@@ -137,10 +143,10 @@ export type Action =
|
|
|
137
143
|
| {| type: "BEGIN_MOVE_POLYGON_POINT", polygon: Polygon, pointIndex: number |}
|
|
138
144
|
| {| type: "BEGIN_MOVE_KEYPOINT", region: Keypoints, keypointId: string |}
|
|
139
145
|
| {|
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
type: "ADD_POLYGON_POINT",
|
|
147
|
+
polygon: Polygon,
|
|
142
148
|
point: { x: number, y: number },
|
|
143
|
-
|
|
149
|
+
pointIndex: number,
|
|
144
150
|
|}
|
|
145
151
|
| {| type: "MOUSE_MOVE", x: number, y: number |}
|
|
146
152
|
| {| type: "MOUSE_DOWN", x: number, y: number |}
|
|
@@ -154,3 +160,5 @@ export type Action =
|
|
|
154
160
|
| {| type: "SELECT_TOOL", selectedTool: ToolEnum |}
|
|
155
161
|
| {| type: "CANCEL" |}
|
|
156
162
|
| {| type: "SELECT_CLASSIFICATION", cls: string |}
|
|
163
|
+
| {| type: "UPDATE_REGIONS", imageId: string, regions: Arrat < Region >}
|
|
164
|
+
| {| type: "IMAGES_CHANGED", updatedAt: Date |}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import React, { memo } from "react"
|
|
4
|
+
import SidebarBoxContainer from "../SidebarBoxContainer"
|
|
5
|
+
import DescriptionIcon from "@mui/icons-material/Description"
|
|
6
|
+
import { styled } from "@mui/material/styles"
|
|
7
|
+
import { createTheme, ThemeProvider } from "@mui/material/styles"
|
|
8
|
+
import { grey } from "@mui/material/colors"
|
|
9
|
+
|
|
10
|
+
type MetadataItemProps = {
|
|
11
|
+
name: string,
|
|
12
|
+
value: string,
|
|
13
|
+
imageIndex: number,
|
|
14
|
+
onChange: ({ name: string, value: String, imageIndex: number }) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MetadataItemDiv = styled("div")(({ theme }) => ({
|
|
18
|
+
display: "flex",
|
|
19
|
+
flexDirection: "column",
|
|
20
|
+
marginBottom: "1rem",
|
|
21
|
+
"& > label": {
|
|
22
|
+
fontSize: "1rem",
|
|
23
|
+
marginBottom: ".5rem",
|
|
24
|
+
textTransform: "capitalize"
|
|
25
|
+
}
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
const MetadataItem = ({ name, value, imageIndex, onChange }: MetadataItemProps) => {
|
|
29
|
+
const handleChange = e => {
|
|
30
|
+
e.preventDefault()
|
|
31
|
+
const { name, value } = e.target
|
|
32
|
+
onChange({
|
|
33
|
+
name,
|
|
34
|
+
value,
|
|
35
|
+
imageIndex
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<MetadataItemDiv>
|
|
41
|
+
<label for={name}>{name}</label>
|
|
42
|
+
<input type="text" value={value} name={name} onChange={handleChange} />
|
|
43
|
+
</MetadataItemDiv>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const MetadataList = ({ title, metadata, imageIndex, onMetadataChange }) => (
|
|
48
|
+
<div>
|
|
49
|
+
<h2>{title}</h2>
|
|
50
|
+
{
|
|
51
|
+
metadata.map(({ key, value }) => (
|
|
52
|
+
<MetadataItem name={key} value={value} imageIndex={imageIndex} onChange={onMetadataChange} />
|
|
53
|
+
))
|
|
54
|
+
}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const theme = createTheme()
|
|
59
|
+
const DivContainer = styled("div")(({ theme }) => ({
|
|
60
|
+
paddingLeft: 16,
|
|
61
|
+
paddingRight: 16,
|
|
62
|
+
fontSize: 12,
|
|
63
|
+
"& h1": { fontSize: 18 },
|
|
64
|
+
"& h2": { fontSize: 14 },
|
|
65
|
+
"& h3": { fontSize: 12 },
|
|
66
|
+
"& h4": { fontSize: 12 },
|
|
67
|
+
"& h5": { fontSize: 12 },
|
|
68
|
+
"& h6": { fontSize: 12 },
|
|
69
|
+
"& p": { fontSize: 12 },
|
|
70
|
+
"& a": {},
|
|
71
|
+
"& img": { width: "100%" },
|
|
72
|
+
}))
|
|
73
|
+
|
|
74
|
+
export const MetadataEditorSidebarBox = ({ state, onMetadataChange }) => {
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<ThemeProvider theme={theme}>
|
|
79
|
+
<SidebarBoxContainer
|
|
80
|
+
title="Metadata"
|
|
81
|
+
icon={<DescriptionIcon style={{ color: grey[700] }} />}
|
|
82
|
+
expandedByDefault={true}
|
|
83
|
+
>
|
|
84
|
+
<DivContainer>
|
|
85
|
+
<MetadataList title="Global" metadata={state.metadata} onMetadataChange={onMetadataChange} />
|
|
86
|
+
{
|
|
87
|
+
state?.images[state.selectedImage]?.metadata && (
|
|
88
|
+
<MetadataList title="Local" metadata={state.images[state.selectedImage].metadata} imageIndex={state.selectedImage} onMetadataChange={onMetadataChange} />
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
</DivContainer>
|
|
93
|
+
</SidebarBoxContainer>
|
|
94
|
+
</ThemeProvider>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default memo(MetadataEditorSidebarBox)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import classnames from "classnames"
|
|
4
|
+
require('./page-selector.css').toString();
|
|
5
|
+
|
|
6
|
+
function PageThumbnail({ src, isActive, onClick, pageNumber }) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
role="button"
|
|
10
|
+
tabIndex={0}
|
|
11
|
+
className={classnames('page-thumbnail', {
|
|
12
|
+
'page-thumbnail-is-active': isActive
|
|
13
|
+
})}
|
|
14
|
+
onClick={onClick}
|
|
15
|
+
>
|
|
16
|
+
<img src={src} alt="" />
|
|
17
|
+
{
|
|
18
|
+
(pageNumber !== undefined) && (
|
|
19
|
+
<div className="page-number-wrapper">
|
|
20
|
+
<span className="page-number">{pageNumber}</span>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
</div >
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function PagesSelector({ pages, onPageClick, onRecalc, onSave, recalcActive, saveActive }) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="page-selector">
|
|
32
|
+
<div className="bottom-buttons">
|
|
33
|
+
<button onClick={onRecalc} disabled={!recalcActive}>Recalc</button>
|
|
34
|
+
<button onClick={onSave} disabled={!saveActive}>Save</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="pages">
|
|
37
|
+
{pages.map((page, idx) => (
|
|
38
|
+
<PageThumbnail
|
|
39
|
+
key={page.id}
|
|
40
|
+
src={page.src}
|
|
41
|
+
isActive={page.isActive}
|
|
42
|
+
onClick={() => onPageClick(idx)}
|
|
43
|
+
/>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
PagesSelector.propTypes = {
|
|
51
|
+
pages: PropTypes.arrayOf(
|
|
52
|
+
PropTypes.shape({
|
|
53
|
+
id: PropTypes.string.isRequired,
|
|
54
|
+
src: PropTypes.string.isRequired,
|
|
55
|
+
isActive: PropTypes.string.isRequired,
|
|
56
|
+
pageNumber: PropTypes.string
|
|
57
|
+
})
|
|
58
|
+
).isRequired,
|
|
59
|
+
onPageClick: PropTypes.func,
|
|
60
|
+
onRecalc: PropTypes.func,
|
|
61
|
+
onSave: PropTypes.func,
|
|
62
|
+
recalcActive: PropTypes.bool,
|
|
63
|
+
saveActive: PropTypes.bool,
|
|
64
|
+
pageNumber: PropTypes.string
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
PagesSelector.defaultProps = {
|
|
68
|
+
onPageClick: () => { },
|
|
69
|
+
onRecalc: () => { },
|
|
70
|
+
onSave: () => { },
|
|
71
|
+
recalcActive: false,
|
|
72
|
+
saveActive: false,
|
|
73
|
+
pageNumber: undefined
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default PagesSelector;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
.page-selector {
|
|
2
|
+
height: 100vh;
|
|
3
|
+
overflow-y: scroll;
|
|
4
|
+
width: 10%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.pages {
|
|
8
|
+
list-style: none;
|
|
9
|
+
padding: 1rem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.page-thumbnail {
|
|
13
|
+
margin-bottom: 1rem;
|
|
14
|
+
border-radius: .25rem !important;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
filter: grayscale(1);
|
|
17
|
+
transition: transform .2s;
|
|
18
|
+
opacity: .5;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.page-thumbnail:hover {
|
|
22
|
+
box-shadow: 0 0 2rem 0 #8898aa !important;
|
|
23
|
+
filter: grayscale(0);
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
opacity: 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.page-thumbnail-is-active {
|
|
29
|
+
filter: grayscale(0);
|
|
30
|
+
opacity: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.page-thumbnail img {
|
|
34
|
+
width: 100%;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.bottom-buttons {
|
|
38
|
+
background: linear-gradient(#8898aa, rgba(255, 255, 255, 0));
|
|
39
|
+
position: sticky;
|
|
40
|
+
top: 0;
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
padding: 1rem;
|
|
44
|
+
margin-bottom: 1rem;
|
|
45
|
+
z-index: 100;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.bottom-buttons button {
|
|
49
|
+
margin-bottom: 1rem;
|
|
50
|
+
width: 100%;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.page-number-wrapper {
|
|
54
|
+
position: absolute;
|
|
55
|
+
bottom: 0;
|
|
56
|
+
width: 100%;
|
|
57
|
+
height: 10%;
|
|
58
|
+
z-index: 100;
|
|
59
|
+
display: flex;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
align-items: center;
|
|
62
|
+
background: linear-gradient(rgba(255, 255, 255, 0), #8898aa);
|
|
63
|
+
padding: .5rem 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.page-number {
|
|
67
|
+
font-size: 1.5rem;
|
|
68
|
+
font-weight: 800;
|
|
69
|
+
}
|
package/src/RegionLabel/index.js
CHANGED
|
@@ -25,6 +25,7 @@ type Props = {
|
|
|
25
25
|
editing?: boolean,
|
|
26
26
|
allowedClasses?: Array<string>,
|
|
27
27
|
allowedTags?: Array<string>,
|
|
28
|
+
allowedGroups?: Array<{ value: String, label: String }>,
|
|
28
29
|
cls?: string,
|
|
29
30
|
tags?: Array<string>,
|
|
30
31
|
onDelete: (Region) => null,
|
|
@@ -33,6 +34,7 @@ type Props = {
|
|
|
33
34
|
onOpen: (Region) => null,
|
|
34
35
|
onRegionClassAdded: () => {},
|
|
35
36
|
allowComments?: boolean,
|
|
37
|
+
hideNotEditingLabel?: boolean,
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export const RegionLabel = ({
|
|
@@ -46,6 +48,8 @@ export const RegionLabel = ({
|
|
|
46
48
|
onOpen,
|
|
47
49
|
onRegionClassAdded,
|
|
48
50
|
allowComments,
|
|
51
|
+
hideNotEditingLabel,
|
|
52
|
+
allowedGroups,
|
|
49
53
|
}: Props) => {
|
|
50
54
|
const classes = useStyles()
|
|
51
55
|
const commentInputRef = useRef(null)
|
|
@@ -56,6 +60,8 @@ export const RegionLabel = ({
|
|
|
56
60
|
if (commentInput) return commentInput.focus()
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
if (hideNotEditingLabel && !editing) return null
|
|
64
|
+
|
|
59
65
|
return (
|
|
60
66
|
<ThemeProvider theme={theme}>
|
|
61
67
|
<Paper
|
|
@@ -136,6 +142,21 @@ export const RegionLabel = ({
|
|
|
136
142
|
/>
|
|
137
143
|
</div>
|
|
138
144
|
)}
|
|
145
|
+
{(allowedGroups || []).length > 0 && (
|
|
146
|
+
<Select
|
|
147
|
+
onChange={(newGroup) => onChange({
|
|
148
|
+
...(region: any),
|
|
149
|
+
groupId: newGroup.value,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
placeholder="Group"
|
|
154
|
+
value={
|
|
155
|
+
allowedGroups.filter(g => g.value === region.groupId)
|
|
156
|
+
}
|
|
157
|
+
options={allowedGroups}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
139
160
|
{(allowedTags || []).length > 0 && (
|
|
140
161
|
<div style={{ marginTop: 4 }}>
|
|
141
162
|
<Select
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { memo } from "react"
|
|
4
4
|
import colorAlpha from "color-alpha"
|
|
5
|
+
import useColors from '../hooks/use-colors';
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
function clamp(num, min, max) {
|
|
7
9
|
return num <= min ? min : num >= max ? max : num
|
|
@@ -31,19 +33,36 @@ const RegionComponents = {
|
|
|
31
33
|
/>
|
|
32
34
|
</g>
|
|
33
35
|
)),
|
|
34
|
-
box: memo(({ region, iw, ih }) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
box: memo(({ region, iw, ih }) => {
|
|
37
|
+
const { clsColor, groupColor } = useColors();
|
|
38
|
+
|
|
39
|
+
if (region.groupId !== undefined) {
|
|
40
|
+
return <g transform={`translate(${region.x * iw} ${region.y * ih})`}>
|
|
41
|
+
<rect
|
|
42
|
+
strokeWidth={(region.groupHighlighted) ? 3 : 0}
|
|
43
|
+
x={0}
|
|
44
|
+
y={0}
|
|
45
|
+
width={Math.max(region.w * iw, 0)}
|
|
46
|
+
height={Math.max(region.h * ih, 0)}
|
|
47
|
+
stroke={colorAlpha(clsColor(region.cls), 0.85)}
|
|
48
|
+
fill={(region.groupHighlighted) ? colorAlpha(groupColor(region.groupId), 0.5) : colorAlpha(groupColor(region.groupId), 0.25)}
|
|
49
|
+
/>
|
|
50
|
+
</g>
|
|
51
|
+
} else {
|
|
52
|
+
return <g transform={`translate(${region.x * iw} ${region.y * ih})`}>
|
|
53
|
+
<rect
|
|
54
|
+
strokeWidth={2}
|
|
55
|
+
x={0}
|
|
56
|
+
y={0}
|
|
57
|
+
width={Math.max(region.w * iw, 0)}
|
|
58
|
+
height={Math.max(region.h * ih, 0)}
|
|
59
|
+
stroke={colorAlpha(clsColor(region.cls), 0.85)}
|
|
60
|
+
fill={colorAlpha(clsColor(region.cls), 0.25)}
|
|
61
|
+
/>
|
|
62
|
+
</g>
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
),
|
|
47
66
|
polygon: memo(({ region, iw, ih, fullSegmentationMode }) => {
|
|
48
67
|
const Component = region.open ? "polyline" : "polygon"
|
|
49
68
|
const alphaBase = fullSegmentationMode ? 0.5 : 1
|
|
@@ -144,9 +163,8 @@ const RegionComponents = {
|
|
|
144
163
|
{points.map(({ x, y, angle }, i) => (
|
|
145
164
|
<g
|
|
146
165
|
key={i}
|
|
147
|
-
transform={`translate(${x * iw} ${y * ih}) rotate(${
|
|
148
|
-
|
|
149
|
-
})`}
|
|
166
|
+
transform={`translate(${x * iw} ${y * ih}) rotate(${(-(angle || 0) * 180) / Math.PI
|
|
167
|
+
})`}
|
|
150
168
|
>
|
|
151
169
|
<g>
|
|
152
170
|
<rect
|
package/src/RegionTags/index.js
CHANGED
|
@@ -28,6 +28,8 @@ export const RegionTags = ({
|
|
|
28
28
|
RegionEditLabel,
|
|
29
29
|
onRegionClassAdded,
|
|
30
30
|
allowComments,
|
|
31
|
+
hideNotEditingLabel,
|
|
32
|
+
allowedGroups
|
|
31
33
|
}) => {
|
|
32
34
|
const RegionLabel =
|
|
33
35
|
RegionEditLabel != null ? RegionEditLabel : DefaultRegionLabel
|
|
@@ -44,9 +46,9 @@ export const RegionTags = ({
|
|
|
44
46
|
|
|
45
47
|
const coords = displayOnTop
|
|
46
48
|
? {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
left: pbox.x,
|
|
50
|
+
top: pbox.y - margin / 2,
|
|
51
|
+
}
|
|
50
52
|
: { left: pbox.x, top: pbox.y + pbox.h + margin / 2 }
|
|
51
53
|
if (region.locked) {
|
|
52
54
|
return (
|
|
@@ -120,6 +122,8 @@ export const RegionTags = ({
|
|
|
120
122
|
imageSrc={imageSrc}
|
|
121
123
|
onRegionClassAdded={onRegionClassAdded}
|
|
122
124
|
allowComments={allowComments}
|
|
125
|
+
hideNotEditingLabel={hideNotEditingLabel}
|
|
126
|
+
allowedGroups={allowedGroups}
|
|
123
127
|
/>
|
|
124
128
|
</div>
|
|
125
129
|
</div>
|
|
@@ -20,7 +20,7 @@ const pullSettingsFromLocalStorage = () => {
|
|
|
20
20
|
settings[key.replace("settings_", "")] = JSON.parse(
|
|
21
21
|
window.localStorage.getItem(key)
|
|
22
22
|
)
|
|
23
|
-
} catch (e) {}
|
|
23
|
+
} catch (e) { }
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
return settings
|
|
@@ -28,8 +28,12 @@ const pullSettingsFromLocalStorage = () => {
|
|
|
28
28
|
|
|
29
29
|
export const useSettings = () => useContext(SettingsContext)
|
|
30
30
|
|
|
31
|
-
export const SettingsProvider = ({ children }) => {
|
|
32
|
-
const [state, changeState] = useState(
|
|
31
|
+
export const SettingsProvider = ({ children, clsColors, groupColors }) => {
|
|
32
|
+
const [state, changeState] = useState({
|
|
33
|
+
...() => pullSettingsFromLocalStorage(),
|
|
34
|
+
clsColors,
|
|
35
|
+
groupColors,
|
|
36
|
+
})
|
|
33
37
|
const changeSetting = (setting: string, value: any) => {
|
|
34
38
|
changeState({ ...state, [setting]: value })
|
|
35
39
|
window.localStorage.setItem(`settings_${setting}`, JSON.stringify(value))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useSettings } from "../SettingsProvider"
|
|
2
|
+
|
|
3
|
+
function defaultClsColor(cls) {
|
|
4
|
+
switch (cls) {
|
|
5
|
+
case 'title':
|
|
6
|
+
return '#f70202'
|
|
7
|
+
case 'subtitle':
|
|
8
|
+
return "#ffb405"
|
|
9
|
+
case 'text':
|
|
10
|
+
return "#14deef"
|
|
11
|
+
case 'author':
|
|
12
|
+
return "#f8d51e"
|
|
13
|
+
case 'appendix':
|
|
14
|
+
return "#bfede2"
|
|
15
|
+
case 'photo_author':
|
|
16
|
+
return "#9a17bb"
|
|
17
|
+
case 'photo_caption':
|
|
18
|
+
return "#ff84f6"
|
|
19
|
+
case 'advertisement':
|
|
20
|
+
return "#ffb201"
|
|
21
|
+
case 'other_graphics':
|
|
22
|
+
return "#ff5400"
|
|
23
|
+
case 'unknown':
|
|
24
|
+
return "#bfede2"
|
|
25
|
+
case 'about_author':
|
|
26
|
+
return "#9a17bb"
|
|
27
|
+
case 'image':
|
|
28
|
+
return "#14deef"
|
|
29
|
+
case 'interview':
|
|
30
|
+
return "#23b20f"
|
|
31
|
+
case 'table':
|
|
32
|
+
return "#02b4ba"
|
|
33
|
+
default:
|
|
34
|
+
return "#02b4ba"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function defaultGroupColor(groupId) {
|
|
39
|
+
switch (groupId) {
|
|
40
|
+
case "0":
|
|
41
|
+
return "#FDDFDF"
|
|
42
|
+
case "1":
|
|
43
|
+
return "#FCF7DE"
|
|
44
|
+
case "2":
|
|
45
|
+
return "#DEFDE0"
|
|
46
|
+
case "3":
|
|
47
|
+
return "#DEF3FD"
|
|
48
|
+
case "4":
|
|
49
|
+
return "#F0DEFD"
|
|
50
|
+
default:
|
|
51
|
+
return "#F0DEFD"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const useColors = () => {
|
|
56
|
+
const { clsColors, groupColors } = useSettings()
|
|
57
|
+
|
|
58
|
+
const clsColor = (cls) => {
|
|
59
|
+
if (clsColors[cls]) { return clsColors[cls] }
|
|
60
|
+
return defaultClsColor(cls)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const groupColor = (groupId) => {
|
|
64
|
+
if (groupColors[groupId]) { return groupColors[groupId] }
|
|
65
|
+
return defaultGroupColor(groupId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
clsColor,
|
|
70
|
+
groupColor
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default useColors;
|