All files / core/src/RenderingEngine/helpers getCameraVectors.ts

93.05% Statements 67/72
77.14% Branches 27/35
100% Functions 5/5
93.05% Lines 67/72

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358              428x 428x                                                                                                                                       96x     40x 40x     32x 32x     24x 24x               96x 96x 96x     96x             96x                                     96x     96x 288x 288x 288x   288x   864x 288x     576x 576x   576x 376x 376x 376x         288x   288x 288x 104x     288x         96x 96x 96x   96x                                                                                                                             96x       96x     96x 96x 64x   96x     96x 96x         96x         96x 96x 96x 96x 96x 96x     96x 96x       96x                                                                                                             96x     96x         96x         96x             96x 96x 96x     96x 40x 56x 32x   24x      
import * as metaData from '../../metaData';
import * as CONSTANTS from '../../constants';
import * as Enums from '../../enums';
import type * as Types from '../../types';
 
import { vec3 } from 'gl-matrix';
 
const { MPR_CAMERA_VALUES } = CONSTANTS;
const { OrientationAxis } = Enums;
 
export interface CameraPositionConfig {
  orientation?: Enums.OrientationAxis;
  useViewportNormal?: boolean;
}
 
/**
 * Calculate camera position values based on DICOM image orientation vectors.
 *
 * This function transforms rotated DICOM coordinate system vectors into the standard
 * orthogonal MPR camera coordinate system. It uses a best-fit algorithm to match
 * the input vectors with the reference MPR camera values, automatically handling
 * vector inversion when necessary.
 *
 * Algorithm:
 * 1. Normalize all input vectors to ensure consistent calculations
 * 2. Get reference camera values based on the specified orientation
 * 3. For each reference vector (viewRight, viewUp, viewPlaneNormal):
 *    - Calculate dot products with all available input vectors
 *    - Find the input vector with highest absolute dot product (best alignment)
 *    - Remove the matched input vector from further competition
 *    - Invert the vector if the dot product is negative
 * 4. Return the mapped camera coordinate system
 *
 * Note: The algorithm handles 45-degree rotations by ensuring each input vector
 * is only assigned once, preventing multiple reference vectors from competing
 * for the same input vector when dot products are similar.
 *
 * @param rowCosineVec - Row direction cosine vector from DICOM ImageOrientationPatient.
 *                       This represents the direction of the first row of pixels in the image
 *                       relative to the patient coordinate system.
 * @param colCosineVec - Column direction cosine vector from DICOM ImageOrientationPatient.
 *                       This represents the direction of the first column of pixels in the image
 *                       relative to the patient coordinate system.
 * @param scanAxisNormal - Normal vector perpendicular to the image plane.
 *                         Typically computed as the cross product of rowCosineVec and colCosineVec.
 * @param orientation - Target orientation axis (axial, sagittal, or coronal) that determines
 *                      which reference MPR camera values to use for mapping.
 *
 * @returns Object containing the mapped camera coordinate system:
 *          - viewPlaneNormal: Vector perpendicular to the viewing plane
 *          - viewUp: Vector pointing "up" in the camera coordinate system
 *          - viewRight: Vector pointing "right" in the camera coordinate system
 *
 * @example
 * ```typescript
 * const rowCosine = vec3.fromValues(1, 0, 0);
 * const colCosine = vec3.fromValues(0, 1, 0);
 * const scanNormal = vec3.fromValues(0, 0, 1);
 *
 * const cameraValues = calculateCameraPosition(
 *   rowCosine,
 *   colCosine,
 *   scanNormal,
 *   OrientationAxis.AXIAL
 * );
 * ```
 */
export function calculateCameraPosition(
  rowCosineVec: vec3,
  colCosineVec: vec3,
  scanAxisNormal: vec3,
  orientation: Enums.OrientationAxis
) {
  // Get reference MPR camera values based on orientation
  let referenceCameraValues;
 
  switch (orientation) {
    case OrientationAxis.AXIAL:
    case OrientationAxis.AXIAL_REFORMAT:
      referenceCameraValues = MPR_CAMERA_VALUES.axial;
      break;
    case OrientationAxis.SAGITTAL:
    case OrientationAxis.SAGITTAL_REFORMAT:
      referenceCameraValues = MPR_CAMERA_VALUES.sagittal;
      break;
    case OrientationAxis.CORONAL:
    case OrientationAxis.CORONAL_REFORMAT:
      referenceCameraValues = MPR_CAMERA_VALUES.coronal;
      break;
    default:
      // Default to axial if orientation is not recognized
      referenceCameraValues = MPR_CAMERA_VALUES.axial;
      break;
  }
 
  // Normalize input vectors
  const normalizedRowCosine = vec3.normalize(vec3.create(), rowCosineVec);
  const normalizedColCosine = vec3.normalize(vec3.create(), colCosineVec);
  const normalizedScanAxis = vec3.normalize(vec3.create(), scanAxisNormal);
 
  // Create array of input vectors for comparison
  const inputVectors = [
    normalizedRowCosine,
    normalizedColCosine,
    normalizedScanAxis,
  ];
 
  // Create array of reference vectors
  const referenceVectors = [
    vec3.fromValues(
      referenceCameraValues.viewRight[0],
      referenceCameraValues.viewRight[1],
      referenceCameraValues.viewRight[2]
    ),
    vec3.fromValues(
      referenceCameraValues.viewUp[0],
      referenceCameraValues.viewUp[1],
      referenceCameraValues.viewUp[2]
    ),
    vec3.fromValues(
      referenceCameraValues.viewPlaneNormal[0],
      referenceCameraValues.viewPlaneNormal[1],
      referenceCameraValues.viewPlaneNormal[2]
    ),
  ];
 
  // Track which input vectors have been used to avoid double assignment
  const usedInputIndices = new Set<number>();
 
  // Find best match for each reference vector, excluding already used input vectors
  const findBestMatch = (refVector: vec3) => {
    let bestMatch = 0;
    let bestDot = -2; // Start with value less than minimum possible dot product
    let shouldInvert = false;
 
    inputVectors.forEach((inputVec, index) => {
      // Skip if this input vector has already been assigned
      if (usedInputIndices.has(index)) {
        return;
      }
 
      const dot = vec3.dot(refVector, inputVec);
      const absDot = Math.abs(dot);
 
      if (absDot > bestDot) {
        bestDot = absDot;
        bestMatch = index;
        shouldInvert = dot < 0;
      }
    });
 
    // Mark this input vector as used
    usedInputIndices.add(bestMatch);
 
    const matchedVector = vec3.clone(inputVectors[bestMatch]);
    if (shouldInvert) {
      vec3.negate(matchedVector, matchedVector);
    }
 
    return matchedVector;
  };
 
  // Map reference vectors to input vectors in order of priority
  // This ensures each input vector is only used once, even for 45-degree rotations
  const viewRight = findBestMatch(referenceVectors[0]);
  const viewUp = findBestMatch(referenceVectors[1]);
  const viewPlaneNormal = findBestMatch(referenceVectors[2]);
 
  return {
    viewPlaneNormal: [
      viewPlaneNormal[0],
      viewPlaneNormal[1],
      viewPlaneNormal[2],
    ] as [number, number, number],
    viewUp: [viewUp[0], viewUp[1], viewUp[2]] as [number, number, number],
    viewRight: [viewRight[0], viewRight[1], viewRight[2]] as [
      number,
      number,
      number,
    ],
  };
}
 
/**
 * Calculate camera position values from viewport metadata.
 *
 * This is a convenience function that extracts DICOM image orientation data
 * from a viewport and automatically calculates the appropriate camera position
 * values. It handles the complete workflow from DICOM metadata extraction
 * to camera coordinate system calculation.
 *
 * Workflow:
 * 1. Extract current image ID from viewport
 * 2. Get ImageOrientationPatient from DICOM metadata
 * 3. Parse row and column cosine vectors from ImageOrientationPatient
 * 4. Calculate scan axis normal via cross product
 * 5. Auto-detect orientation if not provided
 * 6. Calculate and return camera position values
 *
 * @param viewport - Cornerstone3D volume viewport instance containing the image data.
 *                   Must have a valid current image ID with associated DICOM metadata.
 * @param config - Configuration object with orientation and normal source options.
 *                 - orientation: Optional target orientation axis. If not provided, the function
 *                   will automatically determine the best orientation based on the scan axis normal vector.
 *                 - useViewportNormal: If true, uses viewport.getCamera().viewPlaneNormal instead of
 *                   calculating from DICOM image orientation data.
 *
 * @returns Object containing the mapped camera coordinate system:
 *          - viewPlaneNormal: Vector perpendicular to the viewing plane
 *          - viewUp: Vector pointing "up" in the camera coordinate system
 *          - viewRight: Vector pointing "right" in the camera coordinate system
 *
 * @throws Will throw an error if the viewport doesn't have a current image ID
 *         or if the DICOM metadata is missing ImageOrientationPatient information.
 *
 * @example
 * ```typescript
 * // Auto-detect orientation
 * const cameraValues = getCameraVectors(viewport);
 *
 * // Force specific orientation
 * const axialCameraValues = getCameraVectors(viewport, { orientation: OrientationAxis.AXIAL });
 *
 * // Use viewport camera normal instead of image normal
 * const viewportCameraValues = getCameraVectors(viewport, { useViewportNormal: true });
 * ```
 */
export function getCameraVectors(
  viewport: Types.IBaseVolumeViewport,
  config?: CameraPositionConfig
) {
  Iif (!viewport.getActors()?.length) {
    return;
  }
 
  Iif (viewport.type !== Enums.ViewportType.ORTHOGRAPHIC) {
    console.warn('Viewport should be a volume viewport');
  }
  let imageId = viewport.getCurrentImageId();
  if (!imageId) {
    imageId = viewport.getImageIds()?.[0];
  }
  Iif (!imageId) {
    return;
  }
  const { imageOrientationPatient } = metaData.get('imagePlaneModule', imageId);
  const rowCosineVec = vec3.fromValues(
    imageOrientationPatient[0],
    imageOrientationPatient[1],
    imageOrientationPatient[2]
  );
  const colCosineVec = vec3.fromValues(
    imageOrientationPatient[3],
    imageOrientationPatient[4],
    imageOrientationPatient[5]
  );
  const scanAxisNormal = vec3.cross(vec3.create(), rowCosineVec, colCosineVec);
  let { orientation } = config || {};
  const { useViewportNormal } = config || {};
  let normalPlaneForOrientation = scanAxisNormal;
  Eif (useViewportNormal) {
    normalPlaneForOrientation = viewport.getCamera().viewPlaneNormal;
  }
 
  Eif (!orientation) {
    orientation = getOrientationFromScanAxisNormal(normalPlaneForOrientation);
  }
 
  // Use the new calculateCameraPosition function
  return calculateCameraPosition(
    rowCosineVec,
    colCosineVec,
    scanAxisNormal,
    orientation
  );
}
 
/**
 * Determine the orientation axis based on the scan axis normal vector.
 *
 * This function automatically identifies whether a scan axis normal vector
 * corresponds to axial, sagittal, or coronal orientation by comparing it
 * with the reference MPR camera view plane normals. It uses dot product
 * calculations to find the best alignment.
 *
 * The function is particularly useful when working with DICOM images that
 * don't have explicit orientation information or when you need to validate
 * the orientation of rotated image data.
 *
 * Algorithm:
 * 1. Normalize the input scan axis normal vector
 * 2. Get reference view plane normals for all three standard orientations
 * 3. Calculate absolute dot products between input and reference vectors
 * 4. Return the orientation with the highest dot product (best alignment)
 *
 * Reference orientations and their view plane normals:
 * - Axial: [0, 0, -1] (looking down from head to feet)
 * - Sagittal: [1, 0, 0] (looking from right to left side)
 * - Coronal: [0, -1, 0] (looking from front to back)
 *
 * @param scanAxisNormal - Normal vector perpendicular to the image plane.
 *                         This vector should represent the direction perpendicular
 *                         to the imaging plane in patient coordinate system.
 *
 * @returns The orientation axis (AXIAL, SAGITTAL, or CORONAL) that best
 *          matches the provided scan axis normal vector.
 *
 * @example
 * ```typescript
 * // For a typical axial scan (normal pointing in Z direction)
 * const axialNormal = vec3.fromValues(0, 0, 1);
 * const orientation = getOrientationFromScanAxisNormal(axialNormal);
 * // Returns: OrientationAxis.AXIAL
 *
 * // For a sagittal scan (normal pointing in X direction)
 * const sagittalNormal = vec3.fromValues(-1, 0, 0);
 * const orientation = getOrientationFromScanAxisNormal(sagittalNormal);
 * // Returns: OrientationAxis.SAGITTAL
 * ```
 */
export function getOrientationFromScanAxisNormal(
  scanAxisNormal: vec3
): Enums.OrientationAxis {
  // Normalize the input vector
  const normalizedScanAxis = vec3.normalize(vec3.create(), scanAxisNormal);
 
  // Get reference view plane normals for each orientation
  const axialNormal = vec3.fromValues(
    MPR_CAMERA_VALUES.axial.viewPlaneNormal[0],
    MPR_CAMERA_VALUES.axial.viewPlaneNormal[1],
    MPR_CAMERA_VALUES.axial.viewPlaneNormal[2]
  );
  const sagittalNormal = vec3.fromValues(
    MPR_CAMERA_VALUES.sagittal.viewPlaneNormal[0],
    MPR_CAMERA_VALUES.sagittal.viewPlaneNormal[1],
    MPR_CAMERA_VALUES.sagittal.viewPlaneNormal[2]
  );
  const coronalNormal = vec3.fromValues(
    MPR_CAMERA_VALUES.coronal.viewPlaneNormal[0],
    MPR_CAMERA_VALUES.coronal.viewPlaneNormal[1],
    MPR_CAMERA_VALUES.coronal.viewPlaneNormal[2]
  );
 
  // Calculate dot products to find best match
  const axialDot = Math.abs(vec3.dot(normalizedScanAxis, axialNormal));
  const sagittalDot = Math.abs(vec3.dot(normalizedScanAxis, sagittalNormal));
  const coronalDot = Math.abs(vec3.dot(normalizedScanAxis, coronalNormal));
 
  // Find the orientation with the highest dot product (best alignment)
  if (axialDot >= sagittalDot && axialDot >= coronalDot) {
    return OrientationAxis.AXIAL;
  } else if (sagittalDot >= coronalDot) {
    return OrientationAxis.SAGITTAL;
  } else {
    return OrientationAxis.CORONAL;
  }
}