-
Notifications
You must be signed in to change notification settings - Fork 27
Add Split Segments Toolkit and Draw Mode for Skeleton Tool #8434
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
Changes from 90 commits
4945585
dcfa9ed
b5c4d42
ab08454
667fa5e
d1691ec
a60c90f
6f960e0
00ab1b0
f2504bf
4f950f1
f5a9cb5
573f1b7
8800265
6bad426
c05b06d
71b584b
6c6bcdb
0c6ae1b
92c7c18
26a1c1b
664c8d7
fa80d3c
9ec00b5
5dcef90
f6f3bb9
97c35cd
85696f4
a4bc77b
4ec5a0d
833db7d
d2c9baa
72ffdd1
0488b7d
cfc5527
a8d0e54
0230fe0
c5c05ac
906843a
6a22e96
1cde4f0
b497b17
546c9e5
1c27181
2621179
8ebe2c0
240cf66
f39d88c
8b59a87
fd834d2
052c85e
27ab3ad
4b3eb62
6b96886
abd87d6
38a232d
6e80117
fd778e0
5bfd5c3
e69977b
2315786
8692405
7a867fe
6408e56
4bb5587
2f700cd
3fa7455
89bbe1d
0ed911d
98a2070
5767138
7b7e752
87122a9
3454e58
2c42d98
ea64de6
13cf9f4
429f82a
c5b7c6b
28ee8ef
3b40430
035aa53
06d26c5
dbfb1bf
34510d0
d10ac1d
6cad801
87ef13d
83b570e
de74025
99fd8c0
4e304ca
82bab40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Split Segments Toolkit | ||
|
||
As explained in the [Toolkits](../ui/toolbar.md#Toolkits) section, there is a dedicated toolkit for splitting segments in your volume annotation. | ||
With this toolkit activated, you can draw a curved 3D surface to split a segment into two along this interface. | ||
|
||
## Workflow | ||
|
||
The recommended steps for splitting a segment are as follows: | ||
philippotto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
1. Use the bounding box tool to create a box around the merge error you want to correct. Theoretically, this step is optional, but keep in mind that relabeling large parts of a segment requires a lot of computing resources and is limited by your computer performance. Consider using the [proofreading tool](../proofreading/tools.html) for large-scale corrections. If you want to correct data for the purpose of ground-truth, it is often enough to only do a local correction instead of proofreading the whole dataset. | ||
2. Go to the first z slice of the bounding box. | ||
3. Create a new skeleton tree and place nodes on the boundary that should split the segment into two. Consider activating the "Pen" mode within the skeleton tool so that you can rapidly draw nodes by dragging the mouse while it's pressed down. | ||
4. Go to the last z slice of the bounding box and repeat the previous step. | ||
5. Now you can inspect all the slices in between. A smooth spline curve will be shown in each slice that is derived from interpolating between adjacent slices. If you want to correct the interpolation, you can create new nodes on a slice. A new spline curve will be computed from these points and the interpolation will be updated. | ||
6. If you are satisfied with the (interpolated) boundary, you can switch to the fill tool and relabel one side, effectively splitting a segment into two. Ensure to enable the 3D mode as well as the "Restrict to Bounding Box" mode. | ||
|
||
## Troubleshooting | ||
|
||
- It can easily happen that the flood-fill operation relabels the entire segment instead of only the part you intended to relabel. This is usually caused by the 3D surface not completely cutting through the segment. Instead, the surface might only go very close to the edge. In that case, the flood-fill operation can traverse over that edge and thus relabel everything (see image below). If this happens, you should undo the fill operation and double-check the surface. To avoid this problem, it is recommended to draw the splitting surface a bit bigger than the actual segment boundaries. | ||
- If multiple spline curves appear per slice, this is often due to a configuration issue. Please adapt the clipping distance in the left sidebar (`Layers` → `Skeleton`). | ||
|
||
 | ||
|
||
## Impact of "Split Segments" Toolkit | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The nodes/skeletons that are being placed in this mode, are the persisted? Or will they disappear after the split operation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yes, it's a normal tree. it is only rendered differently when the split toolkit is active. if the toolkit is deactivated, the tree will look like any other tree. |
||
|
||
Note that the workflow above is only possible because the "Split Segments" toolkit is enabled. | ||
By activating that toolkit, WEBKNOSSOS behaves differently in the following way: | ||
philippotto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- The active tree will be rendered without any edges. Instead, for each slice in which multiple nodes exist, a spline curve is calculated that goes through these nodes. | ||
- Using the spline curves from before, a 3D surface is generated that interpolates through these curves. The surface can be seen in the 3D viewport. | ||
- The flood-fill tool will respect the splitting surface by not crossing it. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import type * as THREE from "three"; | ||
|
||
export function orderPointsWithMST(points: THREE.Vector3[]): THREE.Vector3[] { | ||
/* | ||
* Find the order of points with the shortest distance heuristically. | ||
* This is done by computing the MST of the points and then traversing | ||
* that MST several times (each node is tried as the starting point). | ||
* The shortest order wins. | ||
*/ | ||
if (points.length === 0) return []; | ||
|
||
const mst = computeMST(points); | ||
let bestOrder: number[] = []; | ||
let minLength = Number.POSITIVE_INFINITY; | ||
|
||
for (let startIdx = 0; startIdx < points.length; startIdx++) { | ||
const order = traverseMstDfs(mst, startIdx); | ||
const length = computePathLength(points, order); | ||
|
||
if (length < minLength) { | ||
minLength = length; | ||
bestOrder = order; | ||
} | ||
} | ||
|
||
return bestOrder.map((index) => points[index]); | ||
} | ||
|
||
// Mostly generated with ChatGPT: | ||
philippotto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
class DisjointSet { | ||
// Union find datastructure | ||
private parent: number[]; | ||
private rank: number[]; | ||
|
||
constructor(n: number) { | ||
this.parent = Array.from({ length: n }, (_, i) => i); | ||
this.rank = Array(n).fill(0); | ||
} | ||
|
||
find(i: number): number { | ||
if (this.parent[i] !== i) this.parent[i] = this.find(this.parent[i]); | ||
return this.parent[i]; | ||
} | ||
|
||
union(i: number, j: number): void { | ||
let rootI = this.find(i), | ||
rootJ = this.find(j); | ||
if (rootI !== rootJ) { | ||
if (this.rank[rootI] > this.rank[rootJ]) this.parent[rootJ] = rootI; | ||
else if (this.rank[rootI] < this.rank[rootJ]) this.parent[rootI] = rootJ; | ||
else { | ||
this.parent[rootJ] = rootI; | ||
this.rank[rootI]++; | ||
} | ||
} | ||
} | ||
} | ||
|
||
interface Edge { | ||
i: number; | ||
j: number; | ||
dist: number; | ||
} | ||
|
||
function computeMST(points: THREE.Vector3[]): number[][] { | ||
const edges: Edge[] = []; | ||
const numPoints = points.length; | ||
|
||
// Create all possible edges with distances | ||
for (let i = 0; i < numPoints; i++) { | ||
for (let j = i + 1; j < numPoints; j++) { | ||
const dist = points[i].distanceTo(points[j]); | ||
edges.push({ i, j, dist }); | ||
} | ||
} | ||
|
||
// Sort edges by distance (Kruskal's Algorithm) | ||
edges.sort((a, b) => a.dist - b.dist); | ||
|
||
// Compute MST using Kruskal’s Algorithm | ||
const ds = new DisjointSet(numPoints); | ||
const mst: number[][] = Array.from({ length: numPoints }, () => []); | ||
|
||
for (const { i, j } of edges) { | ||
if (ds.find(i) !== ds.find(j)) { | ||
ds.union(i, j); | ||
mst[i].push(j); | ||
mst[j].push(i); | ||
} | ||
} | ||
|
||
return mst; | ||
} | ||
|
||
function traverseMstDfs(mst: number[][], startIdx = 0): number[] { | ||
const visited = new Set<number>(); | ||
const orderedPoints: number[] = []; | ||
|
||
function dfs(node: number) { | ||
if (visited.has(node)) return; | ||
visited.add(node); | ||
orderedPoints.push(node); | ||
for (let neighbor of mst[node]) { | ||
dfs(neighbor); | ||
} | ||
} | ||
|
||
dfs(startIdx); | ||
return orderedPoints; | ||
} | ||
|
||
function computePathLength(points: THREE.Vector3[], order: number[]): number { | ||
let length = 0; | ||
for (let i = 0; i < order.length - 1; i++) { | ||
length += points[order[i]].distanceTo(points[order[i + 1]]); | ||
} | ||
return length; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Broken markdown link anchor
The link to the Toolkits section uses
#Toolkits
, but Markdown heading anchors are typically all lowercase (#toolkits
). This may break the link. Please update accordingly:📝 Committable suggestion