Skip to content

Don't label outside of layer's bbox #8602

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

Merged
merged 30 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f70c941
store containment info in buckets and respect that during applying of…
philippotto May 5, 2025
8ea2c96
fix brushing outside of ds bbox in w dimension
philippotto May 6, 2025
efa6b9a
clean up
philippotto May 6, 2025
07fdd6c
temporarily also render data outside of bbox for testing purposes (di…
philippotto May 6, 2025
c85b63b
improve performance of getOrCreateBucket by deferring containment check
philippotto May 6, 2025
491625b
improve performance
philippotto May 6, 2025
b971621
clean up
philippotto May 6, 2025
d0a3644
fix typing
philippotto May 6, 2025
940deda
extend tests
philippotto May 6, 2025
85595e5
don't use deprecated labeling code in data api
philippotto May 6, 2025
28bdc0e
fix tests
philippotto May 6, 2025
bc2fc6d
fix tests (again)
philippotto May 6, 2025
2464241
clean up (I tried to remove _labelVoxelInResolution_DEPRECATED comple…
philippotto May 6, 2025
76d6dc2
Merge branch 'master' into no-labeling-outside-bbox
philippotto May 6, 2025
aa12cc5
update changelog
philippotto May 6, 2025
fd526a7
code rabbit
philippotto May 6, 2025
fd87641
make containment requirement stronger
philippotto May 6, 2025
a41777c
incorporate feedback
philippotto May 8, 2025
050e46e
remove writable import
philippotto May 8, 2025
0496938
if the bbox is not mag aligned, use math.ceil for the bottom-right bo…
philippotto May 8, 2025
12e2e14
Merge branch 'master' of github.com:scalableminds/webknossos into no-…
philippotto May 14, 2025
624b85b
format
philippotto May 14, 2025
0b8dceb
add missing import
philippotto May 14, 2025
c48663c
remove temporary return false
philippotto May 14, 2025
f6a3ff4
fix typing
philippotto May 14, 2025
33f687c
fix that union bounding box of dataset was always used for segmentati…
philippotto May 16, 2025
ee65bc5
update docs
philippotto May 16, 2025
21121b0
use tracing.boundingBox if it exists
philippotto May 19, 2025
c356887
remove unused bbox fallback
philippotto May 19, 2025
d59c476
Merge branch 'master' into no-labeling-outside-bbox
MichaelBuessemeyer May 19, 2025
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 @@ -24,6 +24,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
### Fixed
- Fixed that layer bounding boxes were sometimes colored green even though this should only happen for tasks. [#8535](https://github.com/scalableminds/webknossos/pull/8535)
- Fixed that annotations could not be opened anymore (caused by #8535). [#8599](https://github.com/scalableminds/webknossos/pull/8599)
- Voxels outside of the layer bounding box cannot be brushed, anymore. [#8602](https://github.com/scalableminds/webknossos/pull/8602)

### Removed

Expand Down
3 changes: 3 additions & 0 deletions frontend/javascripts/libs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import type { BoundingBoxObject, NumberLike } from "oxalis/store";
import type { APIDataset, APIUser } from "types/api_types";
import type { ArbitraryObject, Comparator } from "types/globals";

export type Writeable<T> = { -readonly [P in keyof T]: T[P] };

type UrlParams = Record<string, string>;

// Fix JS modulo bug
// http://javascript.about.com/od/problemsolving/a/modulobug.htm
export function mod(x: number, n: number) {
Expand Down
151 changes: 124 additions & 27 deletions frontend/javascripts/oxalis/api/api_latest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import type { ToastStyle } from "libs/toast";
import Toast from "libs/toast";
import UserLocalStorage from "libs/user_local_storage";
import * as Utils from "libs/utils";
import { coalesce } from "libs/utils";
import { type Writeable, coalesce, mod } from "libs/utils";
import window, { location } from "libs/window";
import _ from "lodash";
import messages from "messages";
import type {
BoundingBoxType,
BucketAddress,
ControlMode,
LabeledVoxelsMap,
OrthoView,
TypedArray,
Vector3,
Expand Down Expand Up @@ -69,6 +70,7 @@ import {
} from "oxalis/model/accessors/skeletontracing_accessor";
import { AnnotationTool, type AnnotationToolId } from "oxalis/model/accessors/tool_accessor";
import {
enforceActiveVolumeTracing,
getActiveCellId,
getActiveSegmentationTracing,
getNameOfRequestedOrVisibleSegmentationLayer,
Expand Down Expand Up @@ -131,6 +133,7 @@ import {
type BatchableUpdateSegmentAction,
batchUpdateGroupsAndSegmentsAction,
clickSegmentAction,
finishAnnotationStrokeAction,
removeSegmentAction,
setActiveCellAction,
setSegmentGroupsAction,
Expand All @@ -139,6 +142,7 @@ import {
import BoundingBox from "oxalis/model/bucket_data_handling/bounding_box";
import type { Bucket, DataBucket } from "oxalis/model/bucket_data_handling/bucket";
import type DataLayer from "oxalis/model/data_layer";
import Dimensions from "oxalis/model/dimensions";
import dimensions from "oxalis/model/dimensions";
import { MagInfo } from "oxalis/model/helpers/mag_info";
import { parseNml } from "oxalis/model/helpers/nml_helpers";
Expand All @@ -148,10 +152,13 @@ import {
globalPositionToBucketPosition,
scaleGlobalPositionWithMagnification,
zoomedAddressToZoomedPosition,
zoomedPositionToZoomedAddress,
} from "oxalis/model/helpers/position_converter";
import { getConstructorForElementClass } from "oxalis/model/helpers/typed_buffer";
import { getMaximumGroupId } from "oxalis/model/reducers/skeletontracing_reducer_helpers";
import { getHalfViewportExtentsInUnitFromState } from "oxalis/model/sagas/saga_selectors";
import { applyLabeledVoxelMapToAllMissingMags } from "oxalis/model/sagas/volume/helpers";
import { applyVoxelMap } from "oxalis/model/volumetracing/volume_annotation_sampling";
import { Model, api } from "oxalis/singletons";
import type {
DatasetConfiguration,
Expand Down Expand Up @@ -412,7 +419,8 @@ class TracingApi {
* @example
* const comment = api.tracing.getCommentForNode(23);
*
* @example // Provide a tree for lookup speed boost
* @example
* // Provide a tree for lookup speed boost
* const comment = api.tracing.getCommentForNode(23, api.getActiveTreeid());
*/
getCommentForNode(nodeId: number, treeId?: number): string | null | undefined {
Expand Down Expand Up @@ -1814,14 +1822,17 @@ class DataApi {
* If the zoom level is provided and points to a not existent mag,
* 0 will be returned.
*
* @example // Return the greyscale value for a bucket
* @example
* // Return the greyscale value for a bucket
* const position = [123, 123, 123];
* api.data.getDataValue("binary", position).then((greyscaleColor) => ...);
*
* @example // Using the await keyword instead of the promise syntax
* @example
* // Using the await keyword instead of the promise syntax
* const greyscaleColor = await api.data.getDataValue("binary", position);
*
* @example // Get the segmentation id for the first volume tracing layer
* @example
* // Get the segmentation id for the first volume tracing layer
* const segmentId = await api.data.getDataValue(api.data.getVolumeTracingLayerIds()[0], position);
*/
async getDataValue(
Expand Down Expand Up @@ -1995,7 +2006,7 @@ class DataApi {
bucketPositionToGlobalAddress(bucketAddress, new MagInfo(magnifications));

const nextBucketInDim = (bucket: BucketAddress, dim: 0 | 1 | 2) => {
const copy = bucket.slice() as BucketAddress;
const copy = bucket.slice() as Writeable<BucketAddress>;
copy[dim]++;
return copy;
};
Expand Down Expand Up @@ -2120,7 +2131,8 @@ class DataApi {
* Downloads a cuboid of raw data from a dataset (not tracing) layer. A new window is opened for the download -
* if that is not the case, please check your pop-up blocker.
*
* @example // Download a cuboid (from (0, 0, 0) to (100, 200, 100)) of raw data from the "segmentation" layer.
* @example
* // Download a cuboid (from (0, 0, 0) to (100, 200, 100)) of raw data from the "segmentation" layer.
* api.data.downloadRawDataCuboid("segmentation", [0,0,0], [100,200,100]);
*/
downloadRawDataCuboid(layerName: string, topLeft: Vector3, bottomRight: Vector3): Promise<void> {
Expand Down Expand Up @@ -2156,32 +2168,115 @@ class DataApi {
}

/**
* Label voxels with the supplied value. Note that this method does not mutate
* the data immediately, but instead returns a promise (since the data might
* have to be downloaded first).
* Label voxels with the supplied value. This method behaves as if the user
* had brushed the provided voxels all at once. If the volume data wasn't
* downloaded completely yet, WEBKNOSSOS will merge the data as soon as it
* was downloaded.
*
* _Volume tracing only!_
*
* @example // Set the segmentation id for some voxels to 1337
* await api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
* @example
* // Set the segmentation id for some voxels to 1337
* api.data.labelVoxels([[1,1,1], [1,2,1], [2,1,1], [2,2,1]], 1337);
*/
async labelVoxels(
voxels: Array<Vector3>,
labelVoxels(
globalPositionsMag1: Vector3[],
label: number,
additionalCoordinates: AdditionalCoordinate[] | null = null,
): Promise<void> {
assertVolume(Store.getState());
const segmentationLayer = this.model.getEnforcedSegmentationTracingLayer();
await Promise.all(
voxels.map((voxel) =>
segmentationLayer.cube._labelVoxelInAllResolutions_DEPRECATED(
voxel,
optAdditionalCoordinates?: AdditionalCoordinate[] | null,
) {
const state = Store.getState();
const allowUpdate = state.annotation.restrictions.allowUpdate;
const additionalCoordinates =
optAdditionalCoordinates === undefined
? state.flycam.additionalCoordinates
: optAdditionalCoordinates;
if (!allowUpdate || globalPositionsMag1.length === 0) return;

const volumeTracing = enforceActiveVolumeTracing(state);
const segmentationLayer = Model.getSegmentationTracingLayer(volumeTracing.tracingId);
const { cube } = segmentationLayer;
const magInfo = getMagInfo(segmentationLayer.mags);
const labeledZoomStep = magInfo.getClosestExistingIndex(0);
const labeledMag = magInfo.getMagByIndexOrThrow(labeledZoomStep);
const dimensionIndices = Dimensions.getIndices(OrthoViews.PLANE_XY);
const thirdDim = dimensionIndices[2];

const globalPositions = globalPositionsMag1.map(
(pos): Vector3 => [
Math.floor(pos[0] / labeledMag[0]),
Math.floor(pos[1] / labeledMag[1]),
Math.floor(pos[2] / labeledMag[2]),
],
);
const groupedByW = _.groupBy(globalPositions, (pos) => pos[thirdDim]);

for (const [wValueStr, group] of Object.entries(groupedByW)) {
const w = Number.parseInt(wValueStr, 10);
const currentLabeledVoxelMap: LabeledVoxelsMap = new Map();

for (const pos of group) {
const bucketZoomedAddress = zoomedPositionToZoomedAddress(
pos,
labeledZoomStep,
additionalCoordinates,
label,
),
);

let labelMap = currentLabeledVoxelMap.get(bucketZoomedAddress);
if (!labelMap) {
labelMap = new Uint8Array(Constants.BUCKET_WIDTH ** 2);
currentLabeledVoxelMap.set(bucketZoomedAddress, labelMap);
}

const a = pos[dimensionIndices[0]];
const b = pos[dimensionIndices[1]];
const localA = mod(a, Constants.BUCKET_WIDTH);
const localB = mod(b, Constants.BUCKET_WIDTH);
labelMap[localA * Constants.BUCKET_WIDTH + localB] = 1;
}

const numberOfSlices = 1;
applyVoxelMap(
currentLabeledVoxelMap,
cube,
label,
(x, y, out) => {
out[0] = x;
out[1] = y;
out[2] = w;
},
numberOfSlices,
thirdDim,
true,
0,
);

const thirdDimensionOfSlice = w * labeledMag[thirdDim];

applyLabeledVoxelMapToAllMissingMags(
currentLabeledVoxelMap,
labeledZoomStep,
dimensionIndices,
magInfo,
cube,
label,
thirdDimensionOfSlice,
true,
0,
);
}

Store.dispatch(
updateSegmentAction(
label,
{
somePosition: globalPositionsMag1[0],
someAdditionalCoordinates: additionalCoordinates || undefined,
},
volumeTracing.tracingId,
),
);
segmentationLayer.cube.pushQueue.push();

Store.dispatch(finishAnnotationStrokeAction(volumeTracing.tracingId));
}

/**
Expand Down Expand Up @@ -2741,7 +2836,8 @@ class UtilsApi {
/**
* Wait for some milliseconds before continuing the control flow.
*
* @example // Wait for 5 seconds
* @example
* // Wait for 5 seconds
* await api.utils.sleep(5000);
*/
sleep(milliseconds: number): Promise<void> {
Expand All @@ -2756,7 +2852,8 @@ class UtilsApi {
* @param {string} type - Can be one of the following: "info", "warning", "success" or "error"
* @param {string} message - The message string you want to show
* @param {number} timeout - Time period in milliseconds after which the toast will be hidden. Time is measured as soon as the user moves the mouse. A value of 0 means that the toast will only hide by clicking on it's X button.
* @example // Show a toast for 5 seconds
* @example
* // Show a toast for 5 seconds
* const removeToast = api.utils.showToast("info", "You just got toasted", false, 5000);
* // ... optionally:
* // removeToast();
Expand Down
7 changes: 6 additions & 1 deletion frontend/javascripts/oxalis/api/wk_dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { V3 } from "libs/mjs";
import { roundTo, sleep } from "libs/utils";
import _ from "lodash";
import { type OrthoView, OrthoViews, type Vector3 } from "oxalis/constants";
import { Store } from "oxalis/singletons";
import { Model, Store } from "oxalis/singletons";
import type { ApiInterface } from "./api_latest";
import type ApiLoader from "./api_loader";

Expand Down Expand Up @@ -59,6 +59,11 @@ export default class WkDev {
return Store;
}

public get model() {
/* Access to the model */
return Model;
}

public get api() {
/* Access to the API (will fail if API is not initialized yet). */
if (this._api == null) {
Expand Down
14 changes: 7 additions & 7 deletions frontend/javascripts/oxalis/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export type NestedMatrix4 = [Vector4, Vector4, Vector4, Vector4]; // Represents
// For 3D data BucketAddress = x, y, z, mag
// For higher dimensional data, BucketAddress = x, y, z, mag, [{name: "t", value: t}, ...]
export type BucketAddress =
| Vector4
| [number, number, number, number, AdditionalCoordinate[] | null];
| readonly [number, number, number, number]
| readonly [number, number, number, number, AdditionalCoordinate[] | null];

export type Point2 = {
x: number;
Expand Down Expand Up @@ -49,11 +49,11 @@ export type Rect = {
height: number;
};
export const AnnotationContentTypes = ["skeleton", "volume", "hybrid"];
export const Vector2Indicies = [0, 1];
export const Vector3Indicies = [0, 1, 2];
export const Vector4Indicies = [0, 1, 2, 3];
export const Vector5Indicies = [0, 1, 2, 3, 4];
export const Vector6Indicies = [0, 1, 2, 3, 4, 5];
export const Vector2Indicies = [0, 1] as const;
export const Vector3Indicies = [0, 1, 2] as const;
export const Vector4Indicies = [0, 1, 2, 3] as const;
export const Vector5Indicies = [0, 1, 2, 3, 4] as const;
export const Vector6Indicies = [0, 1, 2, 3, 4, 5] as const;
export enum OrthoViews {
PLANE_XY = "PLANE_XY",
PLANE_YZ = "PLANE_YZ",
Expand Down
Loading
Loading