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 { ReducerFactory } from './util/Reducers';
import { connect, Dispatch } from 'react-redux'; import { connect, Dispatch } from 'react-redux';
import { RootState } from './index'; import { RootState } from './index';
...@@ -8,111 +6,7 @@ import { Component } from './AppView'; ...@@ -8,111 +6,7 @@ import { Component } from './AppView';
const actions = new ReducerFactory<State, OwnProps>(); const actions = new ReducerFactory<State, OwnProps>();
const CreateBox = actions.createAction<Box>( const initialState: State = {};
(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: [],
};
export const AppReducer = actions.build(initialState); export const AppReducer = actions.build(initialState);
...@@ -127,12 +21,6 @@ export const App = connect( ...@@ -127,12 +21,6 @@ export const App = connect(
}; };
}, },
(dispatch: Dispatch<{}>, props: OwnProps): Actions => { (dispatch: Dispatch<{}>, props: OwnProps): Actions => {
return { 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),
};
} }
)(Component); )(Component);
import { Box } from './api/Image';
import { Option } from './util/Types'; import { Option } from './util/Types';
export interface OwnProps { export interface OwnProps {
...@@ -6,17 +5,9 @@ export interface OwnProps { ...@@ -6,17 +5,9 @@ export interface OwnProps {
} }
export interface State { export interface State {
labels: string[];
image: string;
box: Box[];
} }
export interface Actions { 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 { export interface Props extends OwnProps {
......
import * as React from 'react'; import * as React from 'react';
import { AnnotateImage } from './annotation/AnnotationController'; import { AnnotateImage } from './annotation/AnnotationController';
import { Button, FormGroup, Grid, InputGroup, Jumbotron, Nav, Navbar, NavItem } from 'react-bootstrap'; import { Col, Grid, Jumbotron, Nav, Navbar, NavItem, Row, Table } from 'react-bootstrap';
import { renderIf } from './util/Reacts';
import { Actions, Props } from './AppModel'; import { Actions, Props } from './AppModel';
import { Lifecycle } from './Lifecycle';
import { LoginButton } from './login/LoginController'; import { LoginButton } from './login/LoginController';
import { Score } from './scores/ScoreController'; import { Score } from './scores/ScoreController';
...@@ -25,45 +23,60 @@ export const Component = (props: Props & Actions) => { ...@@ -25,45 +23,60 @@ export const Component = (props: Props & Actions) => {
</Navbar.Collapse> </Navbar.Collapse>
</Navbar> </Navbar>
<Grid> <Grid>
<Lifecycle
componentDidMount={() => {
props.onLoadImage({});
props.onLoadLabels({});
}}
/>
<AnnotateImage <AnnotateImage
imageUrl={props.state.image} authToken={props.authToken}
boxes={props.state.box}
labels={props.state.labels}
onCreateBox={props.onCreateBox}
/> />
<Jumbotron> <Row>
{props.state.box.map((box, i) => { <Col xs={12} md={12}>
return ( <Score/>
<FormGroup key={i}> </Col>
<InputGroup> </Row>
<InputGroup.Addon>{i}: {box.label}</InputGroup.Addon> <Row>
<Button <Col xs={12} md={12}>
id={`d-${i}`} <Jumbotron>
onClick={() => props.onDeleteBox(i)} <h3>Readme</h3>
> <h4>
x Keybindings
</Button> </h4>
</InputGroup> <Table>
</FormGroup> <thead>
); <tr>
})} <th>Key</th>
<Button <th>Effect</th>
default={false} </tr>
onClick={props.onSave} </thead>
> <tbody>
{renderIf( <tr>
props.state.box.length === 0, <td>[Shift+]Arrows</td>
() => 'No Labels', <td>Move second control point. (Use Shift for fast mode)</td>
() => 'Send')} </tr>
</Button> <tr>
</Jumbotron> <td>Ctrl+[Shift+]Arrows</td>
<Score/> <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> </Grid>
</div> </div>
); );
......
...@@ -5,6 +5,8 @@ import { Point, Points } from '../api/Point'; ...@@ -5,6 +5,8 @@ import { Point, Points } from '../api/Point';
import { connect, Dispatch } from 'react-redux'; import { connect, Dispatch } from 'react-redux';
import { RootState } from '../index'; import { RootState } from '../index';
import { Component } from './AnnotationView'; import { Component } from './AnnotationView';
import { Box, Label, PostImage } from '../api/Image';
import axios from 'axios';
const reducer = new ReducerFactory<State, Props>(); const reducer = new ReducerFactory<State, Props>();
...@@ -133,31 +135,131 @@ const TouchEnd = reducer.createAction<React.TouchEvent<{}>>((s, a) => { ...@@ -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 Reset = reducer.createVoidAction((s) => initialState);
const initialState: State = { const ResetSelect = reducer.createVoidAction( s => ({
...s,
...initialSelectState
}));
const initialSelectState = {
initial: true, initial: true,
label: '',
offset: {x: 0, y: 0}, offset: {x: 0, y: 0},
p1: {x: 0, y: 0}, p1: {x: 0, y: 0},
p2: {x: 1, y: 1}, 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 Reducer = reducer.build(initialState);
export const AnnotateImage = connect( export const AnnotateImage = connect(
(state: RootState, ownProps: OwnProps) => { (state: RootState, ownProps: OwnProps) => {
let label = state.annotation.label;
if (label === '' && ownProps.labels.length > 0) {
label = ownProps.labels[0];
}
return ({ return ({
...ownProps, ...ownProps,
state: { state: state.annotation
...state.annotation,
label: label
}
}); });
}, },
(dispatch: Dispatch<Action<{}, {}>>, props: Props): Actions => ({ (dispatch: Dispatch<Action<{}, {}>>, props: Props): Actions => ({
...@@ -169,5 +271,11 @@ export const AnnotateImage = connect( ...@@ -169,5 +271,11 @@ export const AnnotateImage = connect(
onTouchEnd: TouchEnd.create(dispatch, props), onTouchEnd: TouchEnd.create(dispatch, props),
onArrow: ArrowMove.create(dispatch, props), onArrow: ArrowMove.create(dispatch, props),
onChangeLabel: ChangeLabel.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), onReset: Reset.create(dispatch, props),
onResetSelect: ResetSelect.create(dispatch, props)
}))(Component); }))(Component);
import { Point } from '../api/Point'; import { Point } from '../api/Point';
import { Box } from '../api/Image'; import { Box } from '../api/Image';
import { Option } from '../util/Types';
export enum CurrentMove { export enum CurrentMove {
P1, P2, NONE P1, P2, NONE
...@@ -12,13 +13,13 @@ export interface State { ...@@ -12,13 +13,13 @@ export interface State {
p2: Point; p2: Point;
currentMove: CurrentMove; currentMove: CurrentMove;
label: string; label: string;
labels: string[];
image: string;
box: Box[];
} }
export interface OwnProps { export interface OwnProps {
imageUrl: string; authToken: Option<string>;
boxes: Box[];
labels: string[];
onCreateBox: (box: Box) => void;
} }
export interface Actions { export interface Actions {
...@@ -30,7 +31,13 @@ export interface Actions { ...@@ -30,7 +31,13 @@ export interface Actions {
onTouchEnd: (e: React.TouchEvent<{}>) => void; onTouchEnd: (e: React.TouchEvent<{}>) => void;
onArrow: (a: { e: KeyboardEvent, d: Point }) => void; onArrow: (a: { e: KeyboardEvent, d: Point }) => void;
onChangeLabel: (l: string) => void; onChangeLabel: (l: string) => void;
onCreateBox: (box: Box) => void;
onDeleteBox: (boxIndex: number) => void;
onSave: () => void;
onLoadImage: (v: {}) => void;
onLoadLabels: (v: {}) => void;
onReset: () => void; onReset: () => void;
onResetSelect: () => void;
} }
export interface Props extends OwnProps { export interface Props extends OwnProps {
......
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; 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 { Actions, CurrentMove, Props } from './AnnotationModel';
import { LabelSelect } from './LabelSelectComponent'; import { LabelSelect } from './LabelSelectComponent';
import { KeyBinding } from '../util/KeyBinding'; import { KeyBinding } from '../util/KeyBinding';
...@@ -10,83 +10,126 @@ import './AnnotationView.css'; ...@@ -10,83 +10,126 @@ import './AnnotationView.css';
import { renderIf } from '../util/Reacts'; import { renderIf } from '../util/Reacts';
import { Rect } from './RectComponent'; import { Rect } from './RectComponent';
import { Preview } from './PreviewComponent'; import { Preview } from './PreviewComponent';
import { Lifecycle } from '../Lifecycle';
export const Component = (props: Props & Actions) => { export const Component = (props: Props & Actions) => {
const stateStart = Points.reduce(props.state.p1, props.state.p2, Math.min); 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 stateEnd = Points.reduce(props.state.p1, props.state.p2, Math.max);
const stateSize = Points.minus(stateEnd, stateStart); 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'}; let ctrlPointSize = {x: '0.2em', y: '0.2em'};
return ( return (
<div> <Row>
<Jumbotron <Lifecycle
ref={e => preventTouchDefaults(e)} componentDidMount={() => {
onMouseDown={e => props.onMouseDown(e)} props.onLoadImage({});
onMouseMove={e => props.onMouseMove(e)} props.onLoadLabels({});
onMouseUp={e => props.onMouseUp(e)} }}
onTouchStart={e => props.onTouchStart(e)} />
onTouchMove={e => props.onTouchMove(e)} <KeyBinding code="ArrowUp" onKey={e => props.onArrow({e: e, d: {x: 0, y: -.001}})}/>
onTouchEnd={e => props.onTouchEnd(e)} <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}})}/>
<div id="labelImage" className="image draw-panel"> <KeyBinding code="ArrowRight" onKey={e => props.onArrow({e: e, d: {x: .001, y: 0}})}/>
<img src={props.imageUrl} width="100%" draggable={false}/> <KeyBinding code="Escape" onKey={e => props.onResetSelect()}/>
{renderIf(!(props.state.currentMove === CurrentMove.NONE), () => ( <KeyBinding code="Shift+Enter" onKey={e => window.confirm('Are you sure?') ? props.onSave() : null}/>
<Preview