Skip to content

Commit

Permalink
Segment group batch actions (#7164)
Browse files Browse the repository at this point in the history
* add batch actions for segment groups
---------

Co-authored-by: Philipp Otto <[email protected]>
  • Loading branch information
dieknolle3333 and philippotto authored Jul 20, 2023
1 parent dc85d88 commit 9cecaab
Show file tree
Hide file tree
Showing 7 changed files with 519 additions and 80 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Added security.txt according to [RFC 9116](https://www.rfc-editor.org/rfc/rfc9116). The content is configurable and it can be disabled. [#7182](https://github.com/scalableminds/webknossos/pull/7182)
- Added tooltips to explain the task actions "Reset" and "Reset and Cancel". [#7201](https://github.com/scalableminds/webknossos/pull/7201)
- Thumbnail improvements: Thumbnails now use intensity configuration, thumbnails can now be created for float datasets, and they are cached across webknossos restarts. [#7190](https://github.com/scalableminds/webknossos/pull/7190)
- Added batch actions for segment groups, such as changing the color and loading or downloading all corresponding meshes. [#7164](https://github.com/scalableminds/webknossos/pull/7164).

### Changed
- Redesigned the info tab in the right-hand sidebar to be fit the new branding and design language. [#7110](https://github.com/scalableminds/webknossos/pull/7110)
Expand Down
8 changes: 4 additions & 4 deletions frontend/javascripts/admin/dataset/dataset_upload_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import Toast from "libs/toast";
import * as Utils from "libs/utils";
import messages from "messages";
import { trackAction } from "oxalis/model/helpers/analytics";
import { BlobReader, ZipReader, Entry } from "@zip.js/zip.js";
import Zip from "libs/zipjs_wrapper";
import {
CardContainer,
DatasetNameFormItem,
Expand Down Expand Up @@ -490,16 +490,16 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {

if (fileExtension === "zip") {
try {
const reader = new ZipReader(new BlobReader(file));
const reader = new Zip.ZipReader(new Zip.BlobReader(file));
const entries = await reader.getEntries();
await reader.close();
const wkwFile = entries.find((entry: Entry) =>
const wkwFile = entries.find((entry) =>
Utils.isFileExtensionEqualTo(entry.filename, "wkw"),
);
const needsConversion = wkwFile == null;
this.handleNeedsConversionInfo(needsConversion);

const nmlFile = entries.find((entry: Entry) =>
const nmlFile = entries.find((entry) =>
Utils.isFileExtensionEqualTo(entry.filename, "nml"),
);
if (nmlFile) {
Expand Down
13 changes: 13 additions & 0 deletions frontend/javascripts/libs/zipjs_wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as ZipType from "@zip.js/zip.js";

class TransFormStream {}

// Mock zip.js and TransformStream during tests
if (!global.window) {
// @ts-expect-error
global.TransformStream = TransFormStream;
}

const Zip = require("@zip.js/zip.js") as typeof ZipType;

export default Zip;
19 changes: 14 additions & 5 deletions frontend/javascripts/oxalis/model/actions/annotation_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ type DeleteUserBoundingBox = ReturnType<typeof deleteUserBoundingBoxAction>;
export type UpdateIsosurfaceVisibilityAction = ReturnType<typeof updateIsosurfaceVisibilityAction>;
export type MaybeFetchMeshFilesAction = ReturnType<typeof maybeFetchMeshFilesAction>;
export type TriggerIsosurfaceDownloadAction = ReturnType<typeof triggerIsosurfaceDownloadAction>;
export type TriggerIsosurfacesDownloadAction = ReturnType<typeof triggerIsosurfacesDownloadAction>;
export type RefreshIsosurfacesAction = ReturnType<typeof refreshIsosurfacesAction>;
export type RefreshIsosurfaceAction = ReturnType<typeof refreshIsosurfaceAction>;
export type StartedLoadingIsosurfaceAction = ReturnType<typeof startedLoadingIsosurfaceAction>;
export type FinishedLoadingIsosurfaceAction = ReturnType<typeof finishedLoadingIsosurfaceAction>;
export type UpdateMeshFileListAction = ReturnType<typeof updateMeshFileListAction>;
export type UpdateCurrentMeshFileAction = ReturnType<typeof updateCurrentMeshFileAction>;
export type ImportIsosurfaceFromStlAction = ReturnType<typeof importIsosurfaceFromStlAction>;
export type ImportIsosurfaceFromSTLAction = ReturnType<typeof importIsosurfaceFromSTLAction>;
export type RemoveIsosurfaceAction = ReturnType<typeof removeIsosurfaceAction>;
export type AddAdHocIsosurfaceAction = ReturnType<typeof addAdHocIsosurfaceAction>;
export type AddPrecomputedIsosurfaceAction = ReturnType<typeof addPrecomputedIsosurfaceAction>;
Expand Down Expand Up @@ -73,7 +74,7 @@ export type AnnotationActionTypes =
| FinishedLoadingIsosurfaceAction
| UpdateMeshFileListAction
| UpdateCurrentMeshFileAction
| ImportIsosurfaceFromStlAction
| ImportIsosurfaceFromSTLAction
| RemoveIsosurfaceAction
| AddAdHocIsosurfaceAction
| AddPrecomputedIsosurfaceAction
Expand Down Expand Up @@ -212,17 +213,25 @@ export const maybeFetchMeshFilesAction = (
} as const);

export const triggerIsosurfaceDownloadAction = (
cellName: string,
segmentName: string,
segmentId: number,
layerName: string,
) =>
({
type: "TRIGGER_ISOSURFACE_DOWNLOAD",
cellName,
segmentName,
segmentId,
layerName,
} as const);

export const triggerIsosurfacesDownloadAction = (
segmentsArray: Array<{ segmentName: string; segmentId: number; layerName: string }>,
) =>
({
type: "TRIGGER_ISOSURFACES_DOWNLOAD",
segmentsArray,
} as const);

export const refreshIsosurfacesAction = () =>
({
type: "REFRESH_ISOSURFACES",
Expand Down Expand Up @@ -266,7 +275,7 @@ export const updateCurrentMeshFileAction = (
meshFileName,
} as const);

export const importIsosurfaceFromStlAction = (layerName: string, buffer: ArrayBuffer) =>
export const importIsosurfaceFromSTLAction = (layerName: string, buffer: ArrayBuffer) =>
({
type: "IMPORT_ISOSURFACE_FROM_STL",
layerName,
Expand Down
71 changes: 57 additions & 14 deletions frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type { Action } from "oxalis/model/actions/actions";
import type { Vector3 } from "oxalis/constants";
import { MappingStatusEnum } from "oxalis/constants";
import {
ImportIsosurfaceFromStlAction,
ImportIsosurfaceFromSTLAction,
UpdateIsosurfaceVisibilityAction,
RemoveIsosurfaceAction,
RefreshIsosurfaceAction,
Expand All @@ -38,10 +38,11 @@ import {
addPrecomputedIsosurfaceAction,
finishedLoadingIsosurfaceAction,
startedLoadingIsosurfaceAction,
TriggerIsosurfacesDownloadAction,
} from "oxalis/model/actions/annotation_actions";
import type { Saga } from "oxalis/model/sagas/effect-generators";
import { select } from "oxalis/model/sagas/effect-generators";
import { actionChannel, takeEvery, call, take, race, put } from "typed-redux-saga";
import { actionChannel, takeEvery, call, take, race, put, all } from "typed-redux-saga";
import { stlIsosurfaceConstants } from "oxalis/view/right-border-tabs/segments_tab/segments_view";
import {
computeIsosurface,
Expand Down Expand Up @@ -73,6 +74,7 @@ import processTaskWithPool from "libs/task_pool";
import { getBaseSegmentationName } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper";
import { RemoveSegmentAction, UpdateSegmentAction } from "../actions/volumetracing_actions";
import { ResolutionInfo } from "../helpers/resolution_info";
import Zip from "libs/zipjs_wrapper";

export const NO_LOD_MESH_INDEX = -1;
const MAX_RETRY_COUNT = 5;
Expand Down Expand Up @@ -102,7 +104,7 @@ function marchingCubeSizeInMag1(): Vector3 {
: [128, 128, 128];
}
const modifiedCells: Set<number> = new Set();
export function isIsosurfaceStl(buffer: ArrayBuffer): boolean {
export function isIsosurfaceSTL(buffer: ArrayBuffer): boolean {
const dataView = new DataView(buffer);
const isIsosurface = stlIsosurfaceConstants.isosurfaceMarker.every(
(marker, index) => dataView.getUint8(index) === marker,
Expand Down Expand Up @@ -946,14 +948,7 @@ function* downloadIsosurfaceCellById(
}

try {
const stlDataViews = exportToStl(geometry);
// Encode isosurface and cell id property
const { isosurfaceMarker, segmentIdIndex } = stlIsosurfaceConstants;
isosurfaceMarker.forEach((marker, index) => {
stlDataViews[0].setUint8(index, marker);
});
stlDataViews[0].setUint32(segmentIdIndex, segmentId, true);
const blob = new Blob(stlDataViews);
const blob = getSTLBlob(geometry, segmentId);
yield* call(saveAs, blob, `${cellName}-${segmentId}.stl`);
} catch (exception) {
ErrorHandling.notify(exception as Error);
Expand All @@ -962,11 +957,58 @@ function* downloadIsosurfaceCellById(
}
}

function* downloadIsosurfaceCellsAsZIP(
segments: Array<{ segmentName: string; segmentId: number; layerName: string }>,
): Saga<void> {
const { segmentMeshController } = getSceneController();
const zipWriter = new Zip.ZipWriter(new Zip.BlobWriter("application/zip"));
try {
const addFileToZipWriterPromises = segments.map((element) => {
const geometry = segmentMeshController.getIsosurfaceGeometryInBestLOD(
element.segmentId,
element.layerName,
);

if (geometry == null) {
const errorMessage = messages["tracing.not_isosurface_available_to_download"];
Toast.error(errorMessage, {
sticky: false,
});
return;
}
const stlDataReader = new Zip.BlobReader(getSTLBlob(geometry, element.segmentId));
return zipWriter.add(`${element.segmentName}-${element.segmentId}.stl`, stlDataReader);
});
yield all(addFileToZipWriterPromises);
const result = yield* call([zipWriter, zipWriter.close]);
yield* call(saveAs, result as Blob, "mesh-export.zip");
} catch (exception) {
ErrorHandling.notify(exception as Error);
console.error(exception);
Toast.error("Could not export meshes as STL files. See console for details");
}
}

const getSTLBlob = (geometry: THREE.Group, segmentId: number): Blob => {
const stlDataViews = exportToStl(geometry);
// Encode isosurface and cell id property
const { isosurfaceMarker, segmentIdIndex } = stlIsosurfaceConstants;
isosurfaceMarker.forEach((marker, index) => {
stlDataViews[0].setUint8(index, marker);
});
stlDataViews[0].setUint32(segmentIdIndex, segmentId, true);
return new Blob(stlDataViews);
};

function* downloadIsosurfaceCell(action: TriggerIsosurfaceDownloadAction): Saga<void> {
yield* call(downloadIsosurfaceCellById, action.cellName, action.segmentId, action.layerName);
yield* call(downloadIsosurfaceCellById, action.segmentName, action.segmentId, action.layerName);
}

function* downloadIsosurfaceCells(action: TriggerIsosurfacesDownloadAction): Saga<void> {
yield* call(downloadIsosurfaceCellsAsZIP, action.segmentsArray);
}

function* importIsosurfaceFromStl(action: ImportIsosurfaceFromStlAction): Saga<void> {
function* importIsosurfaceFromSTL(action: ImportIsosurfaceFromSTLAction): Saga<void> {
const { layerName, buffer } = action;
const dataView = new DataView(buffer);
const segmentId = dataView.getUint32(stlIsosurfaceConstants.segmentIdIndex, true);
Expand Down Expand Up @@ -1035,7 +1077,8 @@ export default function* isosurfaceSaga(): Saga<void> {
yield* takeEvery(loadAdHocMeshActionChannel, loadAdHocIsosurfaceFromAction);
yield* takeEvery(loadPrecomputedMeshActionChannel, loadPrecomputedMesh);
yield* takeEvery("TRIGGER_ISOSURFACE_DOWNLOAD", downloadIsosurfaceCell);
yield* takeEvery("IMPORT_ISOSURFACE_FROM_STL", importIsosurfaceFromStl);
yield* takeEvery("TRIGGER_ISOSURFACES_DOWNLOAD", downloadIsosurfaceCells);
yield* takeEvery("IMPORT_ISOSURFACE_FROM_STL", importIsosurfaceFromSTL);
yield* takeEvery("REMOVE_ISOSURFACE", removeIsosurface);
yield* takeEvery("REMOVE_SEGMENT", handleRemoveSegment);
yield* takeEvery("REFRESH_ISOSURFACES", refreshIsosurfaces);
Expand Down
Loading

0 comments on commit 9cecaab

Please sign in to comment.