Skip to content

Commit

Permalink
Also show measured distances in vx (in addition to nm) (#5240)
Browse files Browse the repository at this point in the history
* also show measured distance in vx

* clean up code so that voxel distance is integrated at all relevant places and formatted nicely

* update changelog

* fix type

* add missing fullstop and add breaking change section to changelog
  • Loading branch information
philippotto authored Mar 15, 2021
1 parent f569c59 commit ee5dbc7
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 41 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Added a link to dataset view mode from annotation mode info tab. [#5262](https://github.com/scalableminds/webknossos/pull/5262)

### Changed
-
- Measured distances will be shown in voxel space, too. [#5240](https://github.com/scalableminds/webknossos/pull/5240)

### Fixed
- Fixed a regression in the task search which could lead to a frontend crash. [#5267](https://github.com/scalableminds/webknossos/pull/5267)

### Removed
-

### Breaking Change
- The front-end API methods `measurePathLengthBetweenNodes`, `measureAllTrees` and `measureTreeLength` were changed to return a tuple containing the distance in nm and in vx (instead of only returning the distance in nm). [#5240](https://github.com/scalableminds/webknossos/pull/5240)
17 changes: 11 additions & 6 deletions frontend/javascripts/libs/format_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,21 @@ export function formatScale(scaleArr: ?Vector3, roundTo?: number = 2): string {
}
}

export function formatNumberToLength(numberInNm: number): string {
if (numberInNm < 1000) {
return `${numberInNm.toFixed(0)}${ThinSpace}nm`;
} else if (numberInNm < 1000000) {
return `${(numberInNm / 1000).toFixed(1)}${ThinSpace}µm`;
export function formatNumberToLength(lengthInNm: number): string {
if (lengthInNm < 1000) {
return `${lengthInNm.toFixed(0)}${ThinSpace}nm`;
} else if (lengthInNm < 1000000) {
return `${(lengthInNm / 1000).toFixed(1)}${ThinSpace}µm`;
} else {
return `${(numberInNm / 1000000).toFixed(1)}${ThinSpace}mm`;
return `${(lengthInNm / 1000000).toFixed(1)}${ThinSpace}mm`;
}
}
export function formatLengthAsVx(lengthInVx: number, roundTo?: number = 2): string {
const roundedLength = Utils.roundTo(lengthInVx, roundTo);
return `${roundedLength} vx`;
}
export function formatExtentWithLength(
extent: BoundingBoxObject,
formattingFunction: number => string,
Expand Down
4 changes: 2 additions & 2 deletions frontend/javascripts/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ instead. Only enable this option if you understand its effect. All layers will n
"tracing.out_of_task_bounds": "The current position is outside of the task's bounding box.",
"tracing.copy_position": "Copy position to clipboard.",
"tracing.copy_rotation": "Copy rotation to clipboard.",
"tracing.tree_length_notification": (treeName: string, length: string) =>
`The tree ${treeName} has a total path length of ${length}.`,
"tracing.tree_length_notification": (treeName: string, lengthInNm: string, lengthInVx: string) =>
`The tree ${treeName} has a total path length of ${lengthInNm} (${lengthInVx}).`,
"tracing.sharing_modal_basic_information": (sharingActiveNode?: boolean) =>
`This link includes the ${
sharingActiveNode ? "active tree node," : ""
Expand Down
49 changes: 35 additions & 14 deletions frontend/javascripts/oxalis/api/api_latest.js
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,10 @@ class TracingApi {
return result;
}

measureTreeLength(treeId: number): number {
/**
* Measures the length of the given tree and returns the length in nanometer and in voxels.
*/
measureTreeLength(treeId: number): [number, number] {
const skeletonTracing = assertSkeleton(Store.getState().tracing);
const tree = skeletonTracing.trees[treeId];
if (!tree) {
Expand All @@ -644,28 +647,39 @@ class TracingApi {

// Pre-allocate vectors

let lengthAcc = 0;
let lengthNmAcc = 0;
let lengthVxAcc = 0;
for (const edge of tree.edges.all()) {
const sourceNode = tree.nodes.get(edge.source);
const targetNode = tree.nodes.get(edge.target);
lengthAcc += V3.scaledDist(sourceNode.position, targetNode.position, datasetScale);
lengthNmAcc += V3.scaledDist(sourceNode.position, targetNode.position, datasetScale);
lengthVxAcc += V3.length(V3.sub(sourceNode.position, targetNode.position));
}

return lengthAcc;
return [lengthNmAcc, lengthVxAcc];
}

measureAllTrees(): number {
/**
* Measures the length of all trees and returns the length in nanometer and in voxels.
*/
measureAllTrees(): [number, number] {
const skeletonTracing = assertSkeleton(Store.getState().tracing);

const totalLength = _.values(skeletonTracing.trees).reduce(
(sum, currentTree) => sum + this.measureTreeLength(currentTree.treeId),
0,
);
let totalLengthNm = 0;
let totalLengthVx = 0;
_.values(skeletonTracing.trees).forEach(currentTree => {
const [lengthNm, lengthVx] = this.measureTreeLength(currentTree.treeId);
totalLengthNm += lengthNm;
totalLengthVx += lengthVx;
});

return totalLength;
return [totalLengthNm, totalLengthVx];
}

measurePathLengthBetweenNodes(sourceNodeId: number, targetNodeId: number): number {
/**
* Returns the length of the shortest path between two nodes in nanometer and in voxels.
*/
measurePathLengthBetweenNodes(sourceNodeId: number, targetNodeId: number): [number, number] {
const skeletonTracing = assertSkeleton(Store.getState().tracing);
const { node: sourceNode, tree: sourceTree } = getNodeAndTreeOrNull(
skeletonTracing,
Expand All @@ -684,9 +698,14 @@ class TracingApi {
const datasetScale = Store.getState().dataset.dataSource.scale;
// We use the Dijkstra algorithm to get the shortest path between the nodes.
const distanceMap = {};
// The distance map is also maintained in voxel space. This information is only
// used when returning the final distance. The actual path finding is only done in
// the physical space (nm-based).
const distanceMapVx = {};
const getDistance = nodeId =>
distanceMap[nodeId] != null ? distanceMap[nodeId] : Number.POSITIVE_INFINITY;
distanceMap[sourceNode.id] = 0;
distanceMapVx[sourceNode.id] = 0;
// The priority queue saves node id and distance tuples.
const priorityQueue = new PriorityQueue<[number, number]>({
comparator: ([_first, firstDistance], [_second, secondDistance]) =>
Expand All @@ -699,16 +718,18 @@ class TracingApi {
// Calculate the distance to all neighbours and update the distances.
for (const { source, target } of sourceTree.edges.getEdgesForNode(nextNodeId)) {
const neighbourNodeId = source === nextNodeId ? target : source;
const neightbourPosition = sourceTree.nodes.get(neighbourNodeId).position;
const neighbourPosition = sourceTree.nodes.get(neighbourNodeId).position;
const neighbourDistance =
distance + V3.scaledDist(nextNodePosition, neightbourPosition, datasetScale);
distance + V3.scaledDist(nextNodePosition, neighbourPosition, datasetScale);
if (neighbourDistance < getDistance(neighbourNodeId)) {
distanceMap[neighbourNodeId] = neighbourDistance;
const neighbourDistanceVx = V3.length(V3.sub(nextNodePosition, neighbourPosition));
distanceMapVx[neighbourNodeId] = neighbourDistanceVx;
priorityQueue.queue([neighbourNodeId, neighbourDistance]);
}
}
}
return distanceMap[targetNodeId];
return [distanceMap[targetNodeId], distanceMapVx[targetNodeId]];
}

/**
Expand Down
37 changes: 25 additions & 12 deletions frontend/javascripts/oxalis/view/node_context_menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import Toast from "libs/toast";
import Clipboard from "clipboard-js";
import messages from "messages";
import { getNodeAndTree, findTreeByNodeId } from "oxalis/model/accessors/skeletontracing_accessor";
import { formatNumberToLength } from "libs/format_utils";
import { formatNumberToLength, formatLengthAsVx } from "libs/format_utils";
import { roundTo } from "libs/utils";

/* eslint-disable react/no-unused-prop-types */
// The newest eslint version thinks the props listed below aren't used.
Expand Down Expand Up @@ -64,17 +65,26 @@ function copyIconWithTooltip(value: string | number, title: string) {
}

function measureAndShowLengthBetweenNodes(sourceNodeId: number, targetNodeId: number) {
const length = api.tracing.measurePathLengthBetweenNodes(sourceNodeId, targetNodeId);
const [lengthNm, lengthVx] = api.tracing.measurePathLengthBetweenNodes(
sourceNodeId,
targetNodeId,
);
notification.open({
message: `The shortest path length between the nodes is ${formatNumberToLength(length)}.`,
message: `The shortest path length between the nodes is ${formatNumberToLength(
lengthNm,
)} (${formatLengthAsVx(lengthVx)}).`,
icon: <i className="fas fa-ruler" />,
});
}

function measureAndShowFullTreeLength(treeId: number, treeName: string) {
const length = api.tracing.measureTreeLength(treeId);
const [lengthInNm, lengthInVx] = api.tracing.measureTreeLength(treeId);
notification.open({
message: messages["tracing.tree_length_notification"](treeName, formatNumberToLength(length)),
message: messages["tracing.tree_length_notification"](
treeName,
formatNumberToLength(lengthInNm),
formatLengthAsVx(lengthInVx),
),
icon: <i className="fas fa-ruler" />,
});
}
Expand Down Expand Up @@ -224,13 +234,16 @@ function NodeContextMenu(props: Props) {
: null;
const distanceToSelection =
activeNode != null
? formatNumberToLength(
V3.scaledDist(activeNode.position, positionToMeasureDistanceTo, datasetScale),
)
? [
formatNumberToLength(
V3.scaledDist(activeNode.position, positionToMeasureDistanceTo, datasetScale),
),
formatLengthAsVx(V3.length(V3.sub(activeNode.position, positionToMeasureDistanceTo))),
]
: null;
const nodePositionAsString =
nodeContextMenuNode != null
? nodeContextMenuNode.position.map(value => value.toFixed(2)).join(", ")
? nodeContextMenuNode.position.map(value => roundTo(value, 2)).join(", ")
: "";
return (
<React.Fragment>
Expand Down Expand Up @@ -263,9 +276,9 @@ function NodeContextMenu(props: Props) {
) : null}
{distanceToSelection != null ? (
<div className="node-context-menu-item">
<i className="fas fa-ruler" /> {distanceToSelection} to this{" "}
{clickedNodeId != null ? "Node" : "Position"}
{copyIconWithTooltip(distanceToSelection, "Copy the distance")}
<i className="fas fa-ruler" /> {distanceToSelection[0]} ({distanceToSelection[1]}) to
this {clickedNodeId != null ? "Node" : "Position"}
{copyIconWithTooltip(distanceToSelection[0], "Copy the distance")}
</div>
) : null}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
setTreeGroupAction,
} from "oxalis/model/actions/skeletontracing_actions";
import messages from "messages";
import { formatNumberToLength } from "libs/format_utils";
import { formatNumberToLength, formatLengthAsVx } from "libs/format_utils";
import api from "oxalis/api/internal_api";

const CHECKBOX_STYLE = { verticalAlign: "middle" };
Expand Down Expand Up @@ -290,9 +290,14 @@ class TreeHierarchyView extends React.PureComponent<Props, State> {
};

handleMeasureSkeletonLength = (treeId: number, treeName: string) => {
const length = api.tracing.measureTreeLength(treeId);
const [lengthInNm, lengthInVx] = api.tracing.measureTreeLength(treeId);

notification.open({
message: messages["tracing.tree_length_notification"](treeName, formatNumberToLength(length)),
message: messages["tracing.tree_length_notification"](
treeName,
formatNumberToLength(lengthInNm),
formatLengthAsVx(lengthInVx),
),
icon: <i className="fas fa-ruler" />,
});
};
Expand Down
8 changes: 5 additions & 3 deletions frontend/javascripts/oxalis/view/right-menu/trees_tab_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ import * as Utils from "libs/utils";
import api from "oxalis/api/internal_api";
import messages from "messages";
import JSZip from "jszip";
import { formatNumberToLength } from "libs/format_utils";
import { formatNumberToLength, formatLengthAsVx } from "libs/format_utils";

import DeleteGroupModalView from "./delete_group_modal_view";
import AdvancedSearchPopover from "./advanced_search_popover";
Expand Down Expand Up @@ -640,10 +640,12 @@ class TreesTabView extends React.PureComponent<Props, State> {
) : null;

handleMeasureAllSkeletonsLength = () => {
const totalLength = api.tracing.measureAllTrees();
const [totalLengthNm, totalLengthVx] = api.tracing.measureAllTrees();

notification.open({
message: `The total length of all skeletons is ${formatNumberToLength(totalLength)}.`,
message: `The total length of all skeletons is ${formatNumberToLength(
totalLengthNm,
)} (${formatLengthAsVx(totalLengthVx)}).`,
icon: <i className="fas fa-ruler" />,
});
};
Expand Down

0 comments on commit ee5dbc7

Please sign in to comment.