Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
029df28
feat(python): add first version of linear reg workflow
seankmartin Sep 12, 2025
eaf602a
feat(python): update the lin reg with more functions and TODOs
seankmartin Sep 12, 2025
dbbfef4
feat: lin reg export and lin reg unlink
seankmartin Sep 15, 2025
97837bf
feat: add shader and annotation props to lin reg
seankmartin Sep 15, 2025
8fbfeb4
chore: add light typing to lin reg
seankmartin Sep 15, 2025
990ee0d
feat: small update to print of updates
seankmartin Sep 15, 2025
f5eba10
feat: remove dims const
seankmartin Sep 18, 2025
f18f207
feat: scaffold n dim lsqs affine
seankmartin Sep 18, 2025
b9ec70c
fix: correct calling affine
seankmartin Sep 18, 2025
e58ca3a
refactor: standardise terms
seankmartin Sep 18, 2025
dfd654c
feat: improve perf with debounce of lin reg
seankmartin Sep 18, 2025
b8da450
feat: two diff debounces in lin reg update
seankmartin Sep 19, 2025
61488cf
fix: correct caching of number
seankmartin Sep 19, 2025
c91e036
feat: remove fixed dims
seankmartin Sep 19, 2025
085563e
fix: handle channel dims
seankmartin Sep 19, 2025
79793a7
feat: use input state in lin reg to be more robust
seankmartin Sep 19, 2025
38d56f7
feat: making lin reg pipeline more custom
seankmartin Sep 22, 2025
0d467f5
fix: correct a number of TODOs for lin reg
seankmartin Sep 22, 2025
c812212
feat: add different fits to reg workflow
seankmartin Sep 25, 2025
4cddc66
feat(python): add layout display dims unlinked to state
seankmartin Sep 25, 2025
83a1b30
refactor: in progress with large lin reg workflow improvement
seankmartin Sep 25, 2025
bcafa3c
feat: filling out full lin reg workflow
seankmartin Sep 29, 2025
db7f234
feat: apply affine to annotations
seankmartin Sep 29, 2025
d9855e6
feat: small updates
seankmartin Oct 3, 2025
d6ef631
refactor: clarify function names and class
seankmartin Oct 6, 2025
94e5270
feat: save progress of various fixes
seankmartin Oct 6, 2025
409c744
refactor: remove second co-ord space
seankmartin Oct 7, 2025
b105a58
refactor: continue to clarify with only one path
seankmartin Oct 7, 2025
363509b
fix: address various assumptions to make pipeline more stable
seankmartin Oct 8, 2025
1fa3d05
feat: small updates
seankmartin Oct 8, 2025
0bedda6
docs: add info on how to use lin reg scrtip
seankmartin Jan 9, 2026
152ca1a
fix: handle local channels in affine application
seankmartin Jan 9, 2026
0cee9ef
fix: correct estimation algo
seankmartin Jan 12, 2026
dd663a3
fix: correct translation
seankmartin Jan 12, 2026
e40fc8a
test: adding tests to lin reg
seankmartin Jan 12, 2026
c18a24e
fix: correct pulling moving dims
seankmartin Jan 13, 2026
a90b3f8
feat: add output affine
seankmartin Jan 13, 2026
5b330a6
feat: add dump
seankmartin Jan 13, 2026
d5d2fa0
feat: allow annotation non display dims to not clip
seankmartin Jan 15, 2026
1dc57b9
feat: add clip dims to python state
seankmartin Jan 16, 2026
749a129
refactor: largely clean up the linear reg workflow
seankmartin Jan 16, 2026
1c15819
feat: allow to force non-affine. We also try detect it from rank
seankmartin Jan 16, 2026
f7c52e1
feat: add local affine estimate
seankmartin Jan 16, 2026
515027a
fix: correct for t/time
seankmartin Jan 16, 2026
939b81d
fix: correct state dump
seankmartin Jan 16, 2026
265890f
fix: correct picking up from a saved point
seankmartin Jan 16, 2026
4d20cbd
chore: update docs
seankmartin Jan 16, 2026
4dd9b90
refactor: rename config state
seankmartin Jan 16, 2026
3715cd4
Merge branch 'master' into feat/example-linear-registration
seankmartin Jan 19, 2026
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
1,177 changes: 1,177 additions & 0 deletions python/examples/example_linear_registration.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions python/neuroglancer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
LocalAnnotationLayer, # noqa: F401
ManagedLayer, # noqa: F401
Layers, # noqa: F401
LinkedDisplayDimensions, # noqa: F401
LinkedPosition, # noqa: F401
LinkedZoomFactor, # noqa: F401
LinkedDepthRange, # noqa: F401
Expand Down
15 changes: 15 additions & 0 deletions python/neuroglancer/viewer_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1513,6 +1513,14 @@ class Linked(LinkedType):

return Linked

if typing.TYPE_CHECKING or _BUILDING_DOCS:
_LinkedDisplayDimensionsBase = LinkedType[list[str]]
else:
_LinkedDisplayDimensionsBase = make_linked_navigation_type(typed_list(str), lambda x: x)

@export
class LinkedDisplayDimensions(_LinkedDisplayDimensionsBase):
__slots__ = ()

if typing.TYPE_CHECKING or _BUILDING_DOCS:
_LinkedPositionBase = LinkedType[np.typing.NDArray[np.float32]]
Expand Down Expand Up @@ -1741,6 +1749,9 @@ class LayerGroupViewer(JsonObjectWrapper):
layers = wrapped_property("layers", typed_list(str))
layout = wrapped_property("layout", data_panel_layout_wrapper("xy"))
position = wrapped_property("position", LinkedPosition)
display_dimensions = displayDimensions = wrapped_property(
"displayDimensions", LinkedDisplayDimensions
)
velocity = wrapped_property(
"velocity", typed_map(key_type=str, value_type=DimensionPlaybackVelocity)
)
Expand Down Expand Up @@ -1963,6 +1974,10 @@ class ViewerState(JsonObjectWrapper):
tool_palettes = toolPalettes = wrapped_property(
"toolPalettes", typed_map(key_type=str, value_type=ToolPalette)
)
clip_dimensions_weight = clipDimensionsWeight = wrapped_property(
"clipDimensionsWeight",
optional(typed_map(key_type=str, value_type=typed_map(key_type=str, value_type=float)))
)
selection = wrapped_property("selection", DataSelectionState)

@staticmethod
Expand Down
122 changes: 119 additions & 3 deletions src/annotation/renderlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ interface AttachmentState {
chunkTransform: ValueOrError<ChunkTransformParameters>;
displayDimensionRenderInfo: DisplayDimensionRenderInfo;
chunkRenderParameters: AnnotationChunkRenderParameters | undefined;
// Map from layer name to map of dimension name to clip weight
clipDimensionsWeight: Map<string, Map<string, number>>;
}

type TransformedAnnotationSource = FrontendTransformedSource<
Expand All @@ -414,27 +416,52 @@ interface SpatiallyIndexedValidAttachmentState extends AttachmentState {

function getAnnotationProjectionParameters(
chunkDisplayTransform: ChunkDisplayTransformParameters,
layerName: string | undefined,
clipDimensionsWeight: Map<string, Map<string, number>>,
) {
const { chunkTransform } = chunkDisplayTransform;
const { unpaddedRank } = chunkTransform.modelTransform;
const { unpaddedRank, layerDimensionNames } = chunkTransform.modelTransform;
const modelClipBounds = new Float32Array(unpaddedRank * 2);
const renderSubspaceTransform = new Float32Array(unpaddedRank * 3);
renderSubspaceTransform.fill(0);
modelClipBounds.fill(1, unpaddedRank);
const { numChunkDisplayDims, chunkDisplayDimensionIndices } =
chunkDisplayTransform;

// Set display dimensions to not clip (multiplier = 0)
for (let i = 0; i < numChunkDisplayDims; ++i) {
const chunkDim = chunkDisplayDimensionIndices[i];
modelClipBounds[unpaddedRank + chunkDim] = 0;
renderSubspaceTransform[chunkDim * 3 + i] = 1;
}

// Apply custom clip dimension weights to non display dims
if (layerName !== undefined) {
const dimWeights = clipDimensionsWeight.get(layerName);
if (dimWeights !== undefined && dimWeights.size > 0) {
for (const [dimName, weight] of dimWeights) {
// TODO not sure if can directly use layer names
// TODO ensure not in chunk display dims
const dimIndex = layerDimensionNames.indexOf(dimName);
if (dimIndex !== -1 && dimIndex < unpaddedRank) {
// Set the multiplier to the specified weight for this dimension
// Weight of 0.0 = no clipping, 1.0 = full clipping
const newIndex = unpaddedRank + dimIndex;
modelClipBounds[newIndex] = weight;
}
}
}
}

return { modelClipBounds, renderSubspaceTransform };
}

function getChunkRenderParameters(
chunkTransform: ValueOrError<ChunkTransformParameters>,
displayDimensionRenderInfo: DisplayDimensionRenderInfo,
messages: MessageList,
layerName: string | undefined,
clipDimensionsWeight: Map<string, Map<string, number>>,
): AnnotationChunkRenderParameters | undefined {
messages.clearMessages();
const returnError = (message: string) => {
Expand All @@ -458,7 +485,11 @@ function getChunkRenderParameters(
return returnError((e as Error).message);
}
const { modelClipBounds, renderSubspaceTransform } =
getAnnotationProjectionParameters(chunkDisplayTransform);
getAnnotationProjectionParameters(
chunkDisplayTransform,
layerName,
clipDimensionsWeight,
);
return {
chunkTransform,
chunkDisplayTransform,
Expand Down Expand Up @@ -540,13 +571,43 @@ function AnnotationRenderLayer<
const { chunkTransform } = this;
const displayDimensionRenderInfo =
attachment.view.displayDimensionRenderInfo.value;

// Get clip dimensions weight from the viewer
const clipDimensionsWeight = new Map<string, Map<string, number>>();
const viewer = (attachment.view as any).viewer;
const trackableClipDimensionsWeight = viewer?.clipDimensionsWeight;
if (trackableClipDimensionsWeight?.value) {
for (const [
layerName,
dimWeights,
] of trackableClipDimensionsWeight.value) {
clipDimensionsWeight.set(layerName, new Map(dimWeights));
}
}

// Listen for changes to clipDimensionsWeight
if (trackableClipDimensionsWeight) {
attachment.registerDisposer(
trackableClipDimensionsWeight.changed.add(() => {
this.updateAttachmentState(attachment);
this.redrawNeeded.dispatch();
}),
);
}

// Get the layer name
const layerName = this.base.state?.dataSource?.layer?.managedLayer?.name;

attachment.state = {
chunkTransform,
displayDimensionRenderInfo,
clipDimensionsWeight,
chunkRenderParameters: getChunkRenderParameters(
chunkTransform,
displayDimensionRenderInfo,
attachment.messages,
layerName,
clipDimensionsWeight,
),
};
}
Expand All @@ -559,20 +620,61 @@ function AnnotationRenderLayer<
const { chunkTransform } = this;
const displayDimensionRenderInfo =
attachment.view.displayDimensionRenderInfo.value;

// Get clip dimensions weight from the viewer
const clipDimensionsWeight = new Map<string, Map<string, number>>();
const viewer = (attachment.view as any).viewer;
const trackableClipDimensionsWeight = viewer?.clipDimensionsWeight;
if (trackableClipDimensionsWeight?.value) {
for (const [
layerName,
dimWeights,
] of trackableClipDimensionsWeight.value) {
clipDimensionsWeight.set(layerName, new Map(dimWeights));
}
}

// Check if clipDimensionsWeight has changed
let clipWeightsChanged =
state.clipDimensionsWeight.size !== clipDimensionsWeight.size;
if (!clipWeightsChanged) {
for (const [layerName, dimWeights] of clipDimensionsWeight) {
const oldDimWeights = state.clipDimensionsWeight.get(layerName);
if (!oldDimWeights || oldDimWeights.size !== dimWeights.size) {
clipWeightsChanged = true;
break;
}
for (const [dimName, weight] of dimWeights) {
if (oldDimWeights.get(dimName) !== weight) {
clipWeightsChanged = true;
break;
}
}
if (clipWeightsChanged) break;
}
}

// Get the layer name
const layerName = this.base.state?.dataSource?.layer?.managedLayer?.name;

if (
state !== undefined &&
state.chunkTransform === chunkTransform &&
state.displayDimensionRenderInfo === displayDimensionRenderInfo
state.displayDimensionRenderInfo === displayDimensionRenderInfo &&
!clipWeightsChanged
) {
return state.chunkRenderParameters;
}
state.chunkTransform = chunkTransform;
state.displayDimensionRenderInfo = displayDimensionRenderInfo;
state.clipDimensionsWeight = clipDimensionsWeight;
const chunkRenderParameters = (state.chunkRenderParameters =
getChunkRenderParameters(
chunkTransform,
displayDimensionRenderInfo,
attachment.messages,
layerName,
clipDimensionsWeight,
));
return chunkRenderParameters;
}
Expand Down Expand Up @@ -1009,12 +1111,23 @@ const SpatiallyIndexedAnnotationLayer = <
>,
) {
super.attach(attachment);

// Get clip dimensions weight from the viewer
const viewer = (attachment.view as any).viewer;
const trackableClipDimensionsWeight = viewer?.clipDimensionsWeight || {
value: new Map(),
};

// Get the layer name
const layerName = this.base.state?.dataSource?.layer?.managedLayer?.name;

attachment.state!.sources = attachment.registerDisposer(
registerNested(
(
context: RefCounted,
transform: RenderLayerTransformOrError,
displayDimensionRenderInfo: DisplayDimensionRenderInfo,
clipDimensionsWeightValue: Map<string, Map<string, number>>,
) => {
const transformedSources = getVolumetricTransformedSources(
displayDimensionRenderInfo,
Expand All @@ -1033,6 +1146,8 @@ const SpatiallyIndexedAnnotationLayer = <
tsource,
getAnnotationProjectionParameters(
tsource.chunkDisplayTransform,
layerName,
clipDimensionsWeightValue,
),
);
}
Expand All @@ -1052,6 +1167,7 @@ const SpatiallyIndexedAnnotationLayer = <
},
this.base.state.transform,
attachment.view.displayDimensionRenderInfo,
trackableClipDimensionsWeight,
),
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/data_panel_layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { NullarySignal } from "#src/util/signal.js";
import type { Trackable } from "#src/util/trackable.js";
import { optionallyRestoreFromJsonMember } from "#src/util/trackable.js";
import { WatchableMap } from "#src/util/watchable_map.js";
import type { TrackableClipDimensionsWeight } from "#src/viewer.js";
import type { VisibilityPrioritySpecification } from "#src/viewer_state.js";
import { DisplayDimensionsWidget } from "#src/widget/display_dimensions_widget.js";
import type { ScaleBarOptions } from "#src/widget/scale_bar.js";
Expand Down Expand Up @@ -101,6 +102,7 @@ export interface ViewerUIState
crossSectionBackgroundColor: TrackableRGB;
perspectiveViewBackgroundColor: TrackableRGB;
hideCrossSectionBackground3D: TrackableBoolean;
clipDimensionsWeight: TrackableClipDimensionsWeight;
pickRadius: TrackableValue<number>;
}

Expand Down Expand Up @@ -180,6 +182,7 @@ export function getCommonViewerState(viewer: ViewerUIState) {
wireFrame: viewer.wireFrame,
enableAdaptiveDownsampling: viewer.enableAdaptiveDownsampling,
visibleLayerRoles: viewer.visibleLayerRoles,
clipDimensionsWeight: viewer.clipDimensionsWeight,
selectedLayer: viewer.selectedLayer,
visibility: viewer.visibility,
scaleBarOptions: viewer.scaleBarOptions,
Expand Down
5 changes: 5 additions & 0 deletions src/layer_group_viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
CompoundTrackable,
optionallyRestoreFromJsonMember,
} from "#src/util/trackable.js";
import type { TrackableClipDimensionsWeight } from "#src/viewer.js";
import type { WatchableVisibilityPriority } from "#src/visibility_priority/frontend.js";
import { EnumSelectWidget } from "#src/widget/enum_widget.js";
import type { TrackableScaleBarOptions } from "#src/widget/scale_bar.js";
Expand All @@ -104,6 +105,7 @@ export interface LayerGroupViewerState {
crossSectionBackgroundColor: TrackableRGB;
perspectiveViewBackgroundColor: TrackableRGB;
hideCrossSectionBackground3D: TrackableBoolean;
clipDimensionsWeight: TrackableClipDimensionsWeight;
pickRadius: TrackableValue<number>;
}

Expand Down Expand Up @@ -390,6 +392,9 @@ export class LayerGroupViewer extends RefCounted {
get scaleBarOptions() {
return this.viewerState.scaleBarOptions;
}
get clipDimensionsWeight() {
return this.viewerState.clipDimensionsWeight;
}
layerPanel: LayerBar | undefined;
layout: DataPanelLayoutContainer;
toolBinder: LocalToolBinder<this>;
Expand Down
1 change: 1 addition & 0 deletions src/layer_groups_layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ function getCommonViewerState(viewer: Viewer) {
crossSectionBackgroundColor: viewer.crossSectionBackgroundColor,
perspectiveViewBackgroundColor: viewer.perspectiveViewBackgroundColor,
hideCrossSectionBackground3D: viewer.hideCrossSectionBackground3D,
clipDimensionsWeight: viewer.clipDimensionsWeight,
pickRadius: viewer.uiConfiguration.pickRadius,
};
}
Expand Down
Loading