Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add delete segment data update action #7435

Merged
merged 14 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Added support for reading uint24 rgb layers in datasets with zarr2/zarr3/n5/neuroglancerPrecomputed format, as used for voxelytics predictions. [#7413](https://github.com/scalableminds/webknossos/pull/7413)
- Adding a remote dataset can now be done by providing a Neuroglancer URI. [#7416](https://github.com/scalableminds/webknossos/pull/7416)
- Added a filter to the Task List->Stats column to quickly filter for tasks with "Prending", "In-Progress" or "Finished" instances. [#7430](https://github.com/scalableminds/webknossos/pull/7430)
- The data of segments can now be deleted in the segment side panel. [#7435](https://github.com/scalableminds/webknossos/pull/7435)

### Changed
- An appropriate error is returned when requesting an API version that is higher that the current version. [#7424](https://github.com/scalableminds/webknossos/pull/7424)
Expand Down
16 changes: 16 additions & 0 deletions frontend/javascripts/oxalis/model/actions/volumetracing_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type SetLargestSegmentIdAction = ReturnType<typeof setLargestSegmentIdAct
export type SetSegmentsAction = ReturnType<typeof setSegmentsAction>;
export type UpdateSegmentAction = ReturnType<typeof updateSegmentAction>;
export type RemoveSegmentAction = ReturnType<typeof removeSegmentAction>;
export type DeleteSegmentDataAction = ReturnType<typeof deleteSegmentDataAction>;
export type SetSegmentGroupsAction = ReturnType<typeof setSegmentGroupsAction>;
export type SetMappingIsEditableAction = ReturnType<typeof setMappingIsEditableAction>;

Expand Down Expand Up @@ -80,6 +81,7 @@ export type VolumeTracingAction =
| SetSegmentsAction
| UpdateSegmentAction
| RemoveSegmentAction
| DeleteSegmentDataAction
| SetSegmentGroupsAction
| AddBucketToUndoAction
| ImportVolumeTracingAction
Expand Down Expand Up @@ -232,6 +234,20 @@ export const removeSegmentAction = (
timestamp,
} as const);

export const deleteSegmentDataAction = (
segmentId: number,
layerName: string,
callback?: () => void,
timestamp: number = Date.now(),
) =>
({
type: "DELETE_SEGMENT_DATA",
segmentId,
layerName,
callback,
timestamp,
} as const);

export const setSegmentGroupsAction = (
segmentGroups: Array<SegmentGroup>,
layerName: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,8 @@ export class DataBucket {
}
}
}

this.invalidateValueSet();
philippotto marked this conversation as resolved.
Show resolved Hide resolved
}

markAsPulled(): void {
Expand Down
10 changes: 10 additions & 0 deletions frontend/javascripts/oxalis/model/sagas/update_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type UpdateVolumeTracingUpdateAction = ReturnType<typeof updateVolumeTracing>;
export type CreateSegmentUpdateAction = ReturnType<typeof createSegmentVolumeAction>;
export type UpdateSegmentUpdateAction = ReturnType<typeof updateSegmentVolumeAction>;
export type DeleteSegmentUpdateAction = ReturnType<typeof deleteSegmentVolumeAction>;
export type DeleteSegmentDataUpdateAction = ReturnType<typeof deleteSegmentDataVolumeAction>;
type UpdateUserBoundingBoxesUpdateAction = ReturnType<typeof updateUserBoundingBoxes>;
export type UpdateBucketUpdateAction = ReturnType<typeof updateBucket>;
type UpdateSegmentGroupsUpdateAction = ReturnType<typeof updateSegmentGroups>;
Expand Down Expand Up @@ -63,6 +64,7 @@ export type UpdateAction =
| CreateSegmentUpdateAction
| UpdateSegmentUpdateAction
| DeleteSegmentUpdateAction
| DeleteSegmentDataUpdateAction
| UpdateBucketUpdateAction
| UpdateTreeVisibilityUpdateAction
| UpdateTreeEdgesVisibilityUpdateAction
Expand Down Expand Up @@ -346,6 +348,14 @@ export function deleteSegmentVolumeAction(id: number) {
},
} as const;
}
export function deleteSegmentDataVolumeAction(id: number) {
return {
name: "deleteSegmentData",
value: {
id,
},
} as const;
}
export function updateBucket(bucketInfo: SendBucketInfo, base64Data: string) {
return {
name: "updateBucket",
Expand Down
37 changes: 35 additions & 2 deletions frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import type {
ClickSegmentAction,
SetActiveCellAction,
CreateCellAction,
DeleteSegmentDataAction,
} from "oxalis/model/actions/volumetracing_actions";
import {
finishAnnotationStrokeAction,
Expand All @@ -79,7 +80,11 @@ import { select, take } from "oxalis/model/sagas/effect-generators";
import listenToMinCut from "oxalis/model/sagas/min_cut_saga";
import listenToQuickSelect from "oxalis/model/sagas/quick_select_saga";
import { takeEveryUnlessBusy } from "oxalis/model/sagas/saga_helpers";
import { UpdateAction, updateSegmentGroups } from "oxalis/model/sagas/update_actions";
import {
deleteSegmentDataVolumeAction,
UpdateAction,
updateSegmentGroups,
} from "oxalis/model/sagas/update_actions";
import {
createSegmentVolumeAction,
deleteSegmentVolumeAction,
Expand All @@ -90,7 +95,7 @@ import {
updateMappingName,
} from "oxalis/model/sagas/update_actions";
import VolumeLayer from "oxalis/model/volumetracing/volumelayer";
import { Model } from "oxalis/singletons";
import { Model, api } from "oxalis/singletons";
import type { Flycam, SegmentMap, VolumeTracing } from "oxalis/store";
import React from "react";
import { actionChannel, call, fork, put, takeEvery, takeLatest } from "typed-redux-saga";
Expand All @@ -101,6 +106,7 @@ import {
} from "./volume/helpers";
import maybeInterpolateSegmentationLayer from "./volume/volume_interpolation_saga";
import messages from "messages";
import { pushSaveQueueTransaction } from "../actions/save_actions";

export function* watchVolumeTracingAsync(): Saga<void> {
yield* take("WK_READY");
Expand Down Expand Up @@ -842,8 +848,35 @@ function* ensureValidBrushSize(): Saga<void> {
);
}

function* handleDeleteSegmentData(): Saga<void> {
yield* take("WK_READY");
while (true) {
const action = (yield* take("DELETE_SEGMENT_DATA")) as DeleteSegmentDataAction;

yield* put(setBusyBlockingInfoAction(true, "Segment is being deleted."));
yield* put(
pushSaveQueueTransaction(
[deleteSegmentDataVolumeAction(action.segmentId)],
"volume",
action.layerName,
),
);
yield* call([Model, Model.ensureSavedState]);

yield* call([api.data, api.data.reloadBuckets], action.layerName, (bucket) =>
bucket.containsValue(action.segmentId),
);

yield* put(setBusyBlockingInfoAction(false));
if (action.callback) {
action.callback();
}
}
}

export default [
editVolumeLayerAsync,
handleDeleteSegmentData,
ensureToolIsAllowedInResolution,
floodFill,
watchVolumeTracingAsync,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
VerticalAlignBottomOutlined,
EllipsisOutlined,
} from "@ant-design/icons";
import { List, Tooltip, Dropdown, MenuProps } from "antd";
import { List, Tooltip, Dropdown, MenuProps, Modal } from "antd";
import { useDispatch, useSelector } from "react-redux";
import Checkbox, { CheckboxChangeEvent } from "antd/lib/checkbox/Checkbox";
import React from "react";
Expand Down Expand Up @@ -192,6 +192,7 @@ type Props = {
createsNewUndoState: boolean,
) => void;
removeSegment: (arg0: number, arg2: string) => void;
deleteSegmentData: (arg0: number, arg2: string, callback?: () => void) => void;
onSelectSegment: (arg0: Segment) => void;
visibleSegmentationLayer: APISegmentationLayer | null | undefined;
loadAdHocMesh: (
Expand Down Expand Up @@ -373,6 +374,7 @@ function _SegmentListItem({
allowUpdate,
updateSegment,
removeSegment,
deleteSegmentData,
onSelectSegment,
visibleSegmentationLayer,
loadAdHocMesh,
Expand Down Expand Up @@ -489,6 +491,27 @@ function _SegmentListItem({
},
label: "Remove Segment From List",
},
{
key: "deleteSegmentData",
onClick: () => {
if (visibleSegmentationLayer == null) {
return;
}

Modal.confirm({
content: `Are you sure you want to delete the segment's data? This operation will set all voxels with id ${segment.id} to 0.`,
philippotto marked this conversation as resolved.
Show resolved Hide resolved
okText: "Yes, delete",
okType: "danger",
onOk: async () =>
new Promise<void>((resolve) =>
deleteSegmentData(segment.id, visibleSegmentationLayer.name, resolve),
),
});

andCloseContextMenu();
},
label: "Delete Segment's Data",
},
],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import { updateTemporarySettingAction } from "oxalis/model/actions/settings_acti
import {
batchUpdateGroupsAndSegmentsAction,
removeSegmentAction,
deleteSegmentDataAction,
setActiveCellAction,
updateSegmentAction,
} from "oxalis/model/actions/volumetracing_actions";
Expand Down Expand Up @@ -271,6 +272,10 @@ const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
removeSegment(segmentId: number, layerName: string) {
dispatch(removeSegmentAction(segmentId, layerName));
},

deleteSegmentData(segmentId: number, layerName: string, callback?: () => void) {
dispatch(deleteSegmentDataAction(segmentId, layerName, callback));
},
});

type DispatchProps = ReturnType<typeof mapDispatchToProps>;
Expand Down Expand Up @@ -1588,6 +1593,7 @@ class SegmentsView extends React.Component<Props, State> {
allowUpdate={this.props.allowUpdate}
updateSegment={this.props.updateSegment}
removeSegment={this.props.removeSegment}
deleteSegmentData={this.props.deleteSegmentData}
visibleSegmentationLayer={this.props.visibleSegmentationLayer}
loadAdHocMesh={this.props.loadAdHocMesh}
loadPrecomputedMesh={this.props.loadPrecomputedMesh}
Expand Down
5 changes: 5 additions & 0 deletions frontend/javascripts/oxalis/view/version_entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
MoveTreeComponentUpdateAction,
MergeTreeUpdateAction,
UpdateMappingNameUpdateAction,
DeleteSegmentDataUpdateAction,
} from "oxalis/model/sagas/update_actions";
import FormattedDate from "components/formatted_date";
import { MISSING_GROUP_ID } from "oxalis/view/right-border-tabs/tree_hierarchy_view_helpers";
Expand Down Expand Up @@ -167,6 +168,10 @@ const descriptionFns: Record<ServerUpdateAction["name"], (...args: any) => Descr
description: `Deleted the segment with id ${action.value.id} from the segments list.`,
icon: <DeleteOutlined />,
}),
deleteSegmentData: (action: DeleteSegmentDataUpdateAction): Description => ({
description: `Deleted the data of segment ${action.value.id}. All voxels with that id were overwritten with 0.`,
icon: <DeleteOutlined />,
}),
addSegmentIndex: (): Description => ({
description: "Added segment index to enable segment statistics.",
icon: <EditOutlined />,
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/test/controller/url_manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ test("UrlManager should build csv url hash and parse it again", (t) => {
t.deepEqual(UrlManager.parseUrlHash(), urlState);
});

test.only("UrlManager should build csv url hash with additional coordinates and parse it again", (t) => {
philippotto marked this conversation as resolved.
Show resolved Hide resolved
test("UrlManager should build csv url hash with additional coordinates and parse it again", (t) => {
const mode = Constants.MODE_ARBITRARY;
const urlState = {
position: [0, 0, 0] as Vector3,
Expand Down
6 changes: 4 additions & 2 deletions tools/assert-no-test-only.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/bin/bash
exit_code=0
echo "Checking for test.only() in test files."
! grep -r "test\.only(" frontend/javascripts/test || echo "Found test files with test.only() which disables other tests. Please remove the only modifier."
! grep -r "test\.serial\.only(" frontend/javascripts/test || echo "Found test files with test.only() which disables other tests. Please remove the only modifier."
! grep -r "test\.only(" frontend/javascripts/test || { echo "Found test files with test.only() which disables other tests. Please remove the only modifier."; exit_code=1; }
! grep -r "test\.serial\.only(" frontend/javascripts/test || { echo "Found test files with test.only() which disables other tests. Please remove the only modifier."; exit_code=1; }
echo "Done"
exit $exit_code
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ import com.scalableminds.webknossos.datastore.dataformats.wkw.WKWDataFormatHelpe
import com.scalableminds.webknossos.datastore.geometry.NamedBoundingBoxProto
import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits
import com.scalableminds.webknossos.datastore.models.DataRequestCollection.DataRequestCollection
import com.scalableminds.webknossos.datastore.models.datasource.{AdditionalAxis, ElementClass}
import com.scalableminds.webknossos.datastore.models.datasource.{AdditionalAxis, DataLayer, ElementClass}
import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing.ElementClassProto
import com.scalableminds.webknossos.datastore.models.requests.DataServiceDataRequest
import com.scalableminds.webknossos.datastore.models.{AdditionalCoordinate, BucketPosition, WebknossosAdHocMeshRequest}
import com.scalableminds.webknossos.datastore.models.{
AdditionalCoordinate,
BucketPosition,
UnsignedInteger,
UnsignedIntegerArray,
WebknossosAdHocMeshRequest
}
import com.scalableminds.webknossos.datastore.services._
import com.scalableminds.webknossos.tracingstore.tracings.TracingType.TracingType
import com.scalableminds.webknossos.tracingstore.tracings._
Expand Down Expand Up @@ -113,11 +119,11 @@ class VolumeTracingService @Inject()(
tracingFox.futureBox.flatMap {
case Full(tracing) =>
action match {
case action: UpdateBucketVolumeAction =>
case a: UpdateBucketVolumeAction =>
if (tracing.getMappingIsEditable) {
Fox.failure("Cannot mutate volume data in annotation with editable mapping.")
} else
updateBucket(tracingId, tracing, action, segmentIndexBuffer, updateGroup.version, userToken) ?~> "Failed to save volume data."
updateBucket(tracingId, tracing, a, segmentIndexBuffer, updateGroup.version, userToken) ?~> "Failed to save volume data."
case a: UpdateTracingVolumeAction =>
Fox.successful(
tracing.copy(
Expand All @@ -131,6 +137,11 @@ class VolumeTracingService @Inject()(
))
case a: RevertToVersionVolumeAction =>
revertToVolumeVersion(tracingId, a.sourceVersion, updateGroup.version, tracing)
case a: DeleteSegmentDataVolumeAction =>
if (!tracing.getHasSegmentIndex) {
Fox.failure("Cannot delete segment data for annotations without segment index.")
} else
deleteSegmentData(tracingId, tracing, a, segmentIndexBuffer, updateGroup.version) ?~> "Failed to delete segment data."
case _: UpdateTdCamera => Fox.successful(tracing)
case a: ApplyableVolumeAction => Fox.successful(a.applyOn(tracing))
case _ => Fox.failure("Unknown action.")
Expand Down Expand Up @@ -178,6 +189,45 @@ class VolumeTracingService @Inject()(
_ <- segmentIndexBuffer.flush()
} yield volumeTracing

private def deleteSegmentData(tracingId: String,
volumeTracing: VolumeTracing,
a: DeleteSegmentDataVolumeAction,
segmentIndexBuffer: VolumeSegmentIndexBuffer,
version: Long): Fox[VolumeTracing] =
for {
_ <- Fox.successful(())
dataLayer = volumeTracingLayer(tracingId, volumeTracing)
_ <- Fox.serialCombined(volumeTracing.resolutions.toList)(resolution => {
val mag = vec3IntFromProto(resolution)
for {
bucketPositionsRaw <- volumeSegmentIndexService
.getSegmentToBucketIndexWithEmptyFallbackWithoutBuffer(tracingId, a.id, mag)
bucketPositions = bucketPositionsRaw.values
.map(vec3IntFromProto)
.map(_ * mag * DataLayer.bucketLength)
.map(bp => BucketPosition(bp.x, bp.y, bp.z, mag, a.additionalCoordinates))
.toList
_ <- Fox.serialCombined(bucketPositions) {
bucketPosition =>
for {
data <- loadBucket(dataLayer, bucketPosition)
typedData = UnsignedIntegerArray.fromByteArray(data, volumeTracing.elementClass)
filteredData = typedData.map(elem =>
if (elem.toLong == a.id) UnsignedInteger.zeroFromElementClass(volumeTracing.elementClass) else elem)
filteredBytes = UnsignedIntegerArray.toByteArray(filteredData, volumeTracing.elementClass)
_ <- saveBucket(dataLayer, bucketPosition, filteredBytes, version)
_ <- updateSegmentIndex(segmentIndexBuffer,
bucketPosition,
filteredBytes,
Some(data),
volumeTracing.elementClass)
} yield ()
}
} yield ()
})
_ <- segmentIndexBuffer.flush()
} yield volumeTracing

private def assertMagIsValid(tracing: VolumeTracing, mag: Vec3Int): Fox[Unit] =
if (tracing.resolutions.nonEmpty) {
bool2Fox(tracing.resolutions.exists(r => vec3IntFromProto(r) == mag))
Expand Down
Loading