Skip to content

Commit acb6a43

Browse files
seankmartinvidhyalonganiAiga115
authored
feat: high resolution screenshot from viewer (#646)
Co-authored-by: vidhya-metacell <vidhya@metacell.us> Co-authored-by: Aiga115 <aiguljhyldyzbekkyzy@gmail.com>
1 parent 186674a commit acb6a43

File tree

17 files changed

+2390
-22
lines changed

17 files changed

+2390
-22
lines changed

python/neuroglancer/tool/screenshot.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,11 @@ def define_state_modification_args(ap: argparse.ArgumentParser):
596596
type=float,
597597
help="Multiply projection view scale by specified factor.",
598598
)
599+
ap.add_argument(
600+
"--resolution-scale-factor",
601+
type=float,
602+
help="Divide cross section view scale by specified factor. E.g. a 2000x2000 output with a resolution scale factor of 2 will have the same FOV as a 1000x1000 output.",
603+
)
599604
ap.add_argument(
600605
"--system-memory-limit",
601606
type=int,
@@ -635,6 +640,8 @@ def apply_state_modifications(
635640
state.show_default_annotations = args.show_default_annotations
636641
if args.projection_scale_multiplier is not None:
637642
state.projection_scale *= args.projection_scale_multiplier
643+
if args.resolution_scale_factor is not None:
644+
state.cross_section_scale /= args.resolution_scale_factor
638645
if args.cross_section_background_color is not None:
639646
state.cross_section_background_color = args.cross_section_background_color
640647

python/neuroglancer/viewer_config_state.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,34 @@ class LayerSelectedValues(_LayerSelectedValuesBase):
9797
"""Specifies the data values associated with the current mouse position."""
9898

9999

100+
@export
101+
class PanelResolutionData(JsonObjectWrapper):
102+
__slots__ = ()
103+
type = wrapped_property("type", str)
104+
width = wrapped_property("width", int)
105+
height = wrapped_property("height", int)
106+
resolution = wrapped_property("resolution", str)
107+
108+
109+
@export
110+
class LayerResolutionData(JsonObjectWrapper):
111+
__slots__ = ()
112+
name = wrapped_property("name", str)
113+
type = wrapped_property("type", str)
114+
resolution = wrapped_property("resolution", str)
115+
116+
117+
@export
118+
class ScreenshotResolutionMetadata(JsonObjectWrapper):
119+
__slots__ = ()
120+
panel_resolution_data = panelResolutionData = wrapped_property(
121+
"panelResolutionData", typed_list(PanelResolutionData)
122+
)
123+
layer_resolution_data = layerResolutionData = wrapped_property(
124+
"layerResolutionData", typed_list(LayerResolutionData)
125+
)
126+
127+
100128
@export
101129
class ScreenshotReply(JsonObjectWrapper):
102130
__slots__ = ()
@@ -106,6 +134,9 @@ class ScreenshotReply(JsonObjectWrapper):
106134
height = wrapped_property("height", int)
107135
image_type = imageType = wrapped_property("imageType", str)
108136
depth_data = depthData = wrapped_property("depthData", optional(base64.b64decode))
137+
resolution_metadata = resolutionMetadata = wrapped_property(
138+
"resolutionMetadata", ScreenshotResolutionMetadata
139+
)
109140

110141
@property
111142
def image_pixels(self):

src/display_context.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import { FramerateMonitor } from "#src/util/framerate.js";
2525
import type { mat4 } from "#src/util/geom.js";
2626
import { parseFixedLengthArray, verifyFloat01 } from "#src/util/json.js";
2727
import { NullarySignal } from "#src/util/signal.js";
28+
import {
29+
TrackableScreenshotMode,
30+
ScreenshotMode,
31+
} from "#src/util/trackable_screenshot_mode.js";
2832
import type { WatchableVisibilityPriority } from "#src/visibility_priority/frontend.js";
2933
import type { GL } from "#src/webgl/context.js";
3034
import { initializeWebGL } from "#src/webgl/context.js";
@@ -135,7 +139,7 @@ export abstract class RenderedPanel extends RefCounted {
135139

136140
abstract isReady(): boolean;
137141

138-
ensureBoundsUpdated() {
142+
ensureBoundsUpdated(canScaleForScreenshot: boolean = false) {
139143
const { context } = this;
140144
context.ensureBoundsUpdated();
141145
const { boundsGeneration } = context;
@@ -221,8 +225,18 @@ export abstract class RenderedPanel extends RefCounted {
221225
0,
222226
clippedBottom - clippedTop,
223227
));
224-
viewport.logicalWidth = logicalWidth;
225-
viewport.logicalHeight = logicalHeight;
228+
if (
229+
this.context.screenshotMode.value !== ScreenshotMode.OFF &&
230+
canScaleForScreenshot
231+
) {
232+
viewport.width = logicalWidth * screenToCanvasPixelScaleX;
233+
viewport.height = logicalHeight * screenToCanvasPixelScaleY;
234+
viewport.logicalWidth = logicalWidth * screenToCanvasPixelScaleX;
235+
viewport.logicalHeight = logicalHeight * screenToCanvasPixelScaleY;
236+
} else {
237+
viewport.logicalWidth = logicalWidth;
238+
viewport.logicalHeight = logicalHeight;
239+
}
226240
viewport.visibleLeftFraction = (clippedLeft - logicalLeft) / logicalWidth;
227241
viewport.visibleTopFraction = (clippedTop - logicalTop) / logicalHeight;
228242
viewport.visibleWidthFraction = clippedWidth / logicalWidth;
@@ -410,6 +424,9 @@ export class DisplayContext extends RefCounted implements FrameNumberCounter {
410424
rootRect: DOMRect | undefined;
411425
resizeGeneration = 0;
412426
boundsGeneration = -1;
427+
screenshotMode: TrackableScreenshotMode = new TrackableScreenshotMode(
428+
ScreenshotMode.OFF,
429+
);
413430
force3DHistogramForAutoRange = false;
414431
private framerateMonitor = new FramerateMonitor();
415432

@@ -599,8 +616,10 @@ export class DisplayContext extends RefCounted implements FrameNumberCounter {
599616
const { resizeGeneration } = this;
600617
if (this.boundsGeneration === resizeGeneration) return;
601618
const { canvas } = this;
602-
canvas.width = canvas.offsetWidth;
603-
canvas.height = canvas.offsetHeight;
619+
if (this.screenshotMode.value === ScreenshotMode.OFF) {
620+
canvas.width = canvas.offsetWidth;
621+
canvas.height = canvas.offsetHeight;
622+
}
604623
this.canvasRect = canvas.getBoundingClientRect();
605624
this.rootRect = this.container.getBoundingClientRect();
606625
this.boundsGeneration = resizeGeneration;

src/overlay.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,15 @@ export class Overlay extends RefCounted {
4747
document.body.appendChild(container);
4848
this.registerDisposer(new KeyboardEventBinder(this.container, this.keyMap));
4949
this.registerEventListener(container, "action:close", () => {
50-
this.dispose();
50+
this.close();
5151
});
5252
content.focus();
5353
}
5454

55+
close() {
56+
this.dispose();
57+
}
58+
5559
disposed() {
5660
--overlaysOpen;
5761
document.body.removeChild(this.container);

src/perspective_view/panel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ export class PerspectivePanel extends RenderedDataPanel {
605605
}
606606

607607
ensureBoundsUpdated() {
608-
super.ensureBoundsUpdated();
608+
super.ensureBoundsUpdated(true /* canScaleForScreenshot */);
609609
this.projectionParameters.setViewport(this.renderViewport);
610610
}
611611

src/python_integration/screenshots.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,54 @@ import { convertEndian32, Endianness } from "#src/util/endian.js";
2828
import { verifyOptionalString } from "#src/util/json.js";
2929
import { Signal } from "#src/util/signal.js";
3030
import { getCachedJson } from "#src/util/trackable.js";
31+
import { ScreenshotMode } from "#src/util/trackable_screenshot_mode.js";
32+
import type { ResolutionMetadata } from "#src/util/viewer_resolution_stats.js";
33+
import { getViewerResolutionMetadata } from "#src/util/viewer_resolution_stats.js";
3134
import type { Viewer } from "#src/viewer.js";
3235

36+
export interface ScreenshotResult {
37+
id: string;
38+
image: string;
39+
imageType: string;
40+
depthData: string | undefined;
41+
width: number;
42+
height: number;
43+
resolutionMetadata: ResolutionMetadata;
44+
}
45+
46+
export interface ScreenshotActionState {
47+
viewerState: any;
48+
selectedValues: any;
49+
screenshot: ScreenshotResult;
50+
}
51+
52+
export interface ScreenshotChunkStatistics {
53+
downloadLatency: number;
54+
visibleChunksDownloading: number;
55+
visibleChunksFailed: number;
56+
visibleChunksGpuMemory: number;
57+
visibleChunksSystemMemory: number;
58+
visibleChunksTotal: number;
59+
visibleGpuMemory: number;
60+
}
61+
62+
export interface StatisticsActionState {
63+
viewerState: any;
64+
selectedValues: any;
65+
screenshotStatistics: {
66+
id: string;
67+
chunkSources: any[];
68+
total: ScreenshotChunkStatistics;
69+
};
70+
}
71+
3372
export class ScreenshotHandler extends RefCounted {
34-
sendScreenshotRequested = new Signal<(state: any) => void>();
35-
sendStatisticsRequested = new Signal<(state: any) => void>();
73+
sendScreenshotRequested = new Signal<
74+
(state: ScreenshotActionState) => void
75+
>();
76+
sendStatisticsRequested = new Signal<
77+
(state: StatisticsActionState) => void
78+
>();
3679
requestState = new TrackableValue<string | undefined>(
3780
undefined,
3881
verifyOptionalString,
@@ -124,12 +167,14 @@ export class ScreenshotHandler extends RefCounted {
124167
return;
125168
}
126169
const { viewer } = this;
127-
if (!viewer.isReady()) {
170+
const shouldForceScreenshot =
171+
this.viewer.display.screenshotMode.value === ScreenshotMode.FORCE;
172+
if (!viewer.isReady() && !shouldForceScreenshot) {
128173
this.wasAlreadyVisible = false;
129174
this.throttledSendStatistics(requestState);
130175
return;
131176
}
132-
if (!this.wasAlreadyVisible) {
177+
if (!this.wasAlreadyVisible && !shouldForceScreenshot) {
133178
this.throttledSendStatistics(requestState);
134179
this.wasAlreadyVisible = true;
135180
this.debouncedMaybeSendScreenshot();
@@ -140,6 +185,7 @@ export class ScreenshotHandler extends RefCounted {
140185
this.throttledSendStatistics.cancel();
141186
viewer.display.draw();
142187
const screenshotData = viewer.display.canvas.toDataURL();
188+
const resolutionMetadata = getViewerResolutionMetadata(viewer);
143189
const { width, height } = viewer.display.canvas;
144190
const prefix = "data:image/png;base64,";
145191
let imageType: string;
@@ -169,6 +215,7 @@ export class ScreenshotHandler extends RefCounted {
169215
depthData,
170216
width,
171217
height,
218+
resolutionMetadata,
172219
},
173220
};
174221

src/single_mesh/frontend.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,18 @@ import {
2020
ChunkSource,
2121
WithParameters,
2222
} from "#src/chunk_manager/frontend.js";
23+
import {
24+
makeCoordinateSpace,
25+
makeIdentityTransform,
26+
} from "#src/coordinate_transform.js";
27+
import type {
28+
DataSource,
29+
GetKvStoreBasedDataSourceOptions,
30+
KvStoreBasedDataSourceProvider,
31+
} from "#src/datasource/index.js";
2332
import { WithSharedKvStoreContext } from "#src/kvstore/chunk_source_frontend.js";
2433
import type { SharedKvStoreContext } from "#src/kvstore/frontend.js";
34+
import { ensureEmptyUrlSuffix } from "#src/kvstore/url.js";
2535
import type { PickState, VisibleLayerInfo } from "#src/layer/index.js";
2636
import type { PerspectivePanel } from "#src/perspective_view/panel.js";
2737
import type { PerspectiveViewRenderContext } from "#src/perspective_view/render_layer.js";
@@ -85,16 +95,6 @@ import {
8595
TextureFormat,
8696
} from "#src/webgl/texture_access.js";
8797
import { SharedObject } from "#src/worker_rpc.js";
88-
import type {
89-
DataSource,
90-
GetKvStoreBasedDataSourceOptions,
91-
KvStoreBasedDataSourceProvider,
92-
} from "#src/datasource/index.js";
93-
import { ensureEmptyUrlSuffix } from "#src/kvstore/url.js";
94-
import {
95-
makeCoordinateSpace,
96-
makeIdentityTransform,
97-
} from "#src/coordinate_transform.js";
9898

9999
const DEFAULT_FRAGMENT_MAIN = `void main() {
100100
emitGray();

src/sliceview/panel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ export class SliceViewPanel extends RenderedDataPanel {
435435
}
436436

437437
ensureBoundsUpdated() {
438-
super.ensureBoundsUpdated();
438+
super.ensureBoundsUpdated(true /* canScaleForScreenshot */);
439439
this.sliceView.projectionParameters.setViewport(this.renderViewport);
440440
}
441441

src/sliceview/volume/renderlayer.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ export abstract class SliceViewVolumeRenderLayer<
341341
>;
342342
private tempChunkPosition: Float32Array;
343343
shaderParameters: WatchableValueInterface<ShaderParameters>;
344+
highestResolutionLoadedVoxelSize: Float32Array | undefined;
344345
private vertexIdHelper: VertexIdHelper;
345346

346347
constructor(
@@ -570,6 +571,7 @@ void main() {
570571
this.chunkManager.chunkQueueManager.frameNumberCounter.frameNumber,
571572
);
572573
}
574+
this.highestResolutionLoadedVoxelSize = undefined;
573575

574576
let shaderResult: ParameterizedShaderGetterResult<
575577
ShaderParameters,
@@ -692,6 +694,18 @@ void main() {
692694
effectiveVoxelSize[1],
693695
effectiveVoxelSize[2],
694696
);
697+
if (presentCount > 0) {
698+
const medianStoredVoxelSize = this.highestResolutionLoadedVoxelSize
699+
? medianOf3(
700+
this.highestResolutionLoadedVoxelSize[0],
701+
this.highestResolutionLoadedVoxelSize[1],
702+
this.highestResolutionLoadedVoxelSize[2],
703+
)
704+
: Infinity;
705+
if (medianVoxelSize <= medianStoredVoxelSize) {
706+
this.highestResolutionLoadedVoxelSize = effectiveVoxelSize;
707+
}
708+
}
695709
renderScaleHistogram.add(
696710
medianVoxelSize,
697711
medianVoxelSize / projectionParameters.pixelSize,

0 commit comments

Comments
 (0)