Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion webct/blueprints/app/static/js/formats/ScanDocuLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
}
};
};
Expand Down
3 changes: 2 additions & 1 deletion webct/blueprints/app/static/js/formats/XTEKCTLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
}
};
};
Expand Down
8 changes: 6 additions & 2 deletions webct/blueprints/app/static/js/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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
Expand Down Expand Up @@ -225,7 +229,7 @@ def reconstruct(out_folder:Path):

# Reconstruct
recon_data = method.run()

# Save to TIFF
TIFFWriter(recon_data, out_folder).write()
` : ""}
Expand Down
4 changes: 4 additions & 0 deletions webct/blueprints/capture/static/js/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
};
Expand All @@ -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,
};
Expand Down
39 changes: 37 additions & 2 deletions webct/blueprints/capture/static/js/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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) {

Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
};
Expand All @@ -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] + "";
Expand Down
4 changes: 4 additions & 0 deletions webct/blueprints/capture/static/js/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
67 changes: 65 additions & 2 deletions webct/blueprints/capture/static/js/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 + ". <br/>" + RotationValidator.message}
} else if (!validY.valid) {
validY = {valid: false, InvalidReason: "Detector Rotation Y " + validY.InvalidReason + ". <br/>" + RotationValidator.message}
} else if (!validZ.valid) {
validZ = {valid: false, InvalidReason: "Detector Rotation Z " + validZ.InvalidReason + ". <br/>" + 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.
Expand Down Expand Up @@ -99,4 +162,4 @@ export function validateSourceYPosition(SourcePositionElement: SlInput): Valid {
*/
export function validateDetectorYPosition(DetectorPositionElement: SlInput): Valid {
return validateInput(DetectorPositionElement, "Detector's Y Position", YDetectorPositionValidator);
}
}
7 changes: 7 additions & 0 deletions webct/blueprints/capture/static/scss/capture.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
8 changes: 8 additions & 0 deletions webct/blueprints/capture/templates/tab.capture.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@
<sl-input advanced type="number" max="1000" step="0.1" min="-1000" id="inputDetectorPosX" label=""><span slot="prefix">X</span>0<span slot="suffix">mm</span></sl-input>
<sl-input type="number" max="1000" step="0.1" min="-1000" id="inputDetectorPosY"><span slot="prefix">Y</span>0<span slot="suffix">mm</span></sl-input>
<sl-input advanced type="number" max="1000" step="0.1" min="-1000" id="inputDetectorPosZ"><span slot="prefix">Z</span>-144.92<span slot="suffix">mm</span></sl-input>

<p advanced>Detector Rotation</p>
<div advanced style="grid-template-columns: repeat(3, minmax(0, 1fr)); display: grid;">
<sl-input label="Pitch" type="number" min="0" max="360" step="0.1" id="inputDetectorRotateX"></sl-input>
<sl-input label="Yaw" type="number" min="0" max="360" step="0.1" id="inputDetectorRotateY"></sl-input>
<sl-input label="Roll" type="number" min="0" max="360" step="0.1" id="inputDetectorRotateZ"></sl-input>
</div>
<small id="textDetectorRotationWarning"></small>
</div>
<button></button>
</div>
9 changes: 9 additions & 0 deletions webct/components/Capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.")
Expand All @@ -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
Expand Down Expand Up @@ -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"])

Expand All @@ -86,4 +94,5 @@ def from_json(json: dict):
beam_position=beam_pos,
sample_rotation=sample_rot,
laminography_mode=laminography,
detector_rotation=detector_rot,
)
2 changes: 1 addition & 1 deletion webct/components/sim/SimSession.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions webct/components/sim/simulators/GVXRSimulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down