Skip to content

Commit

Permalink
Allow to restore older volume versions (#3349)
Browse files Browse the repository at this point in the history
* collect all bucket on version restore and refresh view, add placeholder if no versions are available

* fix lint, fix tests

* add version when requesting segmentation layer buckets

* allow version restore for volumes

* add possibilty to specify a version in a dataRequest #3146

* fix tests

* add changelog entry and change docs

* reset to newest volume version when closing version view
  • Loading branch information
daniel-wer authored and youri-k committed Oct 16, 2018
1 parent b2a9789 commit be56b0b
Show file tree
Hide file tree
Showing 18 changed files with 97 additions and 40 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md).
### Added

- Added support for duplicate dataset names for different organizations [#3137](https://github.com/scalableminds/webknossos/pull/3137)
- Extended the version restore view and added a view to restore older versions of a volume tracing. Access it through the dropdown next to the Save button. [#3349](https://github.com/scalableminds/webknossos/pull/3349)
- Added support to watch additional dataset directories, automatically creating symbolic links to the main directory [#3330](https://github.com/scalableminds/webknossos/pull/3330)
- A User can now have multiple layouts for tracing views. [#3299](https://github.com/scalableminds/webknossos/pull/3299)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,18 @@ class DataCube {
this.bucketIterator = ++this.bucketIterator % this.MAXIMUM_BUCKET_COUNT;
}

collectAllBuckets(): void {
for (const bucket of this.buckets) {
if (bucket != null) {
this.collectBucket(bucket);
bucket.trigger("bucketCollected");
}
}
this.buckets = [];
this.bucketCount = 0;
this.bucketIterator = 0;
}

collectBucket(bucket: DataBucket): void {
const address = bucket.zoomedAddress;
const bucketIndex = this.getBucketIndex(address);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export default class LayerRenderingManager {
};
name: string;
isSegmentation: boolean;
needsRefresh: boolean = false;

constructor(
name: string,
Expand All @@ -96,6 +97,10 @@ export default class LayerRenderingManager {
this.isSegmentation = isSegmentation;
}

refresh() {
this.needsRefresh = true;
}

setupDataTextures(): void {
const bytes = getByteCount(Store.getState().dataset, this.name);

Expand Down Expand Up @@ -164,14 +169,16 @@ export default class LayerRenderingManager {
(isArbitrary && !_.isEqual(this.lastZoomedMatrix, matrix)) ||
viewMode !== this.lastViewMode ||
sphericalCapRadius !== this.lastSphericalCapRadius ||
isInvisible !== this.lastIsInvisible
isInvisible !== this.lastIsInvisible ||
this.needsRefresh
) {
this.lastSubBucketLocality = subBucketLocality;
this.lastAreas = areas;
this.lastZoomedMatrix = matrix;
this.lastViewMode = viewMode;
this.lastSphericalCapRadius = sphericalCapRadius;
this.lastIsInvisible = isInvisible;
this.needsRefresh = false;

const bucketQueue = new PriorityQueue({
// small priorities take precedence
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type SendBucketInfo = {
type RequestBucketInfo = {
...SendBucketInfo,
fourBit: boolean,
version: ?number,
};

// Converts a zoomed address ([x, y, z, zoomStep] array) into a bucket JSON
Expand All @@ -38,9 +39,11 @@ const createRequestBucketInfo = (
zoomedAddress: Vector4,
resolutions: Array<Vector3>,
fourBit: boolean,
version: ?number,
): RequestBucketInfo => ({
...createSendBucketInfo(zoomedAddress, resolutions),
fourBit,
...(version != null ? { version } : {}),
});

function createSendBucketInfo(zoomedAddress: Vector4, resolutions: Array<Vector3>): SendBucketInfo {
Expand All @@ -55,12 +58,14 @@ export async function requestFromStore(
layerInfo: DataLayerType,
batch: Array<Vector4>,
): Promise<{ buffer: Uint8Array, missingBuckets: number[] }> {
const fourBit =
Store.getState().datasetConfiguration.fourBit &&
!isSegmentationLayer(Store.getState().dataset, layerInfo.name);
const resolutions = getResolutions(Store.getState().dataset);
const state = Store.getState();
const isSegmentation = isSegmentationLayer(state.dataset, layerInfo.name);
const fourBit = state.datasetConfiguration.fourBit && !isSegmentation;
const resolutions = getResolutions(state.dataset);
const version =
isSegmentation && state.tracing.volume != null ? state.tracing.volume.version : null;
const bucketInfo = batch.map(zoomedAddress =>
createRequestBucketInfo(zoomedAddress, resolutions, fourBit),
createRequestBucketInfo(zoomedAddress, resolutions, fourBit, version),
);

return doWithToken(async token => {
Expand Down
10 changes: 8 additions & 2 deletions app/assets/javascripts/oxalis/model/sagas/save_saga.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,17 +418,23 @@ export function* saveTracingTypeAsync(tracingType: "skeleton" | "volume"): Saga<
}
}
yield* take("WK_READY");
const allowUpdate = yield* select(
const initialAllowUpdate = yield* select(
state => state.tracing[tracingType] && state.tracing.restrictions.allowUpdate,
);
if (!allowUpdate) return;
if (!initialAllowUpdate) return;

while (true) {
if (tracingType === "skeleton") {
yield* take([...SkeletonTracingSaveRelevantActions, ...FlycamActions, "UNDO", "REDO"]);
} else {
yield* take([...VolumeTracingSaveRelevantActions, ...FlycamActions]);
}
// The allowUpdate setting could have changed in the meantime
const allowUpdate = yield* select(
state => state.tracing[tracingType] && state.tracing.restrictions.allowUpdate,
);
if (!allowUpdate) return;

const tracing = yield* select(state => state.tracing);
const flycam = yield* select(state => state.flycam);
const items = compactUpdateActions(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ function* createVolumeLayer(planeId: OrthoView): Saga<VolumeLayer> {
}

function* labelWithIterator(iterator, contourTracingMode): Saga<void> {
const allowUpdate = yield* select(state => state.tracing.restrictions.allowUpdate);
if (!allowUpdate) return;

const activeCellId = yield* select(state => enforceVolumeTracing(state.tracing).activeCellId);
const segmentationLayer = yield* call([Model, Model.getSegmentationLayer]);
const { cube } = segmentationLayer;
Expand All @@ -168,6 +171,9 @@ function* labelWithIterator(iterator, contourTracingMode): Saga<void> {
}

function* copySegmentationLayer(action: CopySegmentationLayerAction): Saga<void> {
const allowUpdate = yield* select(state => state.tracing.restrictions.allowUpdate);
if (!allowUpdate) return;

const activeViewport = yield* select(state => state.viewModeData.plane.activeViewport);
if (activeViewport === "TDView") {
// Cannot copy labels from 3D view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ class TracingActionsView extends PureComponent<Props, State> {
);
}

if (isSkeletonMode && restrictions.allowUpdate) {
if (restrictions.allowUpdate) {
elements.push(
<Menu.Item key="restore-button" onClick={this.handleRestore}>
<Icon type="bars" theme="outlined" />
Expand Down
43 changes: 27 additions & 16 deletions app/assets/javascripts/oxalis/view/version_list.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @flow
import * as React from "react";
import _ from "lodash";
import { Spin, List } from "antd";
import { List } from "antd";
import Store from "oxalis/store";
import { ControlModeEnum } from "oxalis/constants";
import Model from "oxalis/model";
Expand All @@ -27,10 +27,20 @@ type State = {
versions: Array<APIUpdateActionBatch>,
};

const VERSION_LIST_PLACEHOLDER = { emptyText: "No versions created yet." };

export async function previewVersion(versions?: Versions) {
const { tracingType, annotationId } = Store.getState().tracing;
await api.tracing.restart(tracingType, annotationId, ControlModeEnum.TRACE, versions);
Store.dispatch(setAnnotationAllowUpdateAction(false));

const segmentationLayer = Model.getSegmentationLayer();
const shouldPreviewVolumeVersion = versions != null && versions.volume != null;
const shouldPreviewNewestVersion = versions == null;
if (segmentationLayer != null && (shouldPreviewVolumeVersion || shouldPreviewNewestVersion)) {
segmentationLayer.cube.collectAllBuckets();
segmentationLayer.layerRenderingManager.refresh();
}
}

class VersionList extends React.Component<Props, State> {
Expand Down Expand Up @@ -80,21 +90,22 @@ class VersionList extends React.Component<Props, State> {
);

return (
<Spin spinning={this.state.isLoading}>
<List>
{filteredVersions.map((batch, index) => (
<VersionEntry
actions={batch.value}
version={batch.version}
isNewest={index === 0}
isActive={this.props.tracing.version === batch.version}
onRestoreVersion={this.restoreVersion}
onPreviewVersion={version => previewVersion({ [this.props.tracingType]: version })}
key={batch.version}
/>
))}
</List>
</Spin>
<List
dataSource={filteredVersions}
loading={this.state.isLoading}
locale={VERSION_LIST_PLACEHOLDER}
renderItem={(batch, index) => (
<VersionEntry
actions={batch.value}
version={batch.version}
isNewest={index === 0}
isActive={this.props.tracing.version === batch.version}
onRestoreVersion={this.restoreVersion}
onPreviewVersion={version => previewVersion({ [this.props.tracingType]: version })}
key={batch.version}
/>
)}
/>
);
}
}
Expand Down
3 changes: 1 addition & 2 deletions app/assets/javascripts/oxalis/view/version_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ class VersionView extends React.Component<Props, State> {
<VersionList tracingType="skeleton" tracing={this.props.tracing.skeleton} />
</TabPane>
) : null}
{/* "TODO: Enable after volume version restore was implemented" */}
{this.props.tracing.volume != null && false ? (
{this.props.tracing.volume != null ? (
<TabPane tab="Volume" key="volume">
<VersionList tracingType="volume" tracing={this.props.tracing.volume} />
</TabPane>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const StoreMock = {
dataSource,
},
datasetConfiguration: { fourBit: _fourBit },
tracing: {},
}),
dispatch: sinon.stub(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,21 @@ const createBranchPointAction = SkeletonTracingActions.createBranchPointAction(
test("SkeletonTracingSaga should create a tree if there is none (saga test)", t => {
const saga = saveTracingTypeAsync("skeleton");
expectValueDeepEqual(t, saga.next(), take("INITIALIZE_SKELETONTRACING"));
saga.next({ initSkeleton: true });
saga.next();
saga.next({ tracing: { trees: {} } });
t.is(saga.next(true).value.PUT.action.type, "CREATE_TREE");
});

test("SkeletonTracingSaga shouldn't do anything if unchanged (saga test)", t => {
const saga = saveTracingTypeAsync("skeleton");
expectValueDeepEqual(t, saga.next(), take("INITIALIZE_SKELETONTRACING"));
saga.next({ initSkeleton: true });
saga.next();
saga.next(initialState.tracing);
saga.next(false);
saga.next();
saga.next(true);
saga.next();
saga.next(true);
saga.next(initialState.tracing);
// only updateTracing
const items = execCall(t, saga.next(initialState.flycam));
Expand All @@ -138,12 +139,13 @@ test("SkeletonTracingSaga should do something if changed (saga test)", t => {

const saga = saveTracingTypeAsync("skeleton");
expectValueDeepEqual(t, saga.next(), take("INITIALIZE_SKELETONTRACING"));
saga.next({ initSkeleton: true });
saga.next();
saga.next(initialState.tracing);
saga.next(false);
saga.next();
saga.next(true);
saga.next();
saga.next(true);
saga.next(newState.tracing);
const items = execCall(t, saga.next(newState.flycam));
t.true(withoutUpdateTracing(items).length > 0);
Expand Down
6 changes: 4 additions & 2 deletions app/assets/javascripts/test/sagas/volumetracing_saga.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ const resetContourAction = VolumeTracingActions.resetContourAction();
test("VolumeTracingSaga shouldn't do anything if unchanged (saga test)", t => {
const saga = saveTracingTypeAsync("volume");
expectValueDeepEqual(t, saga.next(), take("INITIALIZE_VOLUMETRACING"));
saga.next({ initVolume: true });
saga.next();
saga.next(initialState.tracing);
saga.next();
saga.next(true);
saga.next();
saga.next(true);
saga.next(initialState.tracing);
// only updateTracing
const items = execCall(t, saga.next(initialState.flycam));
Expand All @@ -85,11 +86,12 @@ test("VolumeTracingSaga should do something if changed (saga test)", t => {

const saga = saveTracingTypeAsync("volume");
expectValueDeepEqual(t, saga.next(), take("INITIALIZE_VOLUMETRACING"));
saga.next({ initVolume: true });
saga.next();
saga.next(initialState.tracing);
saga.next();
saga.next(true);
saga.next();
saga.next(true);
saga.next(newState.tracing);
const items = execCall(t, saga.next(newState.flycam));
t.is(withoutUpdateTracing(items).length, 0);
Expand Down
2 changes: 1 addition & 1 deletion docs/tracing_ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The most common buttons are:
- `Download`: Starts the download of the current annotation. Skeleton annotations are downloaded as [NML](./data_formats.md#nml) files. Volume annotation downloads contain the raw segmentation data as [WKW](./data_formats.md#wkw) files.
- `Share`: Create a shareable link to your dataset containing the current position, rotation, zoom level etc. Use this to collaboratively work with colleagues. Read more about this feature in the [Sharing guide](./sharing.md).
- `Add Script`: Using the [webKnossos frontend API](https://demo.webknossos.org/assets/docs/frontend-api/index.html) users can interact with webKnossos programmatically. User scripts can be executed from here. Admins can add often used scripts to webKnossos to make them available to all users for easy access.
- `Restore Older Version`: Only available for skeleton tracings. Opens a view that shows all previous versions of a skeleton tracing. From this view, any older version can be selected, previewed, and restored.
- `Restore Older Version`: Opens a view that shows all previous versions of a tracing. From this view, any older version can be selected, previewed, and restored.
- `Import STL Mesh`: 3D Meshes can be imported into the current tracing view by uploading corresponding STL files. Read more information in [Mesh Visualization](#mesh-visualization).

A user can directly jump to positions within their datasets by entering them in the position input field.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ case class WebKnossosDataRequest(
position: Point3D,
zoomStep: Int,
cubeSize: Int,
fourBit: Option[Boolean]
fourBit: Option[Boolean],
version: Option[Long]
) extends AbstractDataRequest {

def cuboid(dataLayer: DataLayer) = Cuboid(
Expand All @@ -36,7 +37,7 @@ case class WebKnossosDataRequest(
cubeSize,
cubeSize)

def settings = DataServiceRequestSettings(halfByte = fourBit.getOrElse(false))
def settings = DataServiceRequestSettings(halfByte = fourBit.getOrElse(false), version)
}

object WebKnossosDataRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import java.nio.file.Path
import com.scalableminds.webknossos.datastore.models.BucketPosition
import com.scalableminds.webknossos.datastore.models.datasource.{DataLayer, DataSource, SegmentationLayer}

case class DataServiceRequestSettings(halfByte: Boolean)
case class DataServiceRequestSettings(halfByte: Boolean, version: Option[Long] = None)

object DataServiceRequestSettings {
val default = DataServiceRequestSettings(halfByte = false)
Expand All @@ -22,7 +22,8 @@ case class DataReadInstruction(
baseDir: Path,
dataSource: DataSource,
dataLayer: DataLayer,
bucket: BucketPosition
bucket: BucketPosition,
version: Option[Long] = None
) {
val cube = bucket.toCube(dataLayer.lengthOfUnderlyingCubes(bucket.resolution))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ class BinaryDataService @Inject()(config: DataStoreConfig) extends FoxImplicits
dataBaseDir,
request.dataSource,
request.dataLayer,
bucket)
bucket,
request.settings.version)

request.dataLayer.bucketProvider.load(readInstruction, cache, loadTimeout)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class VolumeTracingBucketProvider(layer: VolumeTracingLayer)
val volumeDataStore: FossilDBClient = layer.volumeDataStore

override def load(readInstruction: DataReadInstruction, cache: DataCubeCache, timeout: FiniteDuration)(implicit ec: ExecutionContext): Fox[Array[Byte]] = {
loadBucket(layer, readInstruction.bucket)
loadBucket(layer, readInstruction.bucket, readInstruction.version)
}

override def bucketStream(resolution: Int, version: Option[Long] = None): Iterator[(BucketPosition, Array[Byte])] = {
Expand Down
Loading

0 comments on commit be56b0b

Please sign in to comment.