Skip to content

Commit 7385b5e

Browse files
authored
feat: Merge pull request #308 from UniversalDataTool/feat/pose-estimation
fixes #285, Adds pose/keypoints interface
2 parents a600114 + 37eb1d9 commit 7385b5e

File tree

11 files changed

+390
-5
lines changed

11 files changed

+390
-5
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
"react-hotkeys": "^2.0.0",
111111
"react-i18next": "^11.4.0",
112112
"react-icons": "^3.9.0",
113-
"react-image-annotate": "^1.6.1",
113+
"react-image-annotate": "^1.7.0",
114114
"react-material-workspace-layout": "^0.1.6",
115115
"react-scripts": "^3.4.1",
116116
"react-select": "^3.0.8",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react"
2+
3+
export default ({ iface, onChange }) => {
4+
return (
5+
<div>
6+
We'd like to put a little interface to edit poses or landmarks here, but
7+
we need help! Check out this{" "}
8+
<a href="https://github.com/UniversalDataTool/universal-data-tool/issues/307">
9+
Github Issue
10+
</a>{" "}
11+
to learn more! For now just turn to the JSON tab to edit the landmarks
12+
manually!
13+
</div>
14+
)
15+
}

src/components/ConfigureInterface/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import ConfigureDataEntry from "../ConfigureDataEntry"
1414
import ConfigureComposite from "../ConfigureComposite"
1515
import Configure3D from "../Configure3D"
1616
import ConfigureVideoSegmentation from "../ConfigureVideoSegmentation"
17+
import ConfigureImageLandmarkAnnotation from "../ConfigureImageLandmarkAnnotation"
1718

1819
const Container = styled("div")({
1920
padding: 24,
@@ -72,6 +73,9 @@ export const ConfigureInterface = ({
7273
{iface.type === "3d_bounding_box" && (
7374
<Configure3D iface={iface} onChange={onChange} />
7475
)}
76+
{iface.type === "image_landmark_annotation" && (
77+
<ConfigureImageLandmarkAnnotation iface={iface} onChange={onChange} />
78+
)}
7579
</Container>
7680
)
7781
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// @flow
2+
3+
import React, { useMemo } from "react"
4+
import Annotator from "react-image-annotate"
5+
import { styled } from "@material-ui/core/styles"
6+
import useEventCallback from "use-event-callback"
7+
import {
8+
convertFromRIARegionFmt,
9+
convertToRIAImageFmt,
10+
} from "../../utils/ria-format.js"
11+
import useClobberedState from "../../utils/use-clobbered-state"
12+
13+
const Container = styled("div")({
14+
"& .fullscreen": { height: "100%" },
15+
})
16+
17+
const [emptyObj, emptyArr] = [{}, []]
18+
19+
const posingTools = ["create-keypoints"]
20+
21+
export default ({
22+
sampleIndex: globalSampleIndex,
23+
interface: iface,
24+
sampleIndex,
25+
samples = emptyArr,
26+
containerProps = emptyObj,
27+
onSaveTaskOutputItem,
28+
onModifySample,
29+
}) => {
30+
const [selectedIndex, setSelectedIndex] = useClobberedState(
31+
globalSampleIndex,
32+
0
33+
)
34+
35+
const saveCurrentIndexAnnotation = useEventCallback((output) => {
36+
const img = output.images[0]
37+
const annotation = (img.regions || []).map(convertFromRIARegionFmt)
38+
if (onModifySample) {
39+
onModifySample(selectedIndex, { annotation })
40+
} else {
41+
onSaveTaskOutputItem(selectedIndex, annotation)
42+
}
43+
})
44+
45+
const onExit = useEventCallback((output, nextAction) => {
46+
saveCurrentIndexAnnotation(output)
47+
if (containerProps.onExit) containerProps.onExit(nextAction)
48+
})
49+
const onNextImage = useEventCallback((output) => {
50+
if (selectedIndex + 1 >= samples.length) {
51+
onExit(output, "go-to-next")
52+
} else {
53+
saveCurrentIndexAnnotation(output)
54+
if (setSelectedIndex) {
55+
setSelectedIndex(selectedIndex + 1)
56+
} else {
57+
onExit(output, "go-to-next")
58+
}
59+
}
60+
})
61+
const onPrevImage = useEventCallback((output) => {
62+
if (selectedIndex - 1 < 0) {
63+
onExit(output, "go-to-previous")
64+
} else {
65+
saveCurrentIndexAnnotation(output)
66+
if (setSelectedIndex) {
67+
setSelectedIndex(selectedIndex - 1)
68+
} else {
69+
onExit(output, "go-to-previous")
70+
}
71+
}
72+
})
73+
74+
const singleImageList = useMemo(() => {
75+
return [
76+
convertToRIAImageFmt({
77+
title: containerProps.datasetName || `Sample ${selectedIndex}`,
78+
taskDatum: samples[selectedIndex],
79+
output: samples[selectedIndex].annotation,
80+
selectedIndex,
81+
}),
82+
]
83+
// eslint-disable-next-line
84+
}, [selectedIndex, containerProps.datasetName])
85+
86+
return (
87+
<Container
88+
style={{
89+
height: containerProps.height || "calc(100% - 70px)",
90+
minHeight: 600,
91+
width: "100%",
92+
}}
93+
>
94+
<Annotator
95+
key={globalSampleIndex}
96+
keypointDefinitions={iface.keypointDefinitions}
97+
selectedImage={0}
98+
taskDescription={iface.description}
99+
onNextImage={onNextImage}
100+
onPrevImage={onPrevImage}
101+
enabledTools={posingTools}
102+
images={singleImageList}
103+
onExit={onExit}
104+
/>
105+
</Container>
106+
)
107+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// @flow
2+
3+
import React from "react"
4+
5+
import { storiesOf } from "@storybook/react"
6+
import { action } from "@storybook/addon-actions"
7+
8+
import ImageLandmarkAnnotation from "./"
9+
10+
storiesOf("ImageLandmarkAnnotation", module).add("Basic", () => (
11+
<ImageLandmarkAnnotation
12+
onModifySample={action("onModifySample")}
13+
{...{
14+
interface: {
15+
type: "image_landmark_annotation",
16+
17+
// Different configurations of poses / keypoints
18+
keypointDefinitions: {
19+
// This is a pose definition for the upper half of a human
20+
human: {
21+
// Each landmark is a point on the pose.
22+
landmarks: {
23+
head: {
24+
label: "Head",
25+
color: "#f00",
26+
// The default position is the position relative to the cursor
27+
// in [image_width%, image_height%] to place this point at when
28+
// a pose is created
29+
defaultPosition: [0, -0.05],
30+
},
31+
sternum: {
32+
label: "Torso",
33+
color: "#0f0",
34+
defaultPosition: [0, 0],
35+
},
36+
leftElbow: {
37+
label: "Left Elbow",
38+
color: "#00f",
39+
defaultPosition: [-0.05, 0],
40+
},
41+
rightElbow: {
42+
label: "Right Elbow",
43+
color: "#00f",
44+
defaultPosition: [0.05, 0],
45+
},
46+
},
47+
48+
// The connections will determine what lines are drawn between points, they
49+
// are only aesthetic
50+
connections: [
51+
["head", "sternum"],
52+
["sternum", "leftElbow"],
53+
["sternum", "rightElbow"],
54+
],
55+
},
56+
},
57+
},
58+
samples: [
59+
{
60+
// URL pointing to image
61+
imageUrl:
62+
"https://media.gettyimages.com/photos/dog-and-cat-picture-id151350785",
63+
annotation: {
64+
regionType: "keypoints",
65+
keypointsDefinitionId: "human",
66+
points: {
67+
head: { x: 0.54, y: 0.2 },
68+
sternum: { x: 0.57, y: 0.3 },
69+
// when "obscured" is set to true, it means something is blocking or obstructing the visibility of this keypoint in the image
70+
leftElbow: { x: 0.4, y: 0.39, obscured: true },
71+
rightElbow: { x: 0.7, y: 0.32 },
72+
},
73+
},
74+
},
75+
],
76+
}}
77+
/>
78+
))
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// @flow
2+
3+
import React, { useState } from "react"
4+
5+
import { storiesOf } from "@storybook/react"
6+
import { action } from "@storybook/addon-actions"
7+
8+
import SetupPage from "./"
9+
10+
storiesOf("SetupPage", module).add("Empty Dataset", () => {
11+
const [dataset, setDataset] = useState({
12+
interface: {},
13+
})
14+
15+
return (
16+
<SetupPage
17+
dataset={dataset}
18+
onChange={setDataset}
19+
onClearLabelData={action("onClearLabelData")}
20+
/>
21+
)
22+
})

src/components/StartingPage/templates.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ThreeDRotation from "@material-ui/icons/ThreeDRotation"
1111
import OndemandVideoIcon from "@material-ui/icons/OndemandVideo"
1212
import ContactSupport from "@material-ui/icons/ContactSupport"
1313
import LowPriority from "@material-ui/icons/LowPriority"
14+
import AccessibilityNewIcon from "@material-ui/icons/AccessibilityNew"
1415

1516
export const templates = [
1617
{
@@ -291,6 +292,136 @@ export const templates = [
291292
],
292293
},
293294
},
295+
{
296+
name: "Image Landmark Annotation",
297+
Icon: AccessibilityNewIcon,
298+
dataset: {
299+
interface: {
300+
type: "image_landmark_annotation",
301+
keypointDefinitions: {
302+
human: {
303+
landmarks: {
304+
nose: {
305+
label: "Nose",
306+
color: "#f00",
307+
defaultPosition: [0, 0],
308+
},
309+
leftEye: {
310+
label: "Left Eye",
311+
color: "#00f",
312+
defaultPosition: [-0.05, -0.05],
313+
},
314+
leftEar: {
315+
label: "Left Ear",
316+
color: "#0ff",
317+
defaultPosition: [-0.1, -0.05],
318+
},
319+
rightEye: {
320+
label: "Left Eye",
321+
color: "#f0f",
322+
defaultPosition: [0.05, -0.05],
323+
},
324+
rightEar: {
325+
label: "Left Ear",
326+
color: "#00f",
327+
defaultPosition: [0.1, -0.05],
328+
},
329+
sternum: {
330+
label: "Sternum",
331+
color: "#0f0",
332+
defaultPosition: [0, 0.1],
333+
},
334+
leftShoulder: {
335+
label: "Left Shoulder",
336+
color: "#0ff",
337+
defaultPosition: [-0.2, 0.1],
338+
},
339+
rightShoulder: {
340+
label: "Right Shoulder",
341+
color: "#00f",
342+
defaultPosition: [0.2, 0.1],
343+
},
344+
leftElbow: {
345+
label: "Left Elbow",
346+
color: "#0f0",
347+
defaultPosition: [-0.2, 0.2],
348+
},
349+
rightElbow: {
350+
label: "Right Elbow",
351+
color: "#f00",
352+
defaultPosition: [0.2, 0.2],
353+
},
354+
leftHand: {
355+
label: "Left Hand",
356+
color: "#00f",
357+
defaultPosition: [-0.2, 0.3],
358+
},
359+
rightHand: {
360+
label: "Right Hand",
361+
color: "#f0f",
362+
defaultPosition: [0.2, 0.3],
363+
},
364+
leftThigh: {
365+
label: "Left Thigh",
366+
color: "#f00",
367+
defaultPosition: [-0.1, 0.35],
368+
},
369+
rightThigh: {
370+
label: "Right Thigh",
371+
color: "#0ff",
372+
defaultPosition: [0.1, 0.35],
373+
},
374+
leftKnee: {
375+
label: "Left Thigh",
376+
color: "#ff0",
377+
defaultPosition: [-0.15, 0.45],
378+
},
379+
rightKnee: {
380+
label: "Right Thigh",
381+
color: "#0f0",
382+
defaultPosition: [0.15, 0.45],
383+
},
384+
leftFoot: {
385+
label: "Left Foot",
386+
color: "#00f",
387+
defaultPosition: [-0.15, 0.55],
388+
},
389+
rightFoot: {
390+
label: "Right Foot",
391+
color: "#f00",
392+
defaultPosition: [0.15, 0.55],
393+
},
394+
},
395+
connections: [
396+
["sternum", "nose"],
397+
["nose", "leftEye"],
398+
["leftEye", "leftEar"],
399+
["nose", "rightEye"],
400+
["rightEye", "rightEar"],
401+
["sternum", "leftShoulder"],
402+
["leftShoulder", "leftElbow"],
403+
["leftElbow", "leftHand"],
404+
["sternum", "leftThigh"],
405+
["leftThigh", "leftKnee"],
406+
["leftKnee", "leftFoot"],
407+
["sternum", "rightShoulder"],
408+
["rightShoulder", "rightElbow"],
409+
["rightElbow", "rightHand"],
410+
["sternum", "rightThigh"],
411+
["rightThigh", "rightKnee"],
412+
["rightKnee", "rightFoot"],
413+
],
414+
},
415+
},
416+
},
417+
samples: [
418+
{
419+
imageUrl:
420+
"https://media.istockphoto.com/photos/businesswoman-picture-id918002786",
421+
},
422+
],
423+
},
424+
},
294425
{
295426
name: "3D Bounding Box",
296427
Icon: ThreeDRotation,

0 commit comments

Comments
 (0)