Skip to content

Commit 16e64da

Browse files
Use new mesh API for animation job (#7692)
* updated animation job to use proxy mesh infromation for new mesh api * added ad-hoc meshes to animation job * pass fallback layer and adhoc meshing options to worker * upadted changelog * updated wk docs * apply PR feedback * apply PR feedback 2 * reverted colorLayerName to layerName * use tracing id for animations * remove view mode parameter from animations * also store mappingName for precomputed mesh info objects * fix animation modal error message for DS without color layers --------- Co-authored-by: Philipp Otto <[email protected]>
1 parent 5bdc46e commit 16e64da

File tree

11 files changed

+91
-61
lines changed

11 files changed

+91
-61
lines changed

CHANGELOG.unreleased.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
2525
- Changed some internal APIs to use spelling dataset instead of dataSet. This requires all connected datastores to be the latest version. [#7690](https://github.com/scalableminds/webknossos/pull/7690)
2626
- Toasts are shown until WEBKNOSSOS is running in the active browser tab again. Also, the content of most toasts that show errors or warnings is printed to the browser's console. [#7741](https://github.com/scalableminds/webknossos/pull/7741)
2727
- Improved UI speed when editing the description of an annotation. [#7769](https://github.com/scalableminds/webknossos/pull/7769)
28+
- Updated dataset animations to use the new meshing API. Animitation now support ad-hoc meshes and mappings. [#7692](https://github.com/scalableminds/webknossos/pull/7692)
29+
2830

2931
### Fixed
3032
- Fixed that the Command modifier on MacOS wasn't treated correctly for some shortcuts. Also, instead of the Alt key, the ⌥ key is shown as a hint in the status bar on MacOS. [#7659](https://github.com/scalableminds/webknossos/pull/7659)

app/controllers/JobsController.scala

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ case class AnimationJobOptions(
3434
layerName: String,
3535
boundingBox: BoundingBox,
3636
includeWatermark: Boolean,
37-
segmentationLayerName: Option[String],
38-
meshFileName: Option[String],
39-
meshSegmentIds: Array[Int],
37+
meshes: JsValue,
4038
movieResolution: MovieResolutionSetting.Value,
4139
cameraPosition: CameraPositionSetting.Value,
4240
intensityMin: Double,
@@ -406,19 +404,16 @@ class JobsController @Inject()(
406404
}
407405
layerName = animationJobOptions.layerName
408406
_ <- datasetService.assertValidLayerNameLax(layerName)
409-
_ <- Fox.runOptional(animationJobOptions.segmentationLayerName)(datasetService.assertValidLayerNameLax)
410407
exportFileName = s"webknossos_animation_${formatDateForFilename(new Date())}__${datasetName}__$layerName.mp4"
411408
command = JobCommand.render_animation
412409
commandArgs = Json.obj(
413410
"organization_name" -> organizationName,
414411
"dataset_name" -> datasetName,
415412
"export_file_name" -> exportFileName,
416413
"layer_name" -> animationJobOptions.layerName,
417-
"segmentation_layer_name" -> animationJobOptions.segmentationLayerName,
418414
"bounding_box" -> animationJobOptions.boundingBox.toLiteral,
419415
"include_watermark" -> animationJobOptions.includeWatermark,
420-
"mesh_segment_ids" -> animationJobOptions.meshSegmentIds,
421-
"meshfile_name" -> animationJobOptions.meshFileName,
416+
"meshes" -> animationJobOptions.meshes,
422417
"movie_resolution" -> animationJobOptions.movieResolution,
423418
"camera_position" -> animationJobOptions.cameraPosition,
424419
"intensity_min" -> animationJobOptions.intensityMin,

docs/animations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A picture is worth a thousand words. In this spirit, you can use WEBKNOSSOS to c
99
Creating an animation is easy:
1010

1111
1. Open any dataset or annotation that you want to use for your animation.
12-
2. Optionally, load any [pre-computed 3D meshes](./mesh_visualization.md#pre-computed-mesh-generation) for any segments that you wish to highlight.
12+
2. Optionally, load some [3D meshes](./mesh_visualization.md) for any segments that you wish to highlight.
1313
3. For larger datasets, use the bounding box tool to create a bounding box around your area of interest. Smaller datasets can be used in their entirety.
1414
4. From the `Menu` dropdown in navbar at the top of the screen, select "Create Animation".
1515
5. Configure the animation options as desired, i.e. camera movement or resolution.

frontend/javascripts/oxalis/model/actions/annotation_actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ export const addPrecomputedMeshAction = (
307307
seedPosition: Vector3,
308308
seedAdditionalCoordinates: AdditionalCoordinate[] | undefined | null,
309309
meshFileName: string,
310+
mappingName: string | null | undefined,
310311
) =>
311312
({
312313
type: "ADD_PRECOMPUTED_MESH",
@@ -315,6 +316,7 @@ export const addPrecomputedMeshAction = (
315316
seedPosition,
316317
seedAdditionalCoordinates,
317318
meshFileName,
319+
mappingName,
318320
}) as const;
319321

320322
export const setOthersMayEditForAnnotationAction = (othersMayEdit: boolean) =>

frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,14 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState {
321321
}
322322

323323
case "ADD_PRECOMPUTED_MESH": {
324-
const { layerName, segmentId, seedPosition, seedAdditionalCoordinates, meshFileName } =
325-
action;
324+
const {
325+
layerName,
326+
segmentId,
327+
seedPosition,
328+
seedAdditionalCoordinates,
329+
meshFileName,
330+
mappingName,
331+
} = action;
326332
const meshInfo: MeshInformation = {
327333
segmentId: segmentId,
328334
seedPosition,
@@ -331,6 +337,7 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState {
331337
isVisible: true,
332338
isPrecomputed: true,
333339
meshFileName,
340+
mappingName,
334341
};
335342
const additionalCoordinates = state.flycam.additionalCoordinates;
336343
const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates);

frontend/javascripts/oxalis/model/sagas/mesh_saga.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -746,8 +746,16 @@ function* loadPrecomputedMeshForSegmentId(
746746
segmentationLayer: APISegmentationLayer,
747747
): Saga<void> {
748748
const layerName = segmentationLayer.name;
749+
const mappingName = yield* call(getMappingName, segmentationLayer);
749750
yield* put(
750-
addPrecomputedMeshAction(layerName, id, seedPosition, seedAdditionalCoordinates, meshFileName),
751+
addPrecomputedMeshAction(
752+
layerName,
753+
id,
754+
seedPosition,
755+
seedAdditionalCoordinates,
756+
meshFileName,
757+
mappingName,
758+
),
751759
);
752760
yield* put(startedLoadingMeshAction(layerName, id));
753761
const dataset = yield* select((state) => state.dataset);
@@ -813,6 +821,18 @@ function* loadPrecomputedMeshForSegmentId(
813821
yield* put(finishedLoadingMeshAction(layerName, id));
814822
}
815823

824+
function* getMappingName(segmentationLayer: APISegmentationLayer) {
825+
const meshExtraInfo = yield* call(getMeshExtraInfo, segmentationLayer.name, null);
826+
const editableMapping = yield* select((state) =>
827+
getEditableMappingForVolumeTracingId(state, segmentationLayer.tracingId),
828+
);
829+
830+
// meshExtraInfo.mappingName contains the currently active mapping
831+
// (can be the id of an editable mapping). However, we always need to
832+
// use the mapping name of the on-disk mapping.
833+
return editableMapping != null ? editableMapping.baseMappingName : meshExtraInfo.mappingName;
834+
}
835+
816836
function* _getChunkLoadingDescriptors(
817837
id: number,
818838
dataset: APIDataset,
@@ -826,19 +846,14 @@ function* _getChunkLoadingDescriptors(
826846
const { segmentMeshController } = getSceneController();
827847
const version = meshFile.formatVersion;
828848
const { meshFileName } = meshFile;
829-
const meshExtraInfo = yield* call(getMeshExtraInfo, segmentationLayer.name, null);
830849

831850
const editableMapping = yield* select((state) =>
832851
getEditableMappingForVolumeTracingId(state, segmentationLayer.tracingId),
833852
);
834853
const tracing = yield* select((state) =>
835854
getTracingForSegmentationLayer(state, segmentationLayer),
836855
);
837-
const mappingName =
838-
// meshExtraInfo.mappingName contains the currently active mapping
839-
// (can be the id of an editable mapping). However, we always need to
840-
// use the mapping name of the on-disk mapping.
841-
editableMapping != null ? editableMapping.baseMappingName : meshExtraInfo.mappingName;
856+
const mappingName = yield* call(getMappingName, segmentationLayer);
842857

843858
if (version < 3) {
844859
console.warn("The active mesh file uses a version lower than 3, which is not supported");

frontend/javascripts/oxalis/store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,10 +544,10 @@ type BaseMeshInformation = {
544544
readonly seedAdditionalCoordinates?: AdditionalCoordinate[] | null;
545545
readonly isLoading: boolean;
546546
readonly isVisible: boolean;
547+
readonly mappingName: string | null | undefined;
547548
};
548549
export type AdHocMeshInformation = BaseMeshInformation & {
549550
readonly isPrecomputed: false;
550-
readonly mappingName: string | null | undefined;
551551
readonly mappingType: MappingType | null | undefined;
552552
};
553553
export type PrecomputedMeshInformation = BaseMeshInformation & {

frontend/javascripts/oxalis/view/action-bar/create_animation_modal.tsx

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getColorLayers,
1212
getEffectiveIntensityRange,
1313
getLayerByName,
14+
getResolutionInfo,
1415
is2dDataset,
1516
} from "oxalis/model/accessors/dataset_accessor";
1617
import {
@@ -24,6 +25,7 @@ import {
2425
MOVIE_RESOLUTIONS,
2526
APIDataLayer,
2627
APIJobType,
28+
APISegmentationLayer,
2729
} from "types/api_flow_types";
2830
import { InfoCircleOutlined } from "@ant-design/icons";
2931
import { PricingEnforcedSpan } from "components/pricing_enforcers";
@@ -33,8 +35,8 @@ import {
3335
} from "admin/organization/pricing_plan_utils";
3436
import { BoundingBoxType, Vector3 } from "oxalis/constants";
3537
import BoundingBox from "oxalis/model/bucket_data_handling/bounding_box";
36-
import { Model } from "oxalis/singletons";
3738
import { BoundingBoxSelection, LayerSelection } from "./starting_job_modals";
39+
import { getAdditionalCoordinatesAsString } from "oxalis/model/accessors/flycam_accessor";
3840

3941
type Props = {
4042
isOpen: boolean;
@@ -75,12 +77,19 @@ function selectMagForTextureCreation(
7577
return [bestMag, bestDifference];
7678
}
7779

78-
export function CreateAnimationModalWrapper(props: Props) {
80+
export default function CreateAnimationModalWrapper(props: Props) {
7981
const dataset = useSelector((state: OxalisState) => state.dataset);
8082

8183
// early stop if no color layer exists
8284
const colorLayers = getColorLayers(dataset);
83-
if (colorLayers.length === 0) return null;
85+
if (colorLayers.length === 0) {
86+
const { isOpen, onClose } = props;
87+
return (
88+
<Modal open={isOpen} onOk={onClose} onCancel={onClose} title="Create Animation">
89+
WEBKNOSSOS cannot create animations for datasets without color layers.
90+
</Modal>
91+
);
92+
}
8493

8594
return <CreateAnimationModal {...props} />;
8695
}
@@ -128,7 +137,7 @@ function CreateAnimationModal(props: Props) {
128137
const validateAnimationOptions = (
129138
colorLayer: APIDataLayer,
130139
selectedBoundingBox: BoundingBoxType,
131-
meshSegmentIds: number[],
140+
meshes: Partial<MeshInformation>[],
132141
) => {
133142
// Validate the select parameters and dataset to make sure it actually works and does not overload the server
134143

@@ -151,7 +160,7 @@ function CreateAnimationModal(props: Props) {
151160
!is2dDataset(state.dataset) && (colorLayer.additionalAxes?.length || 0) === 0;
152161
if (isDataset3D) errorMessages.push("Sorry, animations are only supported for 3D datasets.");
153162

154-
const isTooManyMeshes = meshSegmentIds.length > MAX_MESHES_PER_ANIMATION;
163+
const isTooManyMeshes = meshes.length > MAX_MESHES_PER_ANIMATION;
155164
if (isTooManyMeshes)
156165
errorMessages.push(
157166
`You selected too many meshes for the animation. Please keep the number of meshes below ${MAX_MESHES_PER_ANIMATION} to create an animation.`,
@@ -171,32 +180,33 @@ function CreateAnimationModal(props: Props) {
171180
(bb) => bb.id === selectedBoundingBoxId,
172181
)!.boundingBox;
173182

174-
// Submit currently visible pre-computed meshes
175-
let meshSegmentIds: number[] = [];
176-
let meshFileName: string | undefined;
177-
let segmentationLayerName: string | undefined;
178-
179-
const visibleSegmentationLayer = Model.getVisibleSegmentationLayer();
180-
181-
if (visibleSegmentationLayer) {
182-
const availableMeshes = state.localSegmentationData[visibleSegmentationLayer.name].meshes;
183-
if (availableMeshes == null) {
184-
throw new Error("There is no mesh data in localSegmentationData.");
185-
}
186-
meshSegmentIds = Object.values(availableMeshes as Record<number, MeshInformation>)
187-
.filter((mesh) => mesh.isVisible && mesh.isPrecomputed)
188-
.map((mesh) => mesh.segmentId);
189-
190-
const currentMeshFile =
191-
state.localSegmentationData[visibleSegmentationLayer.name].currentMeshFile;
192-
meshFileName = currentMeshFile?.meshFileName;
193-
194-
if (visibleSegmentationLayer.fallbackLayerInfo) {
195-
segmentationLayerName = visibleSegmentationLayer.fallbackLayerInfo.name;
196-
} else {
197-
segmentationLayerName = visibleSegmentationLayer.name;
198-
}
199-
}
183+
// Submit currently visible pre-computed & ad-hoc meshes
184+
const axis = getAdditionalCoordinatesAsString([]);
185+
const layerNames = Object.keys(state.localSegmentationData);
186+
const { preferredQualityForMeshAdHocComputation } = state.temporaryConfiguration;
187+
188+
const meshes: RenderAnimationOptions["meshes"] = layerNames.flatMap((layerName) => {
189+
const meshInfos = state.localSegmentationData[layerName]?.meshes?.[axis] || {};
190+
191+
const layer = getLayerByName(state.dataset, layerName) as APISegmentationLayer;
192+
const fullLayerName = layer.fallbackLayerInfo?.name || layerName;
193+
194+
const adhocMagIndex = getResolutionInfo(layer.resolutions).getClosestExistingIndex(
195+
preferredQualityForMeshAdHocComputation,
196+
);
197+
const adhocMag = layer.resolutions[adhocMagIndex];
198+
199+
return Object.values(meshInfos)
200+
.filter((meshInfo: MeshInformation) => meshInfo.isVisible)
201+
.flatMap((meshInfo: MeshInformation) => {
202+
return {
203+
layerName: fullLayerName,
204+
tracingId: layer.tracingId || null,
205+
adhocMag,
206+
...meshInfo,
207+
};
208+
});
209+
});
200210

201211
// Submit the configured min/max intensity info to support float datasets
202212
const [intensityMin, intensityMax] = getEffectiveIntensityRange(
@@ -209,9 +219,7 @@ function CreateAnimationModal(props: Props) {
209219

210220
const animationOptions: RenderAnimationOptions = {
211221
layerName: selectedColorLayerName,
212-
segmentationLayerName,
213-
meshFileName,
214-
meshSegmentIds,
222+
meshes,
215223
intensityMin,
216224
intensityMax,
217225
magForTextures,
@@ -221,7 +229,7 @@ function CreateAnimationModal(props: Props) {
221229
cameraPosition: selectedCameraPosition,
222230
};
223231

224-
if (!validateAnimationOptions(colorLayer, boundingBox, meshSegmentIds)) return;
232+
if (!validateAnimationOptions(colorLayer, boundingBox, meshes)) return;
225233

226234
startRenderAnimationJob(state.dataset.owningOrganization, state.dataset.name, animationOptions);
227235

@@ -338,7 +346,7 @@ function CreateAnimationModal(props: Props) {
338346
>
339347
Include the currently selected 3D meshes
340348
<Tooltip
341-
title="When enabled, all (pre-computed) meshes currently visible in WEBKNOSSOS will be included in the animation."
349+
title="When enabled, all meshes currently visible in WEBKNOSSOS will be included in the animation."
342350
placement="right"
343351
>
344352
<InfoCircleOutlined style={{ marginLeft: 10 }} />
@@ -401,5 +409,3 @@ function CreateAnimationModal(props: Props) {
401409
</Modal>
402410
);
403411
}
404-
405-
export default CreateAnimationModal;

frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ import UrlManager from "oxalis/controller/url_manager";
7474
import { withAuthentication } from "admin/auth/authentication_modal";
7575
import { PrivateLinksModal } from "./private_links_view";
7676
import { ItemType, SubMenuType } from "antd/lib/menu/hooks/useItems";
77-
import { CreateAnimationModalWrapper as CreateAnimationModal } from "./create_animation_modal";
77+
import CreateAnimationModal from "./create_animation_modal";
7878

7979
const AsyncButtonWithAuthentication = withAuthentication<AsyncButtonProps, typeof AsyncButton>(
8080
AsyncButton,

frontend/javascripts/oxalis/view/action-bar/view_dataset_actions_view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import Store, { OxalisState } from "oxalis/store";
2020
import { MenuItemType, SubMenuType } from "antd/lib/menu/hooks/useItems";
2121
import DownloadModalView from "./download_modal_view";
22-
import { CreateAnimationModalWrapper as CreateAnimationModal } from "./create_animation_modal";
22+
import CreateAnimationModal from "./create_animation_modal";
2323

2424
type Props = {
2525
layoutMenu: SubMenuType;

0 commit comments

Comments
 (0)