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 @@ X0mm Y0mm Z-144.92mm + +

Detector Rotation

+
+ + + +
+ diff --git a/webct/components/Capture.py b/webct/components/Capture.py index 127d899..38aab7c 100644 --- a/webct/components/Capture.py +++ b/webct/components/Capture.py @@ -9,6 +9,7 @@ class CaptureParameters: projections: int # Number of projections around an object. capture_angle: int # Total rotation around the sample in degrees. detector_position: Tuple[float, float, float] + detector_rotation: Tuple[float, float, float] beam_position: Tuple[float, float, float] sample_rotation: Tuple[float, float, float] laminography_mode: bool @@ -35,6 +36,7 @@ def from_json(json: dict): or "beam_position" not in json or "detector_position" not in json or "sample_rotation" not in json + or "detector_rotation" not in json or "laminography_mode" not in json ): raise ValueError("Missing keys.") @@ -44,6 +46,7 @@ def from_json(json: dict): tuple(json["beam_position"]) tuple(json["detector_position"]) tuple(json["sample_rotation"]) + tuple(json["detector_rotation"]) bool(json["laminography_mode"]) # Number of projections @@ -76,6 +79,11 @@ def from_json(json: dict): raise ValueError(f"Sample rotation must contain three axis. ({len(sample_rot)} was given).") sample_rot = [float(x) for x in sample_rot] + detector_rot = tuple(json["detector_rotation"]) + if len(detector_rot) != 3: + raise ValueError(f"Detector rotation must contain three axis. ({len(detector_rot)} was given).") + detector_rot = [float(x) for x in detector_rot] + # Laminography mode (rotate around sample's axis) laminography = bool(json["laminography_mode"]) @@ -86,4 +94,5 @@ def from_json(json: dict): beam_position=beam_pos, sample_rotation=sample_rot, laminography_mode=laminography, + detector_rotation=detector_rot, ) diff --git a/webct/components/sim/SimSession.py b/webct/components/sim/SimSession.py index cd93d2e..c890ab9 100644 --- a/webct/components/sim/SimSession.py +++ b/webct/components/sim/SimSession.py @@ -88,7 +88,7 @@ def init_default_parameters(self) -> None: Sample("Dragon Model", "welsh-dragon-small.stl", "mm", "element/aluminium"), ), ) - self.capture = CaptureParameters(360, 360, (0, 100, 0), (0, -400, 0), (0, 0, 90), False) + self.capture = CaptureParameters(360, 360, (0, 100, 0), (0, 0, 0),(0, -400, 0), (0, 0, 90), False) self.recon = FDKParam(filter="ram-lak") @property diff --git a/webct/components/sim/simulators/GVXRSimulator.py b/webct/components/sim/simulators/GVXRSimulator.py index b3199a6..50e0a50 100644 --- a/webct/components/sim/simulators/GVXRSimulator.py +++ b/webct/components/sim/simulators/GVXRSimulator.py @@ -230,6 +230,17 @@ def capture(self, value: CaptureParameters) -> None: # parallel or point mode. We re-set the beam value to fix this. self.beam = self._beam + # Tilt detector based on rotation + from scipy.spatial.transform import Rotation as R + + default_up_rotation = R.from_euler('xyz', [0, 0, -180], degrees=True) + rotation = R.from_euler('xyz', [value.detector_rotation], degrees=True) + up_vector = (default_up_rotation * rotation).as_rotvec().squeeze() / np.pi + + print(F"Up Vector: {up_vector}") + gvxr.setDetectorUpVector(*up_vector) + gvxr.renderLoop() + # Undo rotations in order to reset scene rotation matrix if self.laminography: gvxr.rotateScene(-1 * self.total_rotation[2], 0, 0, 1)