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 bounding box tool #5767

Merged
merged 41 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
09506bd
Add bounding box tool to tollbar (UI only)
MichaelBuessemeyer Oct 1, 2021
6680637
WIP bbox tool
MichaelBuessemeyer Oct 5, 2021
35ee289
add hit planes to check for bbox crosssection hovering
MichaelBuessemeyer Oct 7, 2021
b87ebe7
add manual bbox hovering check
MichaelBuessemeyer Oct 8, 2021
49f9d00
Use own hovered hitbox detection and enable bbox resizing
MichaelBuessemeyer Oct 14, 2021
2830eb7
Remove unused code, fix linting and flow
MichaelBuessemeyer Oct 14, 2021
1c11645
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Oct 14, 2021
fa0c76a
fix tests
MichaelBuessemeyer Oct 14, 2021
e3e8b94
add new bbox on left click,
MichaelBuessemeyer Oct 15, 2021
299ece4
add snapping to resizing bounding boxes
MichaelBuessemeyer Oct 15, 2021
51f2d31
add new bbox button to toolbar and context menu entry
MichaelBuessemeyer Oct 18, 2021
b30d229
reenable cursor chaning on hovered bbox
MichaelBuessemeyer Oct 18, 2021
8523355
fix distored bounding box hit box
MichaelBuessemeyer Oct 18, 2021
b0dfada
fix linting
MichaelBuessemeyer Oct 18, 2021
93ecb91
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Oct 20, 2021
690fe84
various changes
MichaelBuessemeyer Oct 20, 2021
c25fe65
add jump to bbox button and auto saving new bounding boxes
MichaelBuessemeyer Oct 20, 2021
5b4ba2a
add corner dragging
MichaelBuessemeyer Oct 20, 2021
9e1a0cf
add delete item to context menu
MichaelBuessemeyer Oct 20, 2021
57cfc8b
add hide bounding box to context menu
MichaelBuessemeyer Oct 20, 2021
a0ea72d
add change name and change color of bbox to context menu
MichaelBuessemeyer Oct 21, 2021
9aefac9
WIP: Adding BBox undo/redo
MichaelBuessemeyer Oct 21, 2021
1ca5f27
add bounding box undo / redo
MichaelBuessemeyer Oct 22, 2021
7c3ce6d
fix flow
MichaelBuessemeyer Oct 22, 2021
a4598ed
refactor code
MichaelBuessemeyer Oct 22, 2021
d2e49a6
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Oct 25, 2021
5514ba1
fix tests
MichaelBuessemeyer Oct 25, 2021
da41fe6
Apply a lot of feedback
MichaelBuessemeyer Nov 1, 2021
2a5ebdf
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Nov 1, 2021
9e31ef2
fix cyclic imports
MichaelBuessemeyer Nov 1, 2021
82c2ce9
fix little bug caused by refactoring
MichaelBuessemeyer Nov 4, 2021
b4bcfb3
add changelog entry
MichaelBuessemeyer Nov 4, 2021
9fb29e0
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Nov 4, 2021
bd08c5f
Apply suggestions from code review
MichaelBuessemeyer Nov 6, 2021
6c95d89
make distance to bounding box dimension independent
MichaelBuessemeyer Nov 6, 2021
86dfab0
WIP fix tool deselection saga
MichaelBuessemeyer Nov 6, 2021
02496cd
add tests to test annotation tool deselection
MichaelBuessemeyer Nov 6, 2021
f317fd6
small fixes
MichaelBuessemeyer Nov 6, 2021
61fff90
Merge branch 'add-bounding-box-tool' of github.com:scalableminds/webk…
MichaelBuessemeyer Nov 6, 2021
1f0e75b
Merge branch 'master' of github.com:scalableminds/webknossos into add…
MichaelBuessemeyer Nov 14, 2021
d4372a8
Remove the param previous tool from annotation tool setting actions
MichaelBuessemeyer Nov 14, 2021
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
2 changes: 1 addition & 1 deletion frontend/javascripts/libs/window.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
export const alert = typeof window === "undefined" ? console.log.bind(console) : window.alert;
export const document =
typeof window === "undefined" || !window.document
? { getElementById: () => null }
? { getElementById: () => null, body: null }
: window.document;

// See https://github.com/facebook/flow/blob/master/lib/bom.js#L294-L311
Expand Down
10 changes: 9 additions & 1 deletion frontend/javascripts/oxalis/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export const AnnotationToolEnum = {
ERASE_TRACE: "ERASE_TRACE",
FILL_CELL: "FILL_CELL",
PICK_CELL: "PICK_CELL",
BOUNDING_BOX: "BOUNDING_BOX",
};
export const VolumeTools = [
AnnotationToolEnum.BRUSH,
Expand Down Expand Up @@ -232,7 +233,14 @@ export type LabeledVoxelsMap = Map<Vector4, Uint8Array>;
// e.g., z in XY viewport).
export type LabelMasksByBucketAndW = Map<Vector4, Map<number, Uint8Array>>;

export type ShowContextMenuFunction = (number, number, ?number, Vector3, OrthoView) => void;
export type ShowContextMenuFunction = (
number,
number,
?number,
?number,
Vector3,
OrthoView,
) => void;

const Constants = {
ARBITRARY_VIEW: 4,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
// @flow
import { calculateGlobalPos } from "oxalis/model/accessors/view_mode_accessor";
import _ from "lodash";
import type { OrthoView, Point2, Vector3, BoundingBoxType } from "oxalis/constants";
import Store from "oxalis/store";
import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor";
import Dimension, { type DimensionMap, type DimensionIndices } from "oxalis/model/dimensions";
import { changeUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions";
import { getBaseVoxelFactors } from "oxalis/model/scaleinfo";
import getSceneController from "oxalis/controller/scene_controller_provider";
import { document } from "libs/window";

const BOUNDING_BOX_HOVERING_THROTTLE_TIME = 100;

const getNeighbourEdgeIndexByEdgeIndex = {
// The edges are indexed within the plane like this:
// See the distanceArray calculation as a reference.
// +---0---+
// | |
// 2 3
// | |
// +---1---+
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
//
"0": [2, 3],
"1": [2, 3],
"2": [0, 1],
"3": [0, 1],
};
// This value is in "mouse tolerance to trigger a selection". It is in "unzoomed worldcoordinates".
const MAX_DISTANCE_TO_SELECTION = 15;
philippotto marked this conversation as resolved.
Show resolved Hide resolved

function getDistanceToBoundingBoxEdge(
pos: Vector3,
min: Vector3,
max: Vector3,
compareToMin: boolean,
primaryAndSecondaryDim: [DimensionIndices, DimensionIndices],
planeRatio: Vector3,
) {
// This method calculates the distance between the given pos and a single edge of a cross section
// of bounding box given min and max that is displayed in a certain viewport.
// The edge dimension that the edge extents along is given by the first entry of primaryAndSecondaryDim.
// Here goes 0 = x direction, 1 = y direction and 2 = z direction.
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
// The second entry of primaryAndSecondaryDim gives the other extend of the viewport / cross section.
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
// The boolean compareToMin tells which of the two edges in the primary direction should be compared with.
// To calculate the distance there are three cases depending on the primary dimension value of pos.
// - One when the given pos is on the left of the minimum of the edge (case 1),
// - one if pos is on the right of the maximum of the edge (case 2)
// - and the last if pos is inbeween the minimum and maximum of the edge (case 3).
// If the cross section is on the xy viewport and the edge to compare to is the min edge in x direction
// an example for the different cases is: compareToMin = true, primaryAndSecondaryDim = [0,1] = [x,y]
// ---> x
// | min.x max.x
// ↓ y ↓ ↓
// ↓ ↓
// case 1 ↓ case 3 case 3 ↓ case 2
// '. | | .'
// '↘ ↓ ↓ ↙'
// --------------------------------------------------------
// | |
// | edge extends along x direction -> primary dim = x |
// | |

// As the planeRatio is multiplied to the global coordinates passed to this method,
// the distance between the mouse and the bounding box is distorted by the factor of planeRatio.
// That's why we later divide exactly by this factor to let the hit box / distance
// between the mouse and bounding box be the same in each dimension.
const [primaryEdgeDim, secondaryprimaryEdgeDim] = primaryAndSecondaryDim;
const cornerToCompareWith = compareToMin ? min : max;
if (pos[primaryEdgeDim] < min[primaryEdgeDim]) {
// Case 1: Distance to the min corner is needed in primaryEdgeDim.
return (
Math.hypot(
pos[primaryEdgeDim] - min[primaryEdgeDim],
pos[secondaryprimaryEdgeDim] - cornerToCompareWith[secondaryprimaryEdgeDim],
) / planeRatio[primaryEdgeDim]
);
}
if (pos[primaryEdgeDim] > max[primaryEdgeDim]) {
// Case 2: Distance to max Corner is needed in primaryEdgeDim.
return (
Math.hypot(
pos[primaryEdgeDim] - max[primaryEdgeDim],
pos[secondaryprimaryEdgeDim] - cornerToCompareWith[secondaryprimaryEdgeDim],
) / planeRatio[primaryEdgeDim]
);
}
// Case 3:
// If the position is within the bounds of the primaryEdgeDim, the shortest distance
// to the edge is simply the difference between the secondaryprimaryEdgeDim values.
return (
Math.abs(pos[secondaryprimaryEdgeDim] - cornerToCompareWith[secondaryprimaryEdgeDim]) /
planeRatio[primaryEdgeDim]
);
}

export type SelectedEdge = {
boxId: number,
direction: "horizontal" | "vertical",
isMaxEdge: boolean,
edgeId: number,
resizableDimension: 0 | 1 | 2,
};

type DistanceArray = [number, number, number, number];

function computeDistanceArray(
boundingBoxBounds: BoundingBoxType,
globalPosition: Vector3,
indices: DimensionMap,
planeRatio: Vector3,
): DistanceArray {
const { min, max } = boundingBoxBounds;
const distanceArray = [0, 1, 2, 3].map(edgeId => {
const direction = edgeId < 2 ? "horizontal" : "vertical";
const isMaxEdge = edgeId % 2 === 1;
const primaryAndSecondaryDim =
direction === "horizontal" ? [indices[0], indices[1]] : [indices[1], indices[0]];
return getDistanceToBoundingBoxEdge(
globalPosition,
min,
max,
!isMaxEdge,
primaryAndSecondaryDim,
planeRatio,
);
});
return ((distanceArray: any): DistanceArray);
}

// Return the edge or edges of the bounding box closest to the mouse position if their distance is below a certain threshold.
// If no edge is close to the mouse null is returned instead. Otherwise the first entry is always the closest edge.
// If the mouse near a corner, there is always an additional edge that is close to the mouse.
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
// If such an edge exists then this edge is the second entry of the array.
// If the mouse isn't close to a corner of a crossection, the second entry is null.
export function getClosestHoveredBoundingBox(
pos: Point2,
plane: OrthoView,
): [SelectedEdge, ?SelectedEdge] | null {
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
const state = Store.getState();
const globalPosition = calculateGlobalPos(state, pos, plane);
const { userBoundingBoxes } = getSomeTracing(state.tracing);
const indices = Dimension.getIndices(plane);
const planeRatio = getBaseVoxelFactors(state.dataset.dataSource.scale);
const thirdDim = indices[2];

const zoomedMaxDistanceToSelection = MAX_DISTANCE_TO_SELECTION * state.flycam.zoomStep;
let currentNearestDistance = zoomedMaxDistanceToSelection;
let currentNearestBoundingBox = null;
let currentNearestDistanceArray = null;

for (const bbox of userBoundingBoxes) {
const { min, max } = bbox.boundingBox;
const isCrossSectionOfViewportVisible =
globalPosition[thirdDim] >= min[thirdDim] && globalPosition[thirdDim] < max[thirdDim];
if (!isCrossSectionOfViewportVisible) {
continue;
}
// In getNeighbourEdgeIndexByEdgeIndex is a visualization
// of how the indices of the array map to the visible bbox edges.
const distanceArray = computeDistanceArray(
bbox.boundingBox,
globalPosition,
indices,
planeRatio,
);
const minimumDistance = Math.min(...distanceArray);
if (minimumDistance < currentNearestDistance) {
currentNearestDistance = minimumDistance;
currentNearestBoundingBox = bbox;
currentNearestDistanceArray = distanceArray;
}
}
if (currentNearestBoundingBox == null || currentNearestDistanceArray == null) {
return null;
}
const nearestBoundingBox = currentNearestBoundingBox;
const getEdgeInfoFromId = (edgeId: number) => {
const direction = edgeId < 2 ? "horizontal" : "vertical";
const isMaxEdge = edgeId % 2 === 1;
const resizableDimension = direction === "horizontal" ? indices[1] : indices[0];
return {
boxId: nearestBoundingBox.id,
direction,
isMaxEdge,
edgeId,
resizableDimension,
};
};
const nearestEdgeIndex = currentNearestDistanceArray.indexOf(currentNearestDistance);
const primaryEdge = getEdgeInfoFromId(nearestEdgeIndex);
let secondaryEdge = null;
const [firstNeighbourId, secondNeighbourId] = getNeighbourEdgeIndexByEdgeIndex[nearestEdgeIndex];
const firstNeighbourEdgeDistance = currentNearestDistanceArray[firstNeighbourId];
const secondNeighbourEdgeDistance = currentNearestDistanceArray[secondNeighbourId];
if (
firstNeighbourEdgeDistance < secondNeighbourEdgeDistance &&
firstNeighbourEdgeDistance < zoomedMaxDistanceToSelection
) {
secondaryEdge = getEdgeInfoFromId(firstNeighbourId);
} else if (
secondNeighbourEdgeDistance < firstNeighbourEdgeDistance &&
secondNeighbourEdgeDistance < zoomedMaxDistanceToSelection
) {
secondaryEdge = getEdgeInfoFromId(secondNeighbourId);
}
return [primaryEdge, secondaryEdge];
}

export const highlightAndSetCursorOnHoveredBoundingBox = _.throttle(
(delta: Point2, position: Point2, planeId: OrthoView) => {
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
const hoveredEdgesInfo = getClosestHoveredBoundingBox(position, planeId);
const inputCatcher = document.getElementById(`inputcatcher_${planeId}`);
if (hoveredEdgesInfo != null && inputCatcher != null) {
const [primaryHoveredEdge, secondaryHoveredEdge] = hoveredEdgesInfo;
getSceneController().highlightUserBoundingBox(primaryHoveredEdge.boxId);
if (secondaryHoveredEdge != null) {
// If a corner is selected.
inputCatcher.style.cursor =
(primaryHoveredEdge.isMaxEdge && secondaryHoveredEdge.isMaxEdge) ||
(!primaryHoveredEdge.isMaxEdge && !secondaryHoveredEdge.isMaxEdge)
? "nwse-resize"
: "nesw-resize";
} else if (primaryHoveredEdge.direction === "horizontal") {
inputCatcher.style.cursor = "row-resize";
} else {
inputCatcher.style.cursor = "col-resize";
}
} else {
getSceneController().highlightUserBoundingBox(null);
if (inputCatcher != null) {
inputCatcher.style.cursor = "auto";
}
}
},
BOUNDING_BOX_HOVERING_THROTTLE_TIME,
);

export function handleResizingBoundingBox(
mousePosition: Point2,
planeId: OrthoView,
primaryEdge: SelectedEdge,
secondaryEdge: ?SelectedEdge,
) {
const state = Store.getState();
const globalMousePosition = calculateGlobalPos(state, mousePosition, planeId);
const { userBoundingBoxes } = getSomeTracing(state.tracing);
const bboxToResize = userBoundingBoxes.find(bbox => bbox.id === primaryEdge.boxId);
if (!bboxToResize) {
return;
}
const updatedBounds = {
min: [...bboxToResize.boundingBox.min],
max: [...bboxToResize.boundingBox.max],
};
function updateBoundsAccordingToEdge(edge: SelectedEdge): boolean {
const { resizableDimension } = edge;
// For a horizontal edge only consider delta.y, for vertical only delta.x
const newPositionValue = Math.round(globalMousePosition[resizableDimension]);
const minOrMax = edge.isMaxEdge ? "max" : "min";
const oppositeOfMinOrMax = edge.isMaxEdge ? "min" : "max";
const otherEdgeValue = bboxToResize.boundingBox[oppositeOfMinOrMax][resizableDimension];
if (otherEdgeValue === newPositionValue) {
// Do not allow the same value for min and max for one dimension.
return false;
}
const areMinAndMaxEdgeCrossing =
// If the min / max edge is moved over the other one.
(edge.isMaxEdge && newPositionValue < otherEdgeValue) ||
(!edge.isMaxEdge && newPositionValue > otherEdgeValue);
if (areMinAndMaxEdgeCrossing) {
// As the edge moved over the other one, the values for min and max must be switched.
updatedBounds[minOrMax][resizableDimension] = otherEdgeValue;
updatedBounds[oppositeOfMinOrMax][resizableDimension] = newPositionValue;
return true;
} else {
updatedBounds[minOrMax][resizableDimension] = newPositionValue;
return false;
}
}
let didMinAndMaxEdgeSwitch = updateBoundsAccordingToEdge(primaryEdge);
if (didMinAndMaxEdgeSwitch) {
primaryEdge.isMaxEdge = !primaryEdge.isMaxEdge;
}
if (secondaryEdge) {
didMinAndMaxEdgeSwitch = updateBoundsAccordingToEdge(secondaryEdge);
if (didMinAndMaxEdgeSwitch) {
secondaryEdge.isMaxEdge = !secondaryEdge.isMaxEdge;
}
}
Store.dispatch(changeUserBoundingBoxAction(primaryEdge.boxId, { boundingBox: updatedBounds }));
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ export const moveZ = (z: number, oneSlide: boolean): void => {
}
};

export function moveWhenAltIsPressed(delta: Point2, position: Point2, _id: any, event: MouseEvent) {
// Always set the correct mouse position. Otherwise, using alt + mouse move and
// alt + scroll won't result in the correct zoomToMouse behavior.
setMousePosition(position);
if (event.altKey && !event.shiftKey && !event.ctrlKey) {
handleMovePlane(delta);
}
}

export const zoom = (value: number, zoomToMouse: boolean) => {
const { activeViewport } = Store.getState().viewModeData.plane;
if (OrthoViewValuesWithoutTDView.includes(activeViewport)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import getSceneController from "oxalis/controller/scene_controller_provider";
import { renderToTexture } from "oxalis/view/rendering_utils";
import { getBaseVoxelFactors } from "oxalis/model/scaleinfo";
import Dimensions from "oxalis/model/dimensions";
import { getClosestHoveredBoundingBox } from "oxalis/controller/combinations/bounding_box_handlers";

const OrthoViewToNumber: OrthoViewMap<number> = {
[OrthoViews.PLANE_XY]: 0,
Expand Down Expand Up @@ -139,7 +140,16 @@ export function handleOpenContextMenu(
const nodeId = maybeGetNodeIdFromPosition(planeView, position, plane, isTouch);
const state = Store.getState();
const globalPosition = calculateGlobalPos(state, position);
showNodeContextMenuAt(event.pageX, event.pageY, nodeId, globalPosition, activeViewport);
const hoveredEdgesInfo = getClosestHoveredBoundingBox(position, plane);
const clickedBoundingBoxId = hoveredEdgesInfo != null ? hoveredEdgesInfo[0].boxId : null;
showNodeContextMenuAt(
event.pageX,
event.pageY,
nodeId,
clickedBoundingBoxId,
globalPosition,
activeViewport,
);
}

export function moveNode(dx: number, dy: number, nodeId: ?number) {
Expand Down Expand Up @@ -168,25 +178,6 @@ export function moveNode(dx: number, dy: number, nodeId: ?number) {
);
}

export function openContextMenu(
planeView: PlaneView,
position: Point2,
plane: OrthoView,
isTouch: boolean,
event: MouseEvent,
showNodeContextMenuAt: ShowContextMenuFunction,
) {
const state = Store.getState();
const { activeViewport } = state.viewModeData.plane;
if (activeViewport === OrthoViews.TDView) {
return;
}

const nodeId = maybeGetNodeIdFromPosition(planeView, position, plane, isTouch);
const globalPosition = calculateGlobalPos(state, position);
showNodeContextMenuAt(event.pageX, event.pageY, nodeId, globalPosition, activeViewport);
}

philippotto marked this conversation as resolved.
Show resolved Hide resolved
export function setWaypoint(
position: Vector3,
activeViewport: OrthoView,
Expand Down
Loading