Skip to content

Commit

Permalink
Add Volume Tasks (#3712)
Browse files Browse the repository at this point in the history
* [WIP] volume tasks

* fix volume task creation + requesting. TODO: download

* allow to choose volume tasks via UI for simple task creation

* enforce page reload when switching from/to/between volume tasks; show tracing type in dashboard

* add tracing type to task type

* multi-task download: fetch volume data

* add enums to evolutions

* scalafmt

* add volume data to multi-task zip downloads

* scalafmt

* add tasktype tracing type to test db

* validate tracing type when reading task type from db

* adapt front-end to volume task types

* update some flow types

* update snapshots

* update changelog + migrations

* add error handling for the as-yet unsupported volume task features

* disable nml upload if volume task is selected

* remove superfluous return type

* update evolution numbers

* persist volume task bounding box in nml

* read volume task bounding box from nml after re-upload

* scalafmt

* don't clip rendering of color layer in volume tasks

* undo change to saving skeleton bounding box to nml

* don't clip segmentation outside task bounding box

* fix typo

* Update CHANGELOG.md

* apply PR feedback

* remove typ attribute from task

* update snapshots
  • Loading branch information
fm3 authored Feb 6, 2019
1 parent 9cc6ab4 commit f279737
Show file tree
Hide file tree
Showing 46 changed files with 694 additions and 220 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md).
## Unreleased
[Commits](https://github.com/scalableminds/webknossos/compare/19.02.0...HEAD)

### Added

- Added the possibility to create volume annotation tasks. When creating a task type, select whether to create `volume` or `skeleton` tasks. Note that compound viewing for volume tasks is not supported yet. Same for creating volume tasks from uploaded nml/data files. [#3712](https://github.com/scalableminds/webknossos/pull/3712)

### Changed


### Fixed

- The modals for a new task description and recommended task settings are no longer shown in read-only tracings. [#3724](https://github.com/scalableminds/webknossos/pull/3724)
Expand Down
2 changes: 1 addition & 1 deletion MIGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ User-facing changes are documented in the [changelog](CHANGELOG.md).
-

### Postgres Evolutions:
-
- [038-add-tasktype-tracingtype.sql](conf/evolutions/038-add-tasktype-tracingtype.sql)


## [19.02.0](https://github.com/scalableminds/webknossos/releases/tag/19.02.0) - 2019-02-04
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/admin/api_flow_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export type APITaskType = {
+teamName: string,
+settings: APISettings,
+recommendedConfiguration: ?string,
+tracingType: "skeleton" | "volume",
};

export type TaskStatus = { +open: number, +active: number, +finished: number };
Expand Down
73 changes: 53 additions & 20 deletions app/assets/javascripts/admin/task/task_create_form_view.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
// @flow
import { Form, Select, Button, Card, Radio, Upload, Modal, Icon, InputNumber, Spin } from "antd";
import {
Row,
Col,
Form,
Select,
Button,
Card,
Radio,
Upload,
Modal,
Icon,
InputNumber,
Spin,
} from "antd";
import { type RouterHistory, withRouter } from "react-router-dom";
import React from "react";
import _ from "lodash";
Expand Down Expand Up @@ -160,7 +173,6 @@ class TaskCreateFormView extends React.PureComponent<Props, State> {
formValues.nmlFiles = formValues.nmlFiles.map(
wrapperFile => wrapperFile.originFileObj,
);

response = await createTaskFromNML(formValues);
} else {
response = await createTasks([formValues]);
Expand All @@ -183,6 +195,20 @@ class TaskCreateFormView extends React.PureComponent<Props, State> {
return e && e.fileList;
};

isVolumeTaskType = (taskTypeId?: string): boolean => {
const selectedTaskTypeId = taskTypeId || this.props.form.getFieldValue("taskTypeId");
const selectedTaskType = this.state.taskTypes.find(
taskType => taskType.id === selectedTaskTypeId,
);
return selectedTaskType != null ? selectedTaskType.tracingType === "volume" : false;
};

onChangeTaskType = (taskTypeId: string) => {
if (this.isVolumeTaskType(taskTypeId)) {
this.setState({ isNMLSpecification: false });
}
};

render() {
const { getFieldDecorator } = this.props.form;
const isEditingMode = this.props.taskId != null;
Expand All @@ -207,6 +233,7 @@ class TaskCreateFormView extends React.PureComponent<Props, State> {
style={fullWidth}
autoFocus
disabled={isEditingMode}
onChange={this.onChangeTaskType}
>
{this.state.taskTypes.map((taskType: APITaskType) => (
<Option key={taskType.id} value={taskType.id}>
Expand All @@ -217,24 +244,29 @@ class TaskCreateFormView extends React.PureComponent<Props, State> {
)}
</FormItem>

<FormItem label="Experience Domain" hasFeedback>
{getFieldDecorator("neededExperience.domain", {
rules: [{ required: true }],
})(
<SelectExperienceDomain
disabled={isEditingMode}
placeholder="Select an Experience Domain"
notFoundContent={messages["task.domain_does_not_exist"]}
width={100}
/>,
)}
</FormItem>

<FormItem label="Experience Value" hasFeedback>
{getFieldDecorator("neededExperience.value", {
rules: [{ required: true }, { type: "number" }],
})(<InputNumber style={fullWidth} disabled={isEditingMode} />)}
</FormItem>
<Row gutter={8}>
<Col span={12}>
<FormItem label="Experience Domain" hasFeedback>
{getFieldDecorator("neededExperience.domain", {
rules: [{ required: true }],
})(
<SelectExperienceDomain
disabled={isEditingMode}
placeholder="Select an Experience Domain"
notFoundContent={messages["task.domain_does_not_exist"]}
width={100}
/>,
)}
</FormItem>
</Col>
<Col span={12}>
<FormItem label="Experience Value" hasFeedback>
{getFieldDecorator("neededExperience.value", {
rules: [{ required: true }, { type: "number" }],
})(<InputNumber style={fullWidth} disabled={isEditingMode} />)}
</FormItem>
</Col>
</Row>

<FormItem label={instancesLabel} hasFeedback>
{getFieldDecorator("openInstances", {
Expand Down Expand Up @@ -296,6 +328,7 @@ class TaskCreateFormView extends React.PureComponent<Props, State> {
onChange={(evt: SyntheticInputEvent<*>) =>
this.setState({ isNMLSpecification: evt.target.value === "nml" })
}
disabled={this.isVolumeTaskType()}
>
<Radio value="manual" disabled={isEditingMode}>
Manually Specify Starting Postion
Expand Down
55 changes: 40 additions & 15 deletions app/assets/javascripts/admin/tasktype/task_type_create_view.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @flow
import { Form, Checkbox, Input, Select, Card, Button } from "antd";
import { Button, Card, Checkbox, Form, Input, Radio, Select } from "antd";
import { type RouterHistory, withRouter } from "react-router-dom";
import React from "react";
import _ from "lodash";
Expand All @@ -16,6 +16,8 @@ import RecommendedConfigurationView, {
DEFAULT_RECOMMENDED_CONFIGURATION,
} from "admin/tasktype/recommended_configuration_view";

const RadioGroup = Radio.Group;

const FormItem = Form.Item;
const { Option } = Select;
const { TextArea } = Input;
Expand Down Expand Up @@ -94,10 +96,11 @@ class TaskTypeCreateView extends React.PureComponent<Props, State> {

render() {
const { getFieldDecorator } = this.props.form;
const titlePrefix = this.props.taskTypeId ? "Update " : "Create";
const isEditingMode = this.props.taskTypeId != null;
const titlePrefix = isEditingMode ? "Update " : "Create";

return (
<div className="container">
<div className="container" style={{ maxWidth: 1600, margin: "0 auto" }}>
<Card title={<h3>{titlePrefix} Task Type</h3>}>
<Form onSubmit={this.handleSubmit} layout="vertical">
<FormItem label="Summary" hasFeedback>
Expand Down Expand Up @@ -152,6 +155,21 @@ class TaskTypeCreateView extends React.PureComponent<Props, State> {
})(<TextArea rows={10} />)}
</FormItem>

<FormItem label="Tracing Type">
{getFieldDecorator("tracingType", {
initialValue: "skeleton",
})(
<RadioGroup>
<Radio value="skeleton" disabled={isEditingMode}>
Skeleton
</Radio>
<Radio value="volume" disabled={isEditingMode}>
Volume
</Radio>
</RadioGroup>,
)}
</FormItem>

<FormItem label="Allowed Modes" hasFeedback>
{getFieldDecorator("settings.allowedModes", {
rules: [{ required: true }],
Expand All @@ -170,18 +188,6 @@ class TaskTypeCreateView extends React.PureComponent<Props, State> {
)}
</FormItem>

<FormItem label="Settings">
{getFieldDecorator("settings.somaClickingAllowed", {
valuePropName: "checked",
})(<Checkbox>Allow Single-node-tree mode (&quot;Soma clicking&quot;)</Checkbox>)}
</FormItem>

<FormItem>
{getFieldDecorator("settings.branchPointsAllowed", {
valuePropName: "checked",
})(<Checkbox>Allow Branchpoints</Checkbox>)}
</FormItem>

<FormItem label="Preferred Mode" hasFeedback>
{getFieldDecorator("settings.preferredMode")(
<Select allowClear optionFilterProp="children" style={{ width: "100%" }}>
Expand All @@ -193,6 +199,25 @@ class TaskTypeCreateView extends React.PureComponent<Props, State> {
)}
</FormItem>

<div
style={{
display:
this.props.form.getFieldValue("tracingType") === "skeleton" ? "block" : "none",
}}
>
<FormItem label="Settings">
{getFieldDecorator("settings.somaClickingAllowed", {
valuePropName: "checked",
})(<Checkbox>Allow Single-node-tree mode (&quot;Soma clicking&quot;)</Checkbox>)}
</FormItem>

<FormItem>
{getFieldDecorator("settings.branchPointsAllowed", {
valuePropName: "checked",
})(<Checkbox>Allow Branchpoints</Checkbox>)}
</FormItem>
</div>

<FormItem>
<RecommendedConfigurationView
form={this.props.form}
Expand Down
24 changes: 18 additions & 6 deletions app/assets/javascripts/admin/tasktype/task_type_list_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,24 @@ class TaskTypeListView extends React.PureComponent<Props, State> {
dataIndex="settings"
key="allowedModes"
width={100}
render={settings =>
settings.allowedModes.map(mode => (
<Tag key={mode} color={mode === settings.preferredMode ? "blue" : null}>
{mode}
</Tag>
))
render={(settings, taskType) =>
[
taskType.tracingType === "skeleton" ? (
<Tag color="green" key="tracingType">
skeleton
</Tag>
) : (
<Tag color="orange" key="tracingType">
volume
</Tag>
),
].concat(
settings.allowedModes.map(mode => (
<Tag key={mode} color={mode === settings.preferredMode ? "blue" : null}>
{mode}
</Tag>
)),
)
}
/>
<Column
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/dashboard/dashboard_task_list_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ class DashboardTaskListView extends React.PureComponent<PropsWithRouter, State>
<span style={{ marginRight: 8 }}>
{task.type.summary} (<FormattedDate timestamp={task.created} />)
</span>
{task.annotation.tracing.skeleton == null ? null : <Tag color="green">skeleton</Tag>}
{task.annotation.tracing.volume == null ? null : <Tag color="orange">volume</Tag>}
{task.type.settings.allowedModes.map(mode => (
<Tag key={mode}>{mode}</Tag>
))}
Expand Down
11 changes: 8 additions & 3 deletions app/assets/javascripts/oxalis/api/api_latest.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,23 +371,28 @@ class TracingApi {
const state = Store.getState();
const { annotationType, annotationId } = state.tracing;
const { task } = state;
if (task == null) {
// Satisfy flow
throw new Error("Cannot find task to finish.");
}

await Model.save();
await finishAnnotation(annotationId, annotationType);
try {
const annotation = await requestTask();

const isDifferentDataset = state.dataset.name !== annotation.dataSetName;
const isDifferentTaskType = annotation.task.type.id !== Utils.__guard__(task, x => x.type.id);
const isDifferentTaskType = annotation.task.type.id !== task.type.id;
const involvesVolumeTask = state.tracing.volume != null || annotation.tracing.volume != null;

const currentScript = task != null && task.script != null ? task.script.gist : null;
const currentScript = task.script != null ? task.script.gist : null;
const nextScript = annotation.task.script != null ? annotation.task.script.gist : null;
const isDifferentScript = currentScript !== nextScript;

const newTaskUrl = `/annotations/${annotation.typ}/${annotation.id}`;

// In some cases the page needs to be reloaded, in others the tracing can be hot-swapped
if (isDifferentDataset || isDifferentTaskType || isDifferentScript) {
if (isDifferentDataset || isDifferentTaskType || isDifferentScript || involvesVolumeTask) {
location.href = newTaskUrl;
} else {
await this.restart(annotation.typ, annotation.id, ControlModeEnum.TRACE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function getByteCount(dataset: APIDataset, layerName: string): number {
return getByteCountFromLayer(getLayerByName(dataset, layerName));
}

type Boundary = { lowerBoundary: Vector3, upperBoundary: Vector3 };
export type Boundary = { lowerBoundary: Vector3, upperBoundary: Vector3 };

export function getLayerBoundaries(dataset: APIDataset, layerName: string): Boundary {
const { topLeft, width, height, depth } = getLayerByName(dataset, layerName).boundingBox;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,25 @@ class DataCube {
this.cubes[i] = new CubeEntry(zoomedCubeBoundary);
}

this.boundingBox = new BoundingBox(getSomeTracing(Store.getState().tracing).boundingBox, this);
const shouldBeRestrictedByTracingBoundingBox = () => {
const { task } = Store.getState();
const isVolumeTask = task != null && task.type.tracingType === "volume";
return !isVolumeTask;
};
this.boundingBox = new BoundingBox(
shouldBeRestrictedByTracingBoundingBox()
? getSomeTracing(Store.getState().tracing).boundingBox
: null,
this,
);

listenToStoreProperty(
state => getSomeTracing(state.tracing).boundingBox,
boundingBox => {
this.boundingBox = new BoundingBox(boundingBox, this);
this.forgetOutOfBoundaryBuckets();
if (shouldBeRestrictedByTracingBoundingBox()) {
this.boundingBox = new BoundingBox(boundingBox, this);
this.forgetOutOfBoundaryBuckets();
}
},
);
}
Expand Down
13 changes: 13 additions & 0 deletions app/assets/javascripts/oxalis/model/reducers/reducer_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import Maybe from "data.maybe";

import type { APIAnnotation, ServerBoundingBox } from "admin/api_flow_types";
import type { Annotation, BoundingBoxObject } from "oxalis/store";
import type { Boundary } from "oxalis/model/accessors/dataset_accessor";
import type { BoundingBoxType } from "oxalis/constants";
import { V3 } from "libs/mjs";
import * as Utils from "libs/utils";

export function convertServerBoundingBoxToFrontend(
Expand Down Expand Up @@ -32,6 +34,17 @@ export function convertFrontendBoundingBoxToServer(
.getOrElse(null);
}

export function convertBoundariesToBoundingBox(boundary: Boundary): BoundingBoxObject {
const [width, height, depth] = V3.sub(boundary.upperBoundary, boundary.lowerBoundary);
return {
width,
height,
depth,
topLeft: boundary.lowerBoundary,
};
}

// Currently unused.
export function convertPointToVecInBoundingBox(boundingBox: ServerBoundingBox): BoundingBoxObject {
return {
width: boundingBox.width,
Expand Down
Loading

0 comments on commit f279737

Please sign in to comment.