-
Notifications
You must be signed in to change notification settings - Fork 24
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
Support transforming the skeleton layer with affine and TSP transforms #7588
Conversation
…ore affineMatrixInv for affine transforms to reduce inversion loss
… or getNodePosition(...)
frontend/javascripts/oxalis/geometries/materials/edge_shader.ts
Outdated
Show resolved
Hide resolved
frontend/javascripts/oxalis/geometries/materials/node_shader.ts
Outdated
Show resolved
Hide resolved
frontend/javascripts/oxalis/geometries/materials/node_shader.ts
Outdated
Show resolved
Hide resolved
@fm3 I adapted the front-end so that a node position is transformed at the latest possible time (e.g., in the shader or shortly before presenting the current position in the UI). Within the redux store, the untransformed position is stored and that is also sent to the server. To make the semantics clear, I renamed |
… not much more expensive than an if in the shader
That’s right. We would either have to migrate all existing update actions or make both fields optional and use getOrElse logic, with an explicit error if both fields are None. While that would be possible, I don’t know if it would be nicer than just doing the renaming as you said |
Do you have a preference? I think, I'm against the getOrElse logic. This leaves us with two options: 1) keep it as is, 2) rename all existing update actions. I think, 2 would be a bit cleaner (but can also be weird, since |
@philippotto Reviewing, the following came to my mind: Do you think it would be helpful to leverage typescript to prevent mixing untransformed and transformed positions? The way to do it would be nominal/branded/opaque types, see this example. |
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.
Very cool that this is supported now! I'll report back after testing 🧪
frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.ts
Show resolved
Hide resolved
frontend/javascripts/oxalis/geometries/materials/node_shader.ts
Outdated
Show resolved
Hide resolved
frontend/javascripts/oxalis/view/right-border-tabs/skeleton_tab_view.tsx
Show resolved
Hide resolved
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.
Works very well 🎉 Mainly, I think the NML export/import could be improved as well as two other small issues. My notes from testing:
- The icons for the skeleton layer in the layers tab are not vertically aligned
- The tps correspondences are serialized as
<correspondence source="[570.3021454931336,404.5548993008739,502.2248079151061]" target="[568.1197424615385,528.0132258461539,1622.1119725846156]" />
. Is the[570.3021454931336,404.5548993008739,502.2248079151061]
part XML-idiomatic and can it be parsed without resorting to eval or custom string splitting? Otherwise I'd change it to something more idiomatic. -
As mentioned in one of the comments: Exporting a transformed skeleton and then importing it into the same annotation when the skeleton layer is no longer transformed leads to the nodes ending up in the wrong location. When fixing this, pay attention to the fact that NMLs currently can also be imported when the skeleton is transformed.We decided against this for now. See comment. -
I'm not sure whether this broke in this PR: The merger mode modal states "8 Replace the color of the current active tree and its mapped segments with a new one.", but pressing 8 doesn't change anything.see below
In principle, I think, it's a pretty cool idea :) However, I'm a bit afraid that it will require quite some effort for two reasons. First, it only adds safety when the consumer of the node position is strict and requires a special const getPos = (node: Readonly<MutableNode>) => getNodePosition(node, state);
for (const edge of tree.edges.all()) {
const sourceNode = tree.nodes.get(edge.source);
const targetNode = tree.nodes.get(edge.target);
lengthNmAcc += V3.scaledDist(getPos(sourceNode), getPos(targetNode), datasetScale);
lengthVxAcc += V3.length(V3.sub(getPos(sourceNode), getPos(targetNode)));
} cannot easily require a Second, mutation of positions via For these reasons, I'd rather not do this refactoring right now. I hope you agree! |
It can be parsed with |
Definitely agree, thanks for thinking this through :)
Ok, convinced 👍 |
…een working since 8124079
@daniel-wer Thanks again for your review and testing 🙏 Now that our discussion has settled for now, the PR is ready from my point of view. There's certainly potential for improvement in the future, but as mentioned in another comment, I'd defer this for now.
Very interesting! I did some digging and it turns out that this feature was removed a while back in 8124079. However, the modal wasn't updated then. I simply removed the hint from the modal now, because nobody complained about this feature being gone. |
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.
Code LGTM and my previous points from testing were addressed :)
- During a renewed testing I noticed that the skeleton can still be modified even when the skeleton layer is transformed either by using keyboard shortcuts (
Ctrl
+Arrow Keys
to move nodes, for example) or by using the context menu (Rightclick - Create Node Here). I'm also wondering whether "Import Agglomerate Skeleton" or "Import Synapses" from the context menu would do the correct thing for transformed skeleton layers or whether the entries should be disabled (maybe they are already, I didn't have an agglomerate file available for a transformed dataset).
…s rendered with transforms
Good find! I fixed it now :)
To properly support this scenario, we would need the ability that a skeleton layer uses the same transforms as a volume layer. We should tackle this once the need occurs :) |
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.
LGTM 🎉
This PR takes care of transforming skeleton nodes (in the shader and when interacting with the positions) when necessary. During saving, the original node positions are stored (i.e., without any applied transformations).
In contrast to data layers (color and segmentation), the skeleton layer doesn't have an own transforms property. Instead it is treated like a layer that has no transforms at all. If a dataset with transformed layers exist, these layers will be transformed according to their transformations while the skeleton layer has the identity transforms. When one of the layers that have transforms is toggled to be rendered natively, the inverse transform is applied to the skeleton layer. As a result, the nodes will be transformed the same way the other layers are transformed.
When using the front-end export of skeletons to NML with transforms, the currently rendered node positions are written while the active transformation is encoded in the parameters section like the following. The array notation is loosely based on the TrakEm format (that uses something like
<t2_patch oid=“33” width=“949.0” height=“901.0” transform=“matrix(1.0,0.0,-0.0,1.0,53.0,72.0)... />
).Affine:
TPS:
If we decide in the future, that we always want to store the untransformed positions in the NML, we can do so and then change
positionsAreTransformed="true"
to false.URL of deployed dev instance (used for testing):
Steps to test:
TODOs:
[ ] should the node property "position" in the DB be renamed to "transformedPosition" (as the front-end did it?)Issues:
(Please delete unneeded items, merge only when none are left open)