diff --git a/frontend/javascripts/messages.tsx b/frontend/javascripts/messages.tsx index e9e7dc996c0..855d6c22cab 100644 --- a/frontend/javascripts/messages.tsx +++ b/frontend/javascripts/messages.tsx @@ -503,4 +503,6 @@ instead. Only enable this option if you understand its effect. All layers will n `This feature is not available in your organization's plan. Ask the owner of your organization ${organizationOwnerName} to upgrade to a ${requiredPlan} plan or higher.`, "organization.plan.feature_not_available.owner": (requiredPlan: string) => `This feature is not available in your organization's plan. Consider upgrading to a ${requiredPlan} plan or higher.`, + "jobs.wrongNumberOfBoundingBoxes": + "To use the split/merger evaluation, make sure to have exactly one bounding box, either user-defined or from a task.", }; diff --git a/frontend/javascripts/viewer/controller/scene_controller.ts b/frontend/javascripts/viewer/controller/scene_controller.ts index 2bbad2759be..ecd9d887239 100644 --- a/frontend/javascripts/viewer/controller/scene_controller.ts +++ b/frontend/javascripts/viewer/controller/scene_controller.ts @@ -334,13 +334,18 @@ class SceneController { adding a new one. Since this function is executed very rarely, this is not a performance problem. */ - for (const [tracingId, boundingBox] of Object.entries(taskCubeByTracingId)) { + + // Clean up old entries + for (const [tracingId, _boundingBox] of Object.entries(this.taskCubeByTracingId)) { let taskCube = this.taskCubeByTracingId[tracingId]; - // Remove the old box if it exists if (taskCube != null) { taskCube.getMeshes().forEach((mesh) => this.rootNode.remove(mesh)); } this.taskCubeByTracingId[tracingId] = null; + } + // Add new entries + for (const [tracingId, boundingBox] of Object.entries(taskCubeByTracingId)) { + let taskCube = this.taskCubeByTracingId[tracingId]; if (boundingBox == null || Store.getState().task == null) { continue; } @@ -733,7 +738,7 @@ class SceneController { this.updateMeshesAccordingToLayerVisibility(), ), listenToStoreProperty( - (storeState) => getTaskBoundingBoxes(storeState.annotation), + (storeState) => getTaskBoundingBoxes(storeState), (boundingBoxesByTracingId) => this.updateTaskBoundingBoxes(boundingBoxesByTracingId), true, ), diff --git a/frontend/javascripts/viewer/model/accessors/skeletontracing_accessor.ts b/frontend/javascripts/viewer/model/accessors/skeletontracing_accessor.ts index 02937b11776..e273f8f1d4d 100644 --- a/frontend/javascripts/viewer/model/accessors/skeletontracing_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/skeletontracing_accessor.ts @@ -117,6 +117,10 @@ export function findTreeByNodeId(trees: TreeMap, nodeId: number): Tree | undefin return trees.values().find((tree) => tree.nodes.has(nodeId)); } +export function hasEmptyTrees(trees: TreeMap): boolean { + return trees.values().some((tree: Tree) => tree.nodes.size() === 0); +} + export function findTreeByName(trees: TreeMap, treeName: string): Tree | undefined { return trees.values().find((tree: Tree) => tree.name === treeName); } diff --git a/frontend/javascripts/viewer/model/accessors/tracing_accessor.ts b/frontend/javascripts/viewer/model/accessors/tracing_accessor.ts index 79a9c66f55c..b527d952c69 100644 --- a/frontend/javascripts/viewer/model/accessors/tracing_accessor.ts +++ b/frontend/javascripts/viewer/model/accessors/tracing_accessor.ts @@ -86,9 +86,10 @@ export function selectTracing( return tracing; } -function _getTaskBoundingBoxes(annotation: StoreAnnotation) { - const layers = _.compact([annotation.skeleton, ...annotation.volumes, annotation.readOnly]); - +function _getTaskBoundingBoxes(state: WebknossosState) { + const { annotation, task } = state; + if (task == null) return {}; + const layers = _.compact([annotation.skeleton, ...annotation.volumes]); return Object.fromEntries(layers.map((l) => [l.tracingId, l.boundingBox])); } diff --git a/frontend/javascripts/viewer/view/action-bar/starting_job_modals.tsx b/frontend/javascripts/viewer/view/action-bar/starting_job_modals.tsx index 264860f9929..c494ebd6fec 100644 --- a/frontend/javascripts/viewer/view/action-bar/starting_job_modals.tsx +++ b/frontend/javascripts/viewer/view/action-bar/starting_job_modals.tsx @@ -47,6 +47,7 @@ import { rgbToHex, } from "libs/utils"; import _ from "lodash"; +import messages from "messages"; import React, { useEffect, useState, useMemo } from "react"; import { useDispatch } from "react-redux"; import { type APIDataLayer, type APIJob, APIJobType, type VoxelSize } from "types/api_types"; @@ -56,7 +57,11 @@ import { getMagInfo, getSegmentationLayers, } from "viewer/model/accessors/dataset_accessor"; -import { getUserBoundingBoxesFromState } from "viewer/model/accessors/tracing_accessor"; +import { hasEmptyTrees } from "viewer/model/accessors/skeletontracing_accessor"; +import { + getTaskBoundingBoxes, + getUserBoundingBoxesFromState, +} from "viewer/model/accessors/tracing_accessor"; import { getActiveSegmentationTracingLayer, getReadableNameOfVolumeLayer, @@ -636,6 +641,19 @@ function CollapsibleSplitMergerEvaluationSettings({ children: ( +
+ You can evaluate splits/mergers on a given bounding box.
+ By default this is the user defined bounding box or the bounding box of a task.{" "} +
+ Thus your annotation should contain + +
state.dataset); const { neuronInferralCostPerGVx } = features(); - const hasSkeletonAnnotation = useWkSelector((state) => state.annotation.skeleton != null); + const skeletonAnnotation = useWkSelector((state) => state.annotation.skeleton); const dispatch = useDispatch(); const [doSplitMergerEvaluation, setDoSplitMergerEvaluation] = React.useState(false); + return ( dispatch(setAIJobModalStateAction("invisible"))} @@ -1099,6 +1118,31 @@ export function NeuronSegmentationForm() { doSplitMergerEvaluation, ); } + + const state = Store.getState(); + const userBoundingBoxCount = getUserBoundingBoxesFromState(state).length; + + if (userBoundingBoxCount > 1) { + Toast.error(messages["jobs.wrongNumberOfBoundingBoxes"]); + return; + } + + const taskBoundingBoxes = getTaskBoundingBoxes(state); + if (Object.values(taskBoundingBoxes).length + userBoundingBoxCount !== 1) { + Toast.error(messages["jobs.wrongNumberOfBoundingBoxes"]); + return; + } + + if (skeletonAnnotation == null || skeletonAnnotation.trees.size() === 0) { + Toast.error( + "Please ensure that a skeleton tree exists within the selected bounding box.", + ); + return; + } + if (hasEmptyTrees(skeletonAnnotation.trees)) { + Toast.error("Please ensure that all skeleton trees in this annotation have some nodes."); + return; + } return startNeuronInferralJob( dataset.id, colorLayer.name, @@ -1126,7 +1170,7 @@ export function NeuronSegmentationForm() { } jobSpecificInputFields={ - hasSkeletonAnnotation && ( + skeletonAnnotation != null && (