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 23 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
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,247 @@
// @flow
import { calculateGlobalPos } from "oxalis/model/accessors/view_mode_accessor";
import _ from "lodash";
import { type OrthoView, type Point2, type Vector3 } from "oxalis/constants";
import Store from "oxalis/store";
import { getSomeTracing } from "oxalis/model/accessors/tracing_accessor";
import Dimension from "oxalis/model/dimensions";
import { setUserBoundingBoxBoundsAction } from "oxalis/model/actions/annotation_actions";
import { getBaseVoxelFactors } from "oxalis/model/scaleinfo";

const getNeighbourEdgeIndexByEdgeIndex = {
// TODO: Use this to detect corners properly.
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
// 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],
};
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,
edgeDim: number,
otherDim: number,
planeRatio: Vector3,
) {
// There are four cases how the distance to an edge needs to be calculated.
// Here are all cases visualized via a number that are referenced below:
// Note that this is the perspective of the rendered bounding box cross section.
// ---> x
// | 1 1
// ↓ '. .'
// y ↘ ↙
// +-------+
// | |
// 3 --> | | <-- 3
// | |
// +-------+
// ↗ ↖
// .' '.
// 2 2
//
// This example is for the xy viewport for x as the main direction / edgeDim.

// 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 cornerToCompareWith = compareToMin ? min : max;
if (pos[edgeDim] < min[edgeDim]) {
// Case 1: Distance to the min corner is needed in edgeDim.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this is computing the distance to the corner (even though the method is named getDistanceToBoundingBoxEdge). Wouldn't this mean that if I try to resize the top border of a bbox like this:

                                          mouse grabs here
                                                  x
--------------------------------------------------------------------------------------------------
|                                                                                                |
|                                                                                                |

a very long distance to a corner would be returned (because the bbox is very wide) so that I cannot really grab the handle, even though I'm only one pixel away from the border?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first case computes the distance to the left top corner in your example.
But this way of calculating the distance is only chosen when mouse.x is smaller than the min.x of the bounding box.
See pos[edgeDim] < min[edgeDim] in the if at line 63.

Here is a matching when which case kicks in and the vector / distance that is calculated;


             min.x                                            max.x
               ↓                                               ↓
               ↓                                               ↓
     case 1    ↓           case 3      case 3                  ↓   case 2
           '.                |           |                        .'
             '↘              ↓           ↓                      ↙'
               -------------------------------------------------
               |                                               |
               |                                               |

Is a little bit better understandable?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhhh, now I got it :) So, in my example, edgeDim would need to be 0 because it's referring to the case where the horizontal edge is checked. And consequentially, it would compare the x values (and not the y values which I somehow assumed would also be a valid case).

Could you please adapt the above comment to explain how to interpret edgeDim? I.e., 1 means it's a vertical edge. Now, I also understand the "This example is for the xy viewport for y as the main direction / edgeDim." comment which I apparently didn't really grok before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhhh, now I got it :) So, in my example, edgeDim would need to be 0 because it's referring to the case where the horizontal edge is checked. And consequentially, it would compare the x values (and not the y values which I somehow assumed would also be a valid case).

Exactly 🐈

Could you please adapt the above comment to explain how to interpret edgeDim? I.e., 1 means it's a vertical edge.

Not exactly, edgeDim gives the dimension index that the edge extents to: 0 <=> edge direction is x (like above), 1 <=> edge direction is y (like above but downwards), 2 <=> edge direction is z.

I changed the comment highlighted above in the code. Could you please check whether this is now understandable? If not, could you please try to help me explain this logic? I think the explanation is not straightforward.

return (
Math.sqrt(
Math.abs(pos[edgeDim] - min[edgeDim]) ** 2 +
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) ** 2,
) / planeRatio[edgeDim]
);
}
if (pos[edgeDim] > max[edgeDim]) {
// Case 2: Distance to max Corner is needed in edgeDim.
return (
Math.sqrt(
Math.abs(pos[edgeDim] - max[edgeDim]) ** 2 +
Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) ** 2,
) / planeRatio[edgeDim]
);
}
// Case 3:
// If the position is within the bounds of the edgeDim, the shortest distance
// to the edge is simply the difference between the otherDim values.
return Math.abs(pos[otherDim] - cornerToCompareWith[otherDim]) / planeRatio[edgeDim];
}

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

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 reorderedIndices = Dimension.getIndices(plane);
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
const planeRatio = getBaseVoxelFactors(state.dataset.dataSource.scale);
const thirdDim = reorderedIndices[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 = [
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
getDistanceToBoundingBoxEdge(
globalPosition,
min,
max,
true,
reorderedIndices[0],
reorderedIndices[1],
planeRatio,
),
getDistanceToBoundingBoxEdge(
globalPosition,
min,
max,
false,
reorderedIndices[0],
reorderedIndices[1],
planeRatio,
),
getDistanceToBoundingBoxEdge(
globalPosition,
min,
max,
true,
reorderedIndices[1],
reorderedIndices[0],
planeRatio,
),
getDistanceToBoundingBoxEdge(
globalPosition,
min,
max,
false,
reorderedIndices[1],
reorderedIndices[0],
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 = edgeId < 2 ? reorderedIndices[1] : reorderedIndices[0];
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
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 function handleResizingBoundingBox(
mousePosition: Point2,
planeId: OrthoView,
primaryEdge: SelectedEdge,
secondaryEdge: ?SelectedEdge,
): { primary: boolean, secondary: boolean } {
const state = Store.getState();
const globalMousePosition = calculateGlobalPos(state, mousePosition, planeId);
const { userBoundingBoxes } = getSomeTracing(state.tracing);
const didMinAndMaxSwitch = { primary: false, secondary: false };
const bboxToResize = userBoundingBoxes.find(bbox => bbox.id === primaryEdge.boxId);
if (!bboxToResize) {
return didMinAndMaxSwitch;
}
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;
}
}
didMinAndMaxSwitch.primary = updateBoundsAccordingToEdge(primaryEdge);
if (secondaryEdge) {
didMinAndMaxSwitch.secondary = updateBoundsAccordingToEdge(secondaryEdge);
}
Store.dispatch(setUserBoundingBoxBoundsAction(primaryEdge.boxId, updatedBounds));
return didMinAndMaxSwitch;
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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