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

Also show measured distances in vx (in addition to nm) #5240

Merged
merged 7 commits into from
Mar 15, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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 CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Added the possibility to export binary data as tiff (if long-runnings jobs are enabled). [#5195](https://github.com/scalableminds/webknossos/pull/5195)

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

### Fixed
-
Expand Down
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, length: string, lengthInVx: string) =>
`The tree ${treeName} has a total path length of ${length} (${lengthInVx})`,
Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer Mar 9, 2021

Choose a reason for hiding this comment

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

Did you intentionally remove the "." at the end or was this just by accident? I think a dot at the end makes sense as this is a complete sentence :)

"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] {
Copy link
Member Author

Choose a reason for hiding this comment

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

The changes to the api's interface are a bit unfortunate. The probability that someone uses this feature is probably very low and the effort to fix it is quite small. This is why I don't want to bump the api version (which is quite a bit of overhead for us and also for our users which have to check how to upgrade). This is a good example why our current API versioning is sub-ideal :/

Do you agree with this approach, @MichaelBuessemeyer and @daniel-wer? Alternatively, I could keep the old methods and add measureTreeLengthInNmAndVx, measureAllTrees InNmAndVx and measurePathLengthBetweenNodesInNmAndVx. However, that's also quite ugly..

Copy link
Contributor

Choose a reason for hiding this comment

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

How about a third variant:

  1. Rename measureTreeLength to measureTreeLengthWithVX or so.
  2. And just call this method in measureTreeLength and just return the first value. This should match the interface of the old API right?

You can do the same steps for measurePathLengthBetweenNodes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nevertheless, I think your solution is fine. You can go ahead and merge this PR if you are against my proposed variant.

Users that would have to adapt their code should be able to handle this small interface change 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

How about a third variant:

Rename measureTreeLength to measureTreeLengthWithVX or so.
And just call this method in measureTreeLength and just return the first value. This should match the interface of the old API right?

Yes, that would also work (and is kind of what I meant with my suggestion to have an additional method à la measureTreeLengthInNmAndVx). The one thing which bothers me here is the suboptimal naming of the methods (the old method is limited to nm, but would need to keep its name, and the new method is more powerful but would need to have a more specific name). However, I'm not completely against it. Let's wait for a third opinion :)

Copy link
Member

Choose a reason for hiding this comment

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

Seeing that this method has only existed for a couple of months and its addition wasn't announced, I would also think usage is probably extremely small if not zero. You could include this change in the "breaking" section of the changelog :)

Copy link
Member Author

Choose a reason for hiding this comment

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

You could include this change in the "breaking" section of the changelog :)

Good idea! I also agree with your risk assessment so this PR should be good to go now 🕺

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