From 6f5ec8075af49c11abbc4b3f4979dd54651c4ab2 Mon Sep 17 00:00:00 2001
From: WYVERN <35181365+WYVERN@users.noreply.github.com>
Date: Thu, 20 Mar 2025 09:48:14 +0000
Subject: [PATCH] feat: :construction: WIP - Detector rotation controls, start
work on detector pane rotation
---
.../app/static/js/formats/ScanDocuLoader.ts | 3 +-
.../app/static/js/formats/XTEKCTLoader.ts | 3 +-
webct/blueprints/app/static/js/types.ts | 8 ++-
webct/blueprints/capture/static/js/api.ts | 4 ++
webct/blueprints/capture/static/js/capture.ts | 39 ++++++++++-
webct/blueprints/capture/static/js/types.ts | 4 ++
.../capture/static/js/validation.ts | 67 ++++++++++++++++++-
.../capture/static/scss/capture.scss | 7 ++
.../capture/templates/tab.capture.html.j2 | 8 +++
webct/components/Capture.py | 9 +++
webct/components/sim/SimSession.py | 2 +-
.../sim/simulators/GVXRSimulator.py | 11 +++
12 files changed, 156 insertions(+), 9 deletions(-)
diff --git a/webct/blueprints/app/static/js/formats/ScanDocuLoader.ts b/webct/blueprints/app/static/js/formats/ScanDocuLoader.ts
index 8deedbd..7833a72 100644
--- a/webct/blueprints/app/static/js/formats/ScanDocuLoader.ts
+++ b/webct/blueprints/app/static/js/formats/ScanDocuLoader.ts
@@ -83,7 +83,8 @@ export const ScanDocuConfig: FormatLoaderStatic = class ScanDocuConfig implement
numProjections: this.recon.ProjectionCountPer360deg ?? 360,
sampleRotation: [0, 0, 0],
totalAngle: 360,
- laminographyMode: false
+ laminographyMode: false,
+ detectorRotation: [0, 0, 0],
}
};
};
diff --git a/webct/blueprints/app/static/js/formats/XTEKCTLoader.ts b/webct/blueprints/app/static/js/formats/XTEKCTLoader.ts
index 6bf3cc8..a29d124 100644
--- a/webct/blueprints/app/static/js/formats/XTEKCTLoader.ts
+++ b/webct/blueprints/app/static/js/formats/XTEKCTLoader.ts
@@ -142,7 +142,8 @@ export const XTEKCTConfig: FormatLoaderStatic = class XTEKCTConfig implements Fo
numProjections: this.config.Projections ?? 360,
sampleRotation: [0, 0, 0],
totalAngle: this.config.AngularStep * this.config.Projections < 270 ? 180 : 360,
- laminographyMode: false
+ laminographyMode: false,
+ detectorRotation: [0, 0, 0],
}
};
};
diff --git a/webct/blueprints/app/static/js/types.ts b/webct/blueprints/app/static/js/types.ts
index 0041559..84dce44 100644
--- a/webct/blueprints/app/static/js/types.ts
+++ b/webct/blueprints/app/static/js/types.ts
@@ -137,6 +137,10 @@ export class WebCTConfig {
}
if (keys.capture && config.capture !== undefined) {
+ // v1.0.1 - Detector Rotation
+ if (!Object.prototype.hasOwnProperty.call(config.detector, "detectorRotation")) {
+ config.capture.detectorRotation = [0, 0, 0]
+ }
setCaptureParams(config.capture);
}
@@ -153,7 +157,7 @@ export class WebCTConfig {
static to_python(keys:ConfigKeys, options:ExportOptions):string {
// Create a python file to locally simulate and reconstruct WebCT
-
+
// In all cases, we still need gvxr's scan parameters
options.gvxrIncludeScan = true;
let config = this.to_json(WEBCT_FULL_CONFIG, options) as configFull
@@ -225,7 +229,7 @@ def reconstruct(out_folder:Path):
# Reconstruct
recon_data = method.run()
-
+
# Save to TIFF
TIFFWriter(recon_data, out_folder).write()
` : ""}
diff --git a/webct/blueprints/capture/static/js/api.ts b/webct/blueprints/capture/static/js/api.ts
index 755a70a..7e24f1a 100644
--- a/webct/blueprints/capture/static/js/api.ts
+++ b/webct/blueprints/capture/static/js/api.ts
@@ -37,6 +37,7 @@ export interface CaptureResponseRegistry {
projections: number;
capture_angle: number;
detector_position: [number, number, number];
+ detector_rotation: [number, number, number];
beam_position: [number, number, number];
sample_rotation: [number, number, number];
laminography_mode: boolean;
@@ -67,6 +68,7 @@ export interface CaptureRequestRegistry {
projections: number;
capture_angle: number,
detector_position: [number, number, number];
+ detector_rotation: [number, number, number];
beam_position: [number, number, number];
sample_rotation: [number, number, number];
laminography_mode: boolean;
@@ -134,6 +136,7 @@ export function processResponse(data: CaptureResponseRegistry[keyof CaptureRespo
totalAngle: data.capture_angle as 180 | 360,
beamPosition: data.beam_position,
detectorPosition: data.detector_position,
+ detectorRotation: data.detector_rotation,
sampleRotation: data.sample_rotation,
laminographyMode: data.laminography_mode,
};
@@ -150,6 +153,7 @@ export function prepareRequest(data: CaptureProperties): CaptureRequestRegistry[
projections:data.numProjections,
beam_position:data.beamPosition,
detector_position:data.detectorPosition,
+ detector_rotation: data.detectorRotation,
sample_rotation:data.sampleRotation,
laminography_mode: data.laminographyMode,
};
diff --git a/webct/blueprints/capture/static/js/capture.ts b/webct/blueprints/capture/static/js/capture.ts
index 7163a18..db97e5c 100644
--- a/webct/blueprints/capture/static/js/capture.ts
+++ b/webct/blueprints/capture/static/js/capture.ts
@@ -9,7 +9,7 @@ import { PanePixelSizeElement, PaneWidthElement, validateDetector } from "../../
import { CaptureResponseRegistry, processResponse, requestCaptureData, sendCaptureData, prepareRequest, requestCapturePreview } from "./api";
import { CaptureConfigError, CaptureRequestError, showError, showValidationError } from "./errors";
import { CapturePreview, CaptureProperties } from "./types";
-import { validateSourcePosition, validateProjections, validateRotation, validateDetectorPosition, validateSceneRotation, validateSourceYPosition, validateDetectorYPosition } from "./validation";
+import { validateSourcePosition, validateProjections, validateRotation, validateDetectorPosition, validateSceneRotation, validateSourceYPosition, validateDetectorYPosition, validateDetectorRotation } from "./validation";
import { UpdatePage } from "../../../app/static/js/app";
import { Valid } from "../../../base/static/js/validation";
@@ -24,6 +24,10 @@ let BeamPosZElement: SlInput;
let DetectorPosXElement: SlInput;
let DetectorPosYElement: SlInput;
let DetectorPosZElement: SlInput;
+let DetectorRotateXElement: SlInput;
+let DetectorRotateYElement: SlInput;
+let DetectorRotateZElement: SlInput;
+let DetectorRotateWarning: HTMLParagraphElement;
let SamplePosElement: SlRange;
let SampleSDDElement: HTMLParagraphElement;
@@ -74,6 +78,11 @@ export function setupCapture(): boolean {
const detector_posy_element = document.getElementById("inputDetectorPosY");
const detector_posz_element = document.getElementById("inputDetectorPosZ");
+ const detector_rotatex_element = document.getElementById("inputDetectorRotateX");
+ const detector_rotatey_element = document.getElementById("inputDetectorRotateY");
+ const detector_rotatez_element = document.getElementById("inputDetectorRotateZ");
+ const detector_rotate_warning = document.getElementById("textDetectorRotationWarning")
+
const sample_position_element = document.getElementById("rangeSamplePosition");
const sample_position_sdd = document.getElementById("textSDD")
const sample_position_sod = document.getElementById("textSOD")
@@ -109,8 +118,12 @@ export function setupCapture(): boolean {
detector_posx_element == null ||
detector_posy_element == null ||
detector_posz_element == null ||
+ detector_rotatex_element == null ||
+ detector_rotatey_element == null ||
+ detector_rotatez_element == null ||
+ detector_rotate_warning == null ||
laminography_enabled_element == null ||
- range_nyquist == null ||
+ range_nyquist == null ||
magnification_text_element == null ||
voxel_size_text_element == null) {
@@ -125,6 +138,11 @@ export function setupCapture(): boolean {
console.log(detector_posy_element);
console.log(detector_posz_element);
+ console.log(detector_rotatex_element);
+ console.log(detector_rotatey_element);
+ console.log(detector_rotatez_element);
+ console.log(detector_rotate_warning);
+
console.log(sample_position_element);
console.log(sample_position_sdd);
console.log(sample_position_sod);
@@ -195,6 +213,16 @@ export function setupCapture(): boolean {
})
});
+ DetectorRotateWarning = detector_rotate_warning as HTMLParagraphElement;
+ DetectorRotateXElement = detector_rotatex_element as SlInput;
+ DetectorRotateYElement = detector_rotatey_element as SlInput;
+ DetectorRotateZElement = detector_rotatez_element as SlInput;
+ [DetectorRotateXElement, DetectorRotateYElement, DetectorRotateZElement].forEach(element => {
+ element.addEventListener("sl-change", () => {
+ validateDetectorRotation(DetectorRotateXElement, DetectorRotateYElement, DetectorRotateZElement, DetectorRotateWarning)
+ })
+ });
+
CheckboxLaminographyElement = laminography_enabled_element as SlCheckbox;
ButtonRotateClock45Element = sample_rotate_clock_45_element as SlButton;
@@ -290,6 +318,8 @@ export function validateCapture(): void {
validateSourcePosition(DetectorPosXElement),
validateDetectorYPosition(DetectorPosYElement),
validateSourcePosition(DetectorPosZElement),
+ // panel rotation
+ validateDetectorRotation(DetectorRotateXElement, DetectorRotateYElement, DetectorRotateZElement, DetectorRotateWarning),
// beam position
validateSourcePosition(BeamPosXElement),
validateSourceYPosition(BeamPosYElement),
@@ -526,6 +556,7 @@ export function getCaptureParams():CaptureProperties {
totalAngle: parseInt(TotalRotationElement.value as string) as 180 | 360,
beamPosition: [parseFloat(BeamPosXElement.value), parseFloat(BeamPosYElement.value), parseFloat(BeamPosZElement.value)],
detectorPosition: [parseFloat(DetectorPosXElement.value), parseFloat(DetectorPosYElement.value), parseFloat(DetectorPosZElement.value)],
+ detectorRotation: [parseFloat(DetectorRotateXElement.value), parseFloat(DetectorRotateYElement.value), parseFloat(DetectorRotateZElement.value)],
sampleRotation: [parseFloat(SampleRotateXElement.value), parseFloat(SampleRotateYElement.value), parseFloat(SampleRotateZElement.value)],
laminographyMode: CheckboxLaminographyElement.checked,
};
@@ -545,6 +576,10 @@ export function setCaptureParams(properties:CaptureProperties) {
DetectorPosYElement.value = properties.detectorPosition[1] + "";
DetectorPosZElement.value = properties.detectorPosition[2] + "";
+ DetectorRotateXElement.value = properties.detectorRotation[0] + "";
+ DetectorRotateYElement.value = properties.detectorRotation[1] + "";
+ DetectorRotateZElement.value = properties.detectorRotation[2] + "";
+
SampleRotateXElement.value = properties.sampleRotation[0] + "";
SampleRotateYElement.value = properties.sampleRotation[1] + "";
SampleRotateZElement.value = properties.sampleRotation[2] + "";
diff --git a/webct/blueprints/capture/static/js/types.ts b/webct/blueprints/capture/static/js/types.ts
index 9724266..382e3cf 100644
--- a/webct/blueprints/capture/static/js/types.ts
+++ b/webct/blueprints/capture/static/js/types.ts
@@ -20,6 +20,10 @@ export interface CaptureProperties {
* Detector Position relative to sample origin
*/
detectorPosition: [number, number, number];
+ /**
+ * Rotation of detector in degrees.
+ */
+ detectorRotation: [number, number, number];
/**
* Beam Position relative to sample origin
*/
diff --git a/webct/blueprints/capture/static/js/validation.ts b/webct/blueprints/capture/static/js/validation.ts
index e8382d5..f11bee6 100644
--- a/webct/blueprints/capture/static/js/validation.ts
+++ b/webct/blueprints/capture/static/js/validation.ts
@@ -4,7 +4,7 @@
*/
import { SlInput } from "@shoelace-style/shoelace";
-import { Valid, validateInput, Validator } from "../../../base/static/js/validation";
+import { isValid, markValid, Valid, validateInput, Validator } from "../../../base/static/js/validation";
/**
* Validator for projection count.
@@ -36,6 +36,69 @@ export function validateRotation(RotationElement: SlInput): Valid {
return validateInput(RotationElement, "Sample Rotation", RotationValidator);
}
+/**
+ * Validate Sample Rotation text input.
+ * @param RotationElement - Rotation input to validate.
+ * @returns True if input is valid, False otherwise. Side Effect: passed element will have help-text displaying the validation failure reason.
+ */
+export function validateDetectorRotation(RotationElementX: SlInput, RotationElementY: SlInput, RotationElementZ: SlInput, helpTextElement:HTMLParagraphElement): Valid {
+ let validX = isValid(RotationElementX as unknown as HTMLInputElement, RotationValidator)
+ let validY = isValid(RotationElementY as unknown as HTMLInputElement, RotationValidator)
+ let validZ = isValid(RotationElementZ as unknown as HTMLInputElement, RotationValidator)
+
+ if (!validX.valid) {
+ validX = {valid: false, InvalidReason: "Detector Rotation X " + validX.InvalidReason + ".
" + RotationValidator.message}
+ } else if (!validY.valid) {
+ validY = {valid: false, InvalidReason: "Detector Rotation Y " + validY.InvalidReason + ".
" + RotationValidator.message}
+ } else if (!validZ.valid) {
+ validZ = {valid: false, InvalidReason: "Detector Rotation Z " + validZ.InvalidReason + ".
" + RotationValidator.message}
+ }
+
+ // * Special-case: Detector rotation is guarenteed to cause reconstruction
+ // * artefacts. Therefore, enable a warning state if the value is not 0.
+ let allZero = true;
+ [[RotationElementX, validX], [RotationElementY, validY], [RotationElementZ, validZ]].forEach( (set) => {
+ let element = set[0] as SlInput
+ let valid = set[1] as Valid
+
+ element.classList.remove("warning");
+
+ if (valid.valid) {
+ if (parseFloat(element.value) !== 0) {
+ // Element is valid, but isn't the recommended value of zero (0)
+ // change help-text to indicate as such, and mark input element as warning
+ element.classList.add("warning")
+ helpTextElement.classList.add("warning")
+ helpTextElement.textContent = "Detector rotation will cause reconstruction artefacts."
+ allZero = false
+ } else {
+ // element is valid, and is also zero
+ element.helpText = ""
+ markValid(element, valid.valid)
+ }
+ } else {
+ // element is invalid
+ markValid(element, valid.valid)
+ element.helpText = RotationValidator.message == undefined ? "Must be a number." : RotationValidator.message
+ }
+ })
+
+ if (allZero) {
+ // If all boxes are zero, remove the rotation warning
+ helpTextElement.textContent = ""
+ }
+
+ if (!validX.valid) {
+ return validX
+ } else if (!validY.valid) {
+ return validY
+ } else if (!validZ.valid) {
+ return validZ
+ } else {
+ return {valid:true}
+ }
+}
+
/**
* Validate Scene Rotation text input.
* @param RotationElement - Rotation input to validate.
@@ -99,4 +162,4 @@ export function validateSourceYPosition(SourcePositionElement: SlInput): Valid {
*/
export function validateDetectorYPosition(DetectorPositionElement: SlInput): Valid {
return validateInput(DetectorPositionElement, "Detector's Y Position", YDetectorPositionValidator);
-}
\ No newline at end of file
+}
diff --git a/webct/blueprints/capture/static/scss/capture.scss b/webct/blueprints/capture/static/scss/capture.scss
index 33eb6b2..56fe3c4 100644
--- a/webct/blueprints/capture/static/scss/capture.scss
+++ b/webct/blueprints/capture/static/scss/capture.scss
@@ -124,3 +124,10 @@ sl-range::part(tooltip) {
color: var(--sl-color-neutral-500);
font-size: var(--sl-font-size-x-small);
}
+
+#textDetectorRotationWarning {
+ line-height: 1.25;
+ padding-top: 2px;
+ font-size: var(--sl-input-help-text-font-size-medium);
+ color: var(--sl-color-warning-600);
+}
diff --git a/webct/blueprints/capture/templates/tab.capture.html.j2 b/webct/blueprints/capture/templates/tab.capture.html.j2
index cf2693d..62aadb2 100644
--- a/webct/blueprints/capture/templates/tab.capture.html.j2
+++ b/webct/blueprints/capture/templates/tab.capture.html.j2
@@ -73,6 +73,14 @@
Detector Rotation
+