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 UI for segment statistics (volume and bbox) #7249

Merged
merged 61 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
d2f1211
very much WIP: first steps towards route and frontend
dieknolle3333 Jul 14, 2023
739e34e
Merge branch 'master' into segment-volume-route
dieknolle3333 Jul 31, 2023
3f2da45
add url params
dieknolle3333 Jul 31, 2023
4a364c1
show segment size in context menu without unit
dieknolle3333 Aug 1, 2023
3319ab5
start tests
dieknolle3333 Aug 1, 2023
b33297f
add format in multiple dimensions (early implementation) and resp. tests
dieknolle3333 Aug 4, 2023
2de3d67
show volume in context menu of segment
dieknolle3333 Aug 4, 2023
fc8cfbf
Merge branch 'master' into segment-volume-route
dieknolle3333 Aug 4, 2023
537a8c0
add reload button for segment volume
dieknolle3333 Aug 4, 2023
dabc845
add segment statistics entry in group menu and style icons
dieknolle3333 Aug 8, 2023
7a27826
WIP: start implementing segment statistics modal
dieknolle3333 Aug 8, 2023
4c819a4
move statistics modal outside of context menu
dieknolle3333 Aug 11, 2023
a321f2c
show segment sizes in modal
dieknolle3333 Aug 11, 2023
c8e249e
merge master
dieknolle3333 Aug 11, 2023
efa3631
lint
dieknolle3333 Aug 11, 2023
e1223fa
add table to statistics modal
dieknolle3333 Aug 11, 2023
2bfb726
export segment sizes to CSV
dieknolle3333 Aug 11, 2023
5d874a2
make linter happier before I leave
dieknolle3333 Aug 11, 2023
0300bba
revert unintentional change
dieknolle3333 Aug 28, 2023
0ebca26
WIP: add segment bounding box route
fm3 Aug 28, 2023
9c1101f
calculate real segment bbox
fm3 Aug 28, 2023
b699121
performance optimization: filter out inner bucket positions
fm3 Aug 28, 2023
1aa00b6
remove unused import
fm3 Aug 28, 2023
5e4f204
make statistics routes post, allow for multiple segment ids
fm3 Aug 28, 2023
326c14b
rename method
fm3 Aug 28, 2023
9364769
do zero check without type conversion
fm3 Aug 28, 2023
b1fd9b3
Revert "do zero check without type conversion"
fm3 Aug 28, 2023
fc0ad22
compare to segment id correctly
fm3 Aug 28, 2023
1a8e82e
find group segments recursively
dieknolle3333 Aug 28, 2023
b01faf4
very much WIP: use batch request in frontend
dieknolle3333 Aug 28, 2023
6fbe58f
use batch requests in frontend
dieknolle3333 Aug 29, 2023
d7e6910
use bounding box route
dieknolle3333 Aug 29, 2023
cf941c8
Merge branch 'master' into segment-volume-route
dieknolle3333 Aug 29, 2023
0489093
show bounding box in statistics modal
dieknolle3333 Aug 30, 2023
5d2df86
add bounding box to modal
dieknolle3333 Aug 30, 2023
acbb128
add bounding boxes to modal and CSV and add spin while loading data
dieknolle3333 Sep 1, 2023
fba6b52
move cyclic dependency
dieknolle3333 Sep 1, 2023
522869e
Merge branch 'master' into segment-volume-route
dieknolle3333 Sep 1, 2023
2eb9817
WIP: add bounding box to indiv. segment context menu in viewport
dieknolle3333 Sep 1, 2023
6caf73f
add bounding box size to indiv. segment context menu
dieknolle3333 Sep 3, 2023
28fee08
improve bounding box info in indiv. segment context menu
dieknolle3333 Sep 4, 2023
abbd2e1
improve dependencies for requests of bounding box info
dieknolle3333 Sep 4, 2023
bde4511
minor changes to make code ready for review
dieknolle3333 Sep 4, 2023
3ff196b
lint
dieknolle3333 Sep 4, 2023
c27dfdc
Merge branch 'master' into segment-volume-route
dieknolle3333 Sep 4, 2023
4645164
add changelog
dieknolle3333 Sep 4, 2023
da02af4
add keys to table rows
dieknolle3333 Sep 5, 2023
a00ae72
fix errors for datasets with fallback layers
dieknolle3333 Sep 5, 2023
11eb4bb
very much WIP: started to address review
dieknolle3333 Sep 7, 2023
7702b60
[WIP because of failing tests] address review
dieknolle3333 Sep 8, 2023
8b9c9fa
add tests
dieknolle3333 Sep 10, 2023
0354378
use clean way to find out a segments parent group
dieknolle3333 Sep 10, 2023
4e1d173
revert change in test.sh
dieknolle3333 Sep 10, 2023
69c128d
fix frontend merge conflicts
dieknolle3333 Sep 14, 2023
be623d7
trying to fix backend merge conflicts
dieknolle3333 Sep 14, 2023
79c77fc
fix imports after merge
fm3 Sep 14, 2023
41083de
fix saving before opening modal
dieknolle3333 Sep 15, 2023
f6037e0
address re-review
dieknolle3333 Sep 15, 2023
25a9fe0
improve performance of segment list tab
philippotto Sep 18, 2023
4edf2f3
improve csv export and change positioning of export modal
philippotto Sep 18, 2023
351ff05
Merge branch 'master' of github.com:scalableminds/webknossos into seg…
philippotto Sep 18, 2023
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
20 changes: 19 additions & 1 deletion frontend/javascripts/admin/admin_rest_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,14 +958,32 @@ export function getNewestVersionForTracing(
);
}

export function getSegmentVolume(
tracingStoreUrl: string,
tracingId: string,
mag: Vector3,
segmentId: number,
): Promise<number> {
return doWithToken((token) => {
const params = new URLSearchParams({
token: token,
mag: mag.join("-"),
segmentId: String(segmentId),
});
return Request.receiveJSON(
`${tracingStoreUrl}/tracings/volume/${tracingId}/segmentStatistics/volume?${params}`,
);
});
}

export async function importVolumeTracing(
tracing: Tracing,
volumeTracing: VolumeTracing,
dataFile: File,
): Promise<number> {
return doWithToken((token) =>
Request.sendMultipartFormReceiveJSON(
`${tracing.tracingStore.url}/tracings/volume/${volumeTracing.tracingId}/importVolumeData?token=${token}`,
`${tracing.tracingStore.url}/tracings/volume/${volumeTracing}/importVolumeData?token=${token}`,
dieknolle3333 marked this conversation as resolved.
Show resolved Hide resolved
{
data: {
dataFile,
Expand Down
30 changes: 30 additions & 0 deletions frontend/javascripts/libs/format_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,36 @@ export function formatNumberToLength(lengthInNm: number): string {
return formatNumberToUnit(lengthInNm, nmFactorToUnit);
}

const nmFactorToUnit2D = new Map([
[1e-6, "pm²"],
[1, "nm²"],
[1e6, "µm²"],
[1e12, "mm²"],
[1e18, "m²"],
[1e24, "km²"],
]);
/* TODO: or rather:
const nmFactorToUnit2D = new Map(
Array.from(nmFactorToUnit).map((entry) => [Math.pow(entry[0], 2), entry[1].concat("²")]),
);
Although I think that the explicit maps are better to read and it's better to have them stored rather than computing them every time they are needed.
dieknolle3333 marked this conversation as resolved.
Show resolved Hide resolved
*/
export function formatNumberToArea(lengthInNm2: number): string {
return formatNumberToUnit(lengthInNm2, nmFactorToUnit2D, true, 0);
}

const nmFactorToUnit3D = new Map([
[1e-9, "pm³"],
[1, "nm³"],
[1e9, "µm³"],
[1e18, "mm³"],
[1e27, "m³"],
[1e36, "km³"],
]);
export function formatNumberToVolume(lengthInNm3: number): string {
return formatNumberToUnit(lengthInNm3, nmFactorToUnit3D, true, 0);
}

const byteFactorToUnit = new Map([
[1, "B"],
[1e3, "KB"],
Expand Down
72 changes: 68 additions & 4 deletions frontend/javascripts/oxalis/view/context_menu.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CopyOutlined, PushpinOutlined } from "@ant-design/icons";
import { CopyOutlined, PushpinOutlined, ReloadOutlined } from "@ant-design/icons";
import type { Dispatch } from "redux";
import { Dropdown, Empty, notification, Tooltip, Popover, Input, MenuProps } from "antd";
import { connect } from "react-redux";
import React, { createContext, MouseEvent, useContext } from "react";
import React, { createContext, MouseEvent, useContext, useState } from "react";
import type {
APIConnectomeFile,
APIDataset,
Expand Down Expand Up @@ -52,7 +52,7 @@ import {
deleteBranchpointByIdAction,
addTreesAndGroupsAction,
} from "oxalis/model/actions/skeletontracing_actions";
import { formatNumberToLength, formatLengthAsVx } from "libs/format_utils";
import { formatNumberToLength, formatLengthAsVx, formatNumberToVolume } from "libs/format_utils";
import {
getActiveSegmentationTracing,
getSegmentsForLayer,
Expand All @@ -69,6 +69,7 @@ import {
import {
getVisibleSegmentationLayer,
getMappingInfo,
getResolutionInfo,
} from "oxalis/model/accessors/dataset_accessor";
import {
loadAgglomerateSkeletonAtPosition,
Expand Down Expand Up @@ -101,6 +102,9 @@ import {
MenuItemType,
SubMenuType,
} from "antd/lib/menu/hooks/useItems";
import { getSegmentVolume } from "admin/admin_rest_api";
import { useFetch } from "libs/react_helpers";
import { AsyncIconButton } from "components/async_clickables";

type ContextMenuContextValue = React.MutableRefObject<HTMLElement | null> | null;
export const ContextMenuContext = createContext<ContextMenuContextValue>(null);
Expand Down Expand Up @@ -1093,6 +1097,7 @@ function getInfoMenuItem(
}

function ContextMenuInner(propsWithInputRef: Props) {
const [lastTimeVolumeWasFetched, setLastTimeVolumeWasFetched] = useState(new Date());
const inputRef = useContext(ContextMenuContext);
const { ...props } = propsWithInputRef;
const {
Expand All @@ -1107,6 +1112,36 @@ function ContextMenuInner(propsWithInputRef: Props) {
maybeViewport,
} = props;

const segmentIdAtPosition = globalPosition != null ? getSegmentIdForPosition(globalPosition) : 0;
const { visibleSegmentationLayer, volumeTracing } = props;
const hasNoFallbackLayer =
visibleSegmentationLayer != null &&
"fallbackLayer" in visibleSegmentationLayer &&
visibleSegmentationLayer.fallbackLayer == null;
const segmentSize = useFetch(
async () => {
if (visibleSegmentationLayer != null && volumeTracing != null) {
if (hasNoFallbackLayer) {
console.log(contextMenuPosition); //TODO
const tracingId = volumeTracing.tracingId;
const tracingStoreUrl = Store.getState().tracing.tracingStore.url;
const mag = getResolutionInfo(visibleSegmentationLayer.resolutions);
const segmentId = segmentIdAtPosition;
const segmentSize = await getSegmentVolume(
tracingStoreUrl,
tracingId,
mag.getHighestResolution(),
segmentId,
);
console.log(segmentSize);
return formatNumberToVolume(segmentSize);
}
}
},
"loading", //TODO make pretty with spinner
[contextMenuPosition, segmentIdAtPosition, lastTimeVolumeWasFetched],
);

if (contextMenuPosition == null || maybeViewport == null) {
return <></>;
}
Expand Down Expand Up @@ -1143,7 +1178,6 @@ function ContextMenuInner(propsWithInputRef: Props) {
const nodePositionAsString =
// @ts-expect-error ts-migrate(2339) FIXME: Property 'position' does not exist on type 'never'... Remove this comment to see the full error message
nodeContextMenuNode != null ? positionToString(nodeContextMenuNode.position) : "";
const segmentIdAtPosition = globalPosition != null ? getSegmentIdForPosition(globalPosition) : 0;
const infoRows = [];

if (maybeClickedNodeId != null && nodeContextMenuTree != null) {
Expand Down Expand Up @@ -1180,6 +1214,36 @@ function ContextMenuInner(propsWithInputRef: Props) {
);
}

const handleRefreshSegmentVolume = async () => {
await api.tracing.save();
setLastTimeVolumeWasFetched(new Date());
};

const refreshButton = (
<Tooltip title="Update this statistic">
<AsyncIconButton
onClick={handleRefreshSegmentVolume}
type="primary"
icon={<ReloadOutlined />}
style={{ marginLeft: 4 }}
/>
</Tooltip>
);

if (hasNoFallbackLayer) {
infoRows.push(
getInfoMenuItem(
"volumeInfo",
<>
<i className="fas fa-expand-alt segment-context-icon" />
Size: {segmentSize}
{copyIconWithTooltip(segmentSize as string, "Copy size")}
{refreshButton}
</>,
),
);
}

if (distanceToSelection != null) {
infoRows.push(
getInfoMenuItem(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { getSegmentVolume } from "admin/admin_rest_api";
import { Modal, Table } from "antd";
import { formatDateInLocalTimeZone } from "components/formatted_date";
import saveAs from "file-saver";
import { formatNumberToUnit, formatNumberToVolume } from "libs/format_utils";
import { useFetch } from "libs/react_helpers";
import { Unicode } from "oxalis/constants";
import { getResolutionInfo } from "oxalis/model/accessors/dataset_accessor";
import { Segment } from "oxalis/store";
import React from "react";

const SEGMENT_STATISTICS_CSV_HEADER = "groupId,segmendId,segmentName,volumeInVoxel,volumeInNm3";

const { ThinSpace } = Unicode;

type Props = {
onCancel: (...args: Array<any>) => any;
isOpen: boolean;
tracingId: any;
tracingStoreUrl: any;
visibleSegmentationLayer: any;
dieknolle3333 marked this conversation as resolved.
Show resolved Hide resolved
segments: Segment[];
group: number;
};

type SegmentInfo = {
segmentId: number;
segmentName: string;
groupId: number;
//groupName: string; TODO
volumeInNm3: number;
formattedSize: string;
volumeInVoxel: number;
};

const exportStatisticsToCSV = (segmentInformation: Array<SegmentInfo>) => {
if (segmentInformation.length < 0) {
dieknolle3333 marked this conversation as resolved.
Show resolved Hide resolved
return;
}
const segmentStatisticsAsString = segmentInformation
.map(
(segmentInfo) =>
`${segmentInfo.groupId},${segmentInfo.segmentId},${segmentInfo.segmentName},${segmentInfo.volumeInVoxel},${segmentInfo.volumeInNm3}`,
)
.join("\n");
const csv = [SEGMENT_STATISTICS_CSV_HEADER, segmentStatisticsAsString].join("\n");
const filename = `segmentStatistics-${new Date().toLocaleString().replace(/s/, "-")}.csv`; // TODO useful file naming
const blob = new Blob([csv], {
type: "text/plain;charset=utf-8",
});
saveAs(blob, filename);
};

export function SegmentStatisticsModal({
isOpen,
onCancel,
tracingId,
tracingStoreUrl,
visibleSegmentationLayer,
segments,
group,
}: Props) {
console.log(tracingId);
const mag = getResolutionInfo(visibleSegmentationLayer.resolutions);
const nmFactorToUnit = new Map([[1, "nm³"]]);
const dataSource = useFetch(
async () => {
const volumeStrings = await segments.map(async (segment: Segment) => {
return getSegmentVolume(
tracingStoreUrl,
tracingId,
mag.getHighestResolution(),
segment.id,
).then((vol) => {
const formattedSize = formatNumberToVolume(vol);
return {
segmentId: segment.id,
segmentName: segment.name == null ? `Segment ${segment.id}` : segment.name,
groupId: group,
volumeInVoxel: vol,
volumeInNm3: parseInt(formatNumberToUnit(vol, nmFactorToUnit).split(ThinSpace)[0]),
formattedSize: formattedSize,
};
});
});
return Promise.all(volumeStrings);
},
[], //TODO make pretty with spinner
[isOpen],
);
const columns = [
{ title: "Segment", dataIndex: "segmentName", key: "segmentName" },
{ title: "Volume", dataIndex: "formattedSize", key: "formattedSize" },
];

return (
<Modal
title="Segment Statistics"
open={isOpen}
onCancel={onCancel}
style={{ marginRight: 10 }}
onOk={() => exportStatisticsToCSV(dataSource)}
okText="Export to CSV"
>
<Table dataSource={dataSource} columns={columns} />
</Modal>
);
}
Loading