@vitessce/spatial-utils 3.6.11 → 3.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import { VIEWER_PALETTE } from '@vitessce/utils';
5
5
  import { GLOBAL_LABELS, DEFAULT_RASTER_LAYER_PROPS } from '@vitessce/constants-internal';
6
6
  import { log } from '@vitessce/globals';
7
7
  import { getMultiSelectionStats } from './layer-controller.js';
8
+ import { DAG } from './dag.js';
8
9
  /**
9
10
  * Get a representative PixelSource from a loader object returned from
10
11
  * the Vitessce imaging loaders
@@ -440,8 +441,8 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
440
441
  }
441
442
  else if (spatialOutputAxes.length === 2) { // 2D case
442
443
  const nextMat = (new Matrix4()).fromArray([
443
- ...filteredAffine[0],
444
- ...filteredAffine[1],
444
+ filteredAffine[0][0], filteredAffine[0][1], 0, filteredAffine[0][2],
445
+ filteredAffine[1][0], filteredAffine[1][1], 0, filteredAffine[1][2],
445
446
  0, 0, 1, 0,
446
447
  0, 0, 0, 1,
447
448
  ]);
@@ -452,6 +453,10 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
452
453
  const swapMat = (new Matrix4()).fromArray(swapMatNested.flat());
453
454
  mat = mat.multiplyLeft(swapMat);
454
455
  }
456
+ // TODO: is the transpose needed? why?
457
+ // TODO: is transpose only needed when axis-swapping?
458
+ // TODO: is it also needed in the 3D case? Why was it not needed before?
459
+ mat = mat.transpose();
455
460
  }
456
461
  else {
457
462
  throw new Error('Affine transformation must have 2 or 3 rows.');
@@ -488,6 +493,9 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
488
493
  }
489
494
  });
490
495
  }
496
+ if (mat.some(value => Number.isNaN(value))) {
497
+ throw new Error('Matrix contains NaN values');
498
+ }
491
499
  return mat;
492
500
  }
493
501
  export function hexToRgb(hex) {
@@ -511,6 +519,12 @@ export function normalizeAxes(axes) {
511
519
  return axisInfo;
512
520
  });
513
521
  }
522
+ export function getIntrinsicCoordinateSystem(normAxes) {
523
+ // Get the intrinsic coordinate system from the axes.
524
+ // TODO: is this correct?
525
+ // See https://github.com/scverse/spatialdata/pull/963
526
+ return normAxes.map(axis => axis.name).join('');
527
+ }
514
528
  /**
515
529
  * Normalize coordinate transformations to the OME-NGFF v0.4 format,
516
530
  * despite potentially being in the new format proposed in
@@ -531,35 +545,18 @@ export function normalizeCoordinateTransformations(coordinateTransformations, da
531
545
  // This is a new-style coordinate transformation.
532
546
  // (As proposed in https://github.com/ome/ngff/pull/138)
533
547
  const { type } = transform;
534
- if (type === 'translation') {
535
- return {
536
- type,
537
- translation: transform.translation,
538
- };
539
- }
540
- if (type === 'scale') {
541
- return {
542
- type,
543
- scale: transform.scale,
544
- };
545
- }
546
- if (type === 'affine') {
547
- return {
548
- type,
549
- affine: transform.affine,
550
- // Affine transformations can potentially swap axes,
551
- // so we need to include the input and output axes.
552
- input: transform.input,
553
- output: transform.output,
554
- };
555
- }
556
- if (type === 'identity') {
557
- return { type };
558
- }
559
548
  if (type === 'sequence') {
560
- return normalizeCoordinateTransformations(transform.transformations, datasets);
549
+ // Recursion to flatten the sequence of transformations.
550
+ return normalizeCoordinateTransformations(transform.transformations, null);
561
551
  }
562
- log.warn(`Coordinate transformation type "${type}" is not supported.`);
552
+ if (type === 'affine' || type === 'translation' || type === 'scale' || type === 'identity') {
553
+ // TODO: normalize the transform.input/transform.output if they are missing?
554
+ // Old transformations did not specify them for translation/scale/identity.
555
+ // But we are currently only using them for affine.
556
+ return transform;
557
+ }
558
+ // If the type is not recognized, log an error.
559
+ log.error(`Coordinate transformation type "${type}" is not supported.`);
563
560
  }
564
561
  // Assume it was already an old-style (NGFF v0.4) coordinate transformation.
565
562
  return transform;
@@ -579,3 +576,48 @@ export function normalizeCoordinateTransformations(coordinateTransformations, da
579
576
  }
580
577
  return result;
581
578
  }
579
+ /**
580
+ * Convert coordinate transformations to a 4x4 matrix for SpatialData
581
+ * spatial elements. This function is intended to be used with SpatialData
582
+ * objects which have named coordinate systems. This function should
583
+ * be compatible with any SpatialElement (shapes, points, labels, images).
584
+ * @param {{
585
+ * axe?: (string[]|{ name: string, type?: string, unit?: string }[]),
586
+ * coordinateTransformations?: object[],
587
+ * datasets?: object[]
588
+ * }} ngffMetadata For images/labels, this is multiscales[0] from .zattrs.
589
+ * For shapes/points, this is { axes, coordinateTransformations } from .zattrs.
590
+ * @param {string} targetCoordinateSystem A target coordinate system name.
591
+ * @returns {Matrix4} A 4x4 transformation matrix.
592
+ * This can later be multiplied with other matrices if needed.
593
+ */
594
+ export function coordinateTransformationsToMatrixForSpatialData(ngffMetadata, targetCoordinateSystem) {
595
+ const { datasets, coordinateTransformations, axes, } = ngffMetadata; // In the image/labels case, this is multiscales[0].
596
+ // In case axes is an array of strings like ['x', 'y', 'z'],
597
+ // we convert them to a consistent object format.
598
+ const normAxes = normalizeAxes(axes);
599
+ // We know the target coordinate system, but not yet the starting (intrinsic) one.
600
+ const intrinsicCoordinateSystem = getIntrinsicCoordinateSystem(normAxes);
601
+ // Create a DAG of coordinate transformations.
602
+ const edges = coordinateTransformations.map(ct => ({
603
+ from: ct.input.name,
604
+ to: ct.output.name,
605
+ attributes: ct,
606
+ }));
607
+ const dag = new DAG(edges);
608
+ const ctPath = dag.findPath(intrinsicCoordinateSystem, targetCoordinateSystem);
609
+ if (!ctPath) {
610
+ // For example, this can be the case if the targetCoordinateSystem
611
+ // is "global" but the set of coordinate transformations does not
612
+ // include one with output.name "global".
613
+ if (!coordinateTransformations.find(ct => ct.output.name === targetCoordinateSystem)) {
614
+ throw new Error(`No coordinate transformation found with output.name "${targetCoordinateSystem}".`);
615
+ }
616
+ throw new Error(`No path found from "${intrinsicCoordinateSystem}" to "${targetCoordinateSystem}".`);
617
+ }
618
+ const filteredTransformations = ctPath.map(ctEdge => ctEdge.attributes);
619
+ // Normalize the coordinate transformations to OME-NGFF v0.4 format.
620
+ const normalizedTransformations = normalizeCoordinateTransformations(filteredTransformations, datasets);
621
+ // Convert the coordinate transformations to a 4x4 matrix.
622
+ return coordinateTransformationsToMatrix(normalizedTransformations, normAxes);
623
+ }