Commit 63404119 authored by Georg Felbinger's avatar Georg Felbinger
Browse files

version 0.2.0

- fixed issues #1,#2,#3,#4
- larger preview, positioning in corner with most space
parent a413b378
Pipeline #15560 failed with stages
import axios from 'axios';
import { Box, Label, PostImage } from './api/Image';
import { ReducerFactory } from './util/Reducers';
import { connect, Dispatch } from 'react-redux';
import { RootState } from './index';
......@@ -8,111 +6,7 @@ import { Component } from './AppView';
const actions = new ReducerFactory<State, OwnProps>();
const CreateBox = actions.createAction<Box>(
(state, box) => ({
...state,
box: [...state.box, box.value]
})
);
const DeleteBox = actions.createAction<number>(
(state, boxIndex) => ({
...state,
box: state.box.filter((b, i) => i !== boxIndex.value)
})
);
const LoadLabels = actions.createVoidAction(
(state, action) => {
axios
.get('/v1/labels')
.then(response => response.data as string[])
.then(labels => UpdateLabels.create(action.dispatch, action.props)(labels));
return state;
});
const LoadImage = actions.createVoidAction(
(state, action) => {
axios
.get('/v1/image')
.then(response => response.data as Label)
.then(label => UpdateImage.create(action.dispatch, action.props)(label));
return state;
}
);
const SaveImage = actions.createVoidAction(
(state, action) => {
const auth = action.props.authToken
.map(a => ({
authToken: a
} as {}))
.get(() => ({}));
const data: PostImage = {
...auth,
label: {
url: state.image,
boxes: state.box.map(b => ({
...b,
start: {
x: b.start.x * 640,
y: b.start.y * 480
},
size: {
x: b.size.x * 640,
y: b.size.y * 480
}
}))
}
};
axios.post('/v1/image', data).then(() => {
Reset.create(action.dispatch, action.props)();
LoadImage.create(action.dispatch, action.props)();
LoadLabels.create(action.dispatch, action.props)();
});
return state;
}
);
const Reset = actions.createVoidAction(
() => {
return initialState;
}
);
const UpdateImage = actions.createAction<Label>(
(state, action) => {
return {
...state,
image: action.value.url,
box: action.value.boxes.map(b => ({
...b,
start: {
x: b.start.x / 640,
y: b.start.y / 480
},
size: {
x: b.size.x / 640,
y: b.size.y / 480
}
}))
};
}
);
const UpdateLabels = actions.createAction<string[]>(
(state, action) => ({
...state,
labels: action.value
})
);
const initialState: State = {
labels: [],
image: '',
box: [],
};
const initialState: State = {};
export const AppReducer = actions.build(initialState);
......@@ -127,12 +21,6 @@ export const App = connect(
};
},
(dispatch: Dispatch<{}>, props: OwnProps): Actions => {
return {
onCreateBox: CreateBox.create(dispatch, props),
onDeleteBox: DeleteBox.create(dispatch, props),
onSave: SaveImage.create(dispatch, props),
onLoadImage: LoadImage.create(dispatch, props),
onLoadLabels: LoadLabels.create(dispatch, props),
};
return {};
}
)(Component);
import { Box } from './api/Image';
import { Option } from './util/Types';
export interface OwnProps {
......@@ -6,17 +5,9 @@ export interface OwnProps {
}
export interface State {
labels: string[];
image: string;
box: Box[];
}
export interface Actions {
onCreateBox: (box: Box) => void;
onDeleteBox: (boxIndex: number) => void;
onSave: (v: {}) => void;
onLoadImage: (v: {}) => void;
onLoadLabels: (v: {}) => void;
}
export interface Props extends OwnProps {
......
import * as React from 'react';
import { AnnotateImage } from './annotation/AnnotationController';
import { Button, FormGroup, Grid, InputGroup, Jumbotron, Nav, Navbar, NavItem } from 'react-bootstrap';
import { renderIf } from './util/Reacts';
import { Col, Grid, Jumbotron, Nav, Navbar, NavItem, Row, Table } from 'react-bootstrap';
import { Actions, Props } from './AppModel';
import { Lifecycle } from './Lifecycle';
import { LoginButton } from './login/LoginController';
import { Score } from './scores/ScoreController';
......@@ -25,45 +23,60 @@ export const Component = (props: Props & Actions) => {
</Navbar.Collapse>
</Navbar>
<Grid>
<Lifecycle
componentDidMount={() => {
props.onLoadImage({});
props.onLoadLabels({});
}}
/>
<AnnotateImage
imageUrl={props.state.image}
boxes={props.state.box}
labels={props.state.labels}
onCreateBox={props.onCreateBox}
authToken={props.authToken}
/>
<Jumbotron>
{props.state.box.map((box, i) => {
return (
<FormGroup key={i}>
<InputGroup>
<InputGroup.Addon>{i}: {box.label}</InputGroup.Addon>
<Button
id={`d-${i}`}
onClick={() => props.onDeleteBox(i)}
>
x
</Button>
</InputGroup>
</FormGroup>
);
})}
<Button
default={false}
onClick={props.onSave}
>
{renderIf(
props.state.box.length === 0,
() => 'No Labels',
() => 'Send')}
</Button>
</Jumbotron>
<Score/>
<Row>
<Col xs={12} md={12}>
<Score/>
</Col>
</Row>
<Row>
<Col xs={12} md={12}>
<Jumbotron>
<h3>Readme</h3>
<h4>
Keybindings
</h4>
<Table>
<thead>
<tr>
<th>Key</th>
<th>Effect</th>
</tr>
</thead>
<tbody>
<tr>
<td>[Shift+]Arrows</td>
<td>Move second control point. (Use Shift for fast mode)</td>
</tr>
<tr>
<td>Ctrl+[Shift+]Arrows</td>
<td>Move first control point. (Use Shift for fast mode)</td>
</tr>
<tr>
<td>Numbers</td>
<td>Select the label type</td>
</tr>
<tr>
<td>Enter</td>
<td>Add current selection to labels</td>
</tr>
<tr>
<td>Shift+Enter</td>
<td>Send image to server</td>
</tr>
</tbody>
</Table>
<h4>How to label</h4>
<ul>
<li>Select the bounding boxes around objects that you can clearly identify.</li>
<li>Surround the objects as close as possible, but completely.</li>
<li>The robots own belly isn't a robot.</li>
</ul>
</Jumbotron>
</Col>
</Row>
</Grid>
</div>
);
......
......@@ -5,6 +5,8 @@ import { Point, Points } from '../api/Point';
import { connect, Dispatch } from 'react-redux';
import { RootState } from '../index';
import { Component } from './AnnotationView';
import { Box, Label, PostImage } from '../api/Image';
import axios from 'axios';
const reducer = new ReducerFactory<State, Props>();
......@@ -133,31 +135,131 @@ const TouchEnd = reducer.createAction<React.TouchEvent<{}>>((s, a) => {
}
});
const CreateBox = reducer.createAction<Box>(
(state, box) => ({
...state,
box: [...state.box, box.value]
})
);
const DeleteBox = reducer.createAction<number>(
(state, boxIndex) => ({
...state,
box: state.box.filter((b, i) => i !== boxIndex.value)
})
);
const SaveImage = reducer.createVoidAction(
(state, action) => {
const auth = action.props.authToken
.map(a => ({
authToken: a
} as {}))
.get(() => ({}));
const data: PostImage = {
...auth,
label: {
url: state.image,
boxes: state.box.map(b => ({
...b,
start: {
x: b.start.x * 640,
y: b.start.y * 480
},
size: {
x: b.size.x * 640,
y: b.size.y * 480
}
}))
}
};
axios.post('/v1/image', data).then(() => {
Reset.create(action.dispatch, action.props)();
LoadImage.create(action.dispatch, action.props)();
LoadLabels.create(action.dispatch, action.props)();
});
return state;
}
);
const LoadLabels = reducer.createVoidAction(
(state, action) => {
axios
.get('/v1/labels')
.then(response => response.data as string[])
.then(labels => UpdateLabels.create(action.dispatch, action.props)(labels));
return state;
});
const LoadImage = reducer.createVoidAction(
(state, action) => {
axios
.get('/v1/image')
.then(response => response.data as Label)
.then(label => UpdateImage.create(action.dispatch, action.props)(label));
return state;
}
);
const UpdateImage = reducer.createAction<Label>(
(state, action) => {
return {
...state,
image: action.value.url,
box: action.value.boxes.map(b => ({
...b,
start: {
x: b.start.x / 640,
y: b.start.y / 480
},
size: {
x: b.size.x / 640,
y: b.size.y / 480
}
}))
};
}
);
const UpdateLabels = reducer.createAction<string[]>(
(state, action) => ({
...state,
label: state.label === '' ? action.value[0] : state.label,
labels: action.value
})
);
const Reset = reducer.createVoidAction((s) => initialState);
const initialState: State = {
const ResetSelect = reducer.createVoidAction( s => ({
...s,
...initialSelectState
}));
const initialSelectState = {
initial: true,
label: '',
offset: {x: 0, y: 0},
p1: {x: 0, y: 0},
p2: {x: 1, y: 1},
currentMove: CurrentMove.NONE
currentMove: CurrentMove.NONE,
};
const initialState: State = {
...initialSelectState,
label: '',
labels: [],
box: [],
image: ''
};
export const Reducer = reducer.build(initialState);
export const AnnotateImage = connect(
(state: RootState, ownProps: OwnProps) => {
let label = state.annotation.label;
if (label === '' && ownProps.labels.length > 0) {
label = ownProps.labels[0];
}
return ({
...ownProps,
state: {
...state.annotation,
label: label
}
state: state.annotation
});
},
(dispatch: Dispatch<Action<{}, {}>>, props: Props): Actions => ({
......@@ -169,5 +271,11 @@ export const AnnotateImage = connect(
onTouchEnd: TouchEnd.create(dispatch, props),
onArrow: ArrowMove.create(dispatch, props),
onChangeLabel: ChangeLabel.create(dispatch, props),
onCreateBox: CreateBox.create(dispatch, props),
onDeleteBox: DeleteBox.create(dispatch, props),
onSave: SaveImage.create(dispatch, props),
onLoadImage: LoadImage.create(dispatch, props),
onLoadLabels: LoadLabels.create(dispatch, props),
onReset: Reset.create(dispatch, props),
onResetSelect: ResetSelect.create(dispatch, props)
}))(Component);
import { Point } from '../api/Point';
import { Box } from '../api/Image';
import { Option } from '../util/Types';
export enum CurrentMove {
P1, P2, NONE
......@@ -12,13 +13,13 @@ export interface State {
p2: Point;
currentMove: CurrentMove;
label: string;
labels: string[];
image: string;
box: Box[];
}
export interface OwnProps {
imageUrl: string;
boxes: Box[];
labels: string[];
onCreateBox: (box: Box) => void;
authToken: Option<string>;
}
export interface Actions {
......@@ -30,7 +31,13 @@ export interface Actions {
onTouchEnd: (e: React.TouchEvent<{}>) => void;
onArrow: (a: { e: KeyboardEvent, d: Point }) => void;
onChangeLabel: (l: string) => void;
onCreateBox: (box: Box) => void;
onDeleteBox: (boxIndex: number) => void;
onSave: () => void;
onLoadImage: (v: {}) => void;
onLoadLabels: (v: {}) => void;
onReset: () => void;
onResetSelect: () => void;
}
export interface Props extends OwnProps {
......
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Jumbotron } from 'react-bootstrap';
import { Button, Col, ControlLabel, FormGroup, InputGroup, Jumbotron, Row } from 'react-bootstrap';
import { Actions, CurrentMove, Props } from './AnnotationModel';
import { LabelSelect } from './LabelSelectComponent';
import { KeyBinding } from '../util/KeyBinding';
......@@ -10,83 +10,126 @@ import './AnnotationView.css';
import { renderIf } from '../util/Reacts';
import { Rect } from './RectComponent';
import { Preview } from './PreviewComponent';
import { Lifecycle } from '../Lifecycle';
export const Component = (props: Props & Actions) => {
const stateStart = Points.reduce(props.state.p1, props.state.p2, Math.min);
const stateEnd = Points.reduce(props.state.p1, props.state.p2, Math.max);
const stateSize = Points.minus(stateEnd, stateStart);
let movingX = props.state.currentMove === CurrentMove.P1 ? props.state.p1.x : props.state.p2.x;
const movingLeft = stateStart.x !== movingX;
const posX = movingLeft ? stateStart.x - .15 : stateEnd.x + .025;
const previewPos = {x: `${posX * 100}%`, y: `${stateStart.y * 100}%`};
let ctrlPointSize = {x: '0.2em', y: '0.2em'};
return (
<div>
<Jumbotron
ref={e => preventTouchDefaults(e)}
onMouseDown={e => props.onMouseDown(e)}
onMouseMove={e => props.onMouseMove(e)}
onMouseUp={e => props.onMouseUp(e)}
onTouchStart={e => props.onTouchStart(e)}
onTouchMove={e => props.onTouchMove(e)}
onTouchEnd={e => props.onTouchEnd(e)}
>
<div id="labelImage" className="image draw-panel">
<img src={props.imageUrl} width="100%" draggable={false}/>
{renderIf(!(props.state.currentMove === CurrentMove.NONE), () => (
<Preview
imageUrl={props.imageUrl}
stateStart={stateStart}
stateSize={stateSize}
position={previewPos}
/>
))}
{renderIf(!props.state.initial, () => (
<div>
<Rect
start={ToPercent(stateStart)}
size={ToPercent(stateSize)}
content={props.state.label}
color="pink"
/>
<Rect
start={ToPercent(props.state.p1)}
size={ctrlPointSize}
background="pink"
color="pink"
/>
<Rect
start={ToPercent(props.state.p2)}
size={ctrlPointSize}
background="pink"
color="pink"
<Row>
<Lifecycle
componentDidMount={() => {
props.onLoadImage({});
props.onLoadLabels({});
}}
/>
<KeyBinding code="ArrowUp" onKey={e => props.onArrow({e: e, d: {x: 0, y: -.001}})}/>
<KeyBinding code="ArrowDown" onKey={e => props.onArrow({e: e, d: {x: 0, y: .001}})}/>
<KeyBinding code="ArrowLeft" onKey={e => props.onArrow({e: e, d: {x: -.001, y: 0}})}/>
<KeyBinding code="ArrowRight" onKey={e => props.onArrow({e: e, d: {x: .001, y: 0}})}/>
<KeyBinding code="Escape" onKey={e => props.onResetSelect()}/>
<KeyBinding code="Shift+Enter" onKey={e => window.confirm('Are you sure?') ? props.onSave() : null}/>
<Col xs={12} md={8}>
<Jumbotron
ref={e => preventTouchDefaults(e)}
onMouseDown={e => props.onMouseDown(e)}
onMouseMove={e => props.onMouseMove(e)}
onMouseUp={e => props.onMouseUp(e)}
onTouchStart={e => props.onTouchStart(e)}
onTouchMove={e => props.onTouchMove(e)}
onTouchEnd={e => props.onTouchEnd(e)}
>
<div id="labelImage" className="image draw-panel">
<img src={props.state.image} width="100%" draggable={false}/>
{renderIf(!(props.state.currentMove === CurrentMove.NONE), () => (
<Preview
imageUrl={props.state.image}
stateStart={stateStart}
stateSize={stateSize}
left={stateStart.x > (1 - stateEnd.x) ? '0px' : undefined}
right={stateStart.x <= (1 - stateEnd.x) ? '0px' : undefined}
top={stateStart.y > (1 - stateEnd.y) ? '0px' : undefined}
bottom={stateStart.y <= (1 - stateEnd.y) ? '0px' : undefined}
/>
</div>
))}
{props.boxes.map((b, i) => (
<Rect start={ToPercent(b.start)} size={ToPercent(b.size)} content={`${i}: ${b.label}`}/>
))}
</div>
</Jumbotron>
<Jumbotron>
<LabelSelect
labels={props.labels}
label={props.state.label}
onChangeLabel={props.onChangeLabel}
onSave={l => {
if (!props.state.initial) {
props.onCreateBox(makeBox(l, props.state.p1, props.state.p2));
props.onReset();
}
}}
/>
<KeyBinding code="ArrowUp" onKey={e => props.onArrow({e: e, d: {x: 0, y: -.001}})}/>
<KeyBinding code="ArrowDown" onKey={e => props.onArrow({e: e, d: {x: 0, y: .001}})}/>
<KeyBinding code="ArrowLeft" onKey={e => props.onArrow({e: e, d: {x: -.001, y: 0}})}/>
<KeyBinding code="ArrowRight" onKey={e => props.onArrow({e: e, d: {x: .001, y: 0}})}/>
<KeyBinding code="Escape" onKey={e => props.onReset()}/>
</Jumbotron>