src_image_maskFactory.js

// namespaces
var dwv = dwv || {};
dwv.image = dwv.image || {};

/**
 * Check two position patients for equality.
 *
 * @param {*} pos1 The first position patient.
 * @param {*} pos2 The second position patient.
 * @returns {boolean} True is equal.
 */
dwv.dicom.equalPosPat = function (pos1, pos2) {
  return JSON.stringify(pos1) === JSON.stringify(pos2);
};

/**
 * Get a position patient compare function accroding to an
 * input orientation.
 *
 * @param {dwv.math.Matrix33} orientation The orientation matrix.
 * @returns {Function} The position compare function.
 */
dwv.dicom.getComparePosPat = function (orientation) {
  var invOrientation = orientation.getInverse();
  return function (pos1, pos2) {
    var p1 = invOrientation.multiplyArray3D(pos1);
    var p2 = invOrientation.multiplyArray3D(pos2);
    return p1[2] - p2[2];
  };
};

/**
 * Check that a DICOM tag definition is present in a parsed element.
 *
 * @param {object} rootElement The root dicom element.
 * @param {object} tagDefinition The tag definition as {name, tag, type, enum}.
 */
dwv.dicom.checkTag = function (rootElement, tagDefinition) {
  var tagValue = rootElement.getFromKey(tagDefinition.tag);
  // check null and undefined
  if (tagDefinition.type === 1 || tagDefinition.type === 2) {
    if (tagValue === null || typeof tagValue === 'undefined') {
      throw new Error('Missing or empty ' + tagDefinition.name);
    }
  } else {
    if (tagValue === null || typeof tagValue === 'undefined') {
      // non mandatory value, exit
      return;
    }
  }
  var includes = false;
  if (Array.isArray(tagValue)) {
    // trim
    tagValue = tagValue.map(function (item) {
      return dwv.dicom.cleanString(item);
    });
    for (var i = 0; i < tagDefinition.enum.length; ++i) {
      if (!Array.isArray(tagDefinition.enum[i])) {
        throw new Error('Cannot compare array and non array tag value.');
      }
      if (dwv.utils.arraySortEquals(tagDefinition.enum[i], tagValue)) {
        includes = true;
        break;
      }
    }
  } else {
    // trim
    if (typeof tagValue === 'string') {
      tagValue = dwv.dicom.cleanString(tagValue);
    }

    includes = tagDefinition.enum.includes(tagValue);
  }
  if (!includes) {
    throw new Error(
      'Unsupported ' + tagDefinition.name + ' value: ' + tagValue);
  }
};

/**
 * List of DICOM Seg required tags.
 */
dwv.dicom.requiredDicomSegTags = [
  {
    name: 'TransferSyntaxUID',
    tag: 'x00020010',
    type: '1',
    enum: ['1.2.840.10008.1.2.1']
  },
  {
    name: 'MediaStorageSOPClassUID',
    tag: 'x00020002',
    type: '1',
    enum: ['1.2.840.10008.5.1.4.1.1.66.4']
  },
  {
    name: 'SOPClassUID',
    tag: 'x00020002',
    type: '1',
    enum: ['1.2.840.10008.5.1.4.1.1.66.4']
  },
  {
    name: 'Modality',
    tag: 'x00080060',
    type: '1',
    enum: ['SEG']
  },
  {
    name: 'SegmentationType',
    tag: 'x00620001',
    type: '1',
    enum: ['BINARY']
  },
  {
    name: 'DimensionOrganizationType',
    tag: 'x00209311',
    type: '3',
    enum: ['3D']
  },
  {
    name: 'ImageType',
    tag: 'x00080008',
    type: '1',
    enum: [['DERIVED', 'PRIMARY']]
  },
  {
    name: 'SamplesPerPixel',
    tag: 'x00280002',
    type: '1',
    enum: [1]
  },
  {
    name: 'PhotometricInterpretation',
    tag: 'x00280004',
    type: '1',
    enum: ['MONOCHROME2']
  },
  {
    name: 'PixelRepresentation',
    tag: 'x00280103',
    type: '1',
    enum: [0]
  },
  {
    name: 'BitsAllocated',
    tag: 'x00280100',
    type: '1',
    enum: [1]
  },
  {
    name: 'BitsStored',
    tag: 'x00280101',
    type: '1',
    enum: [1]
  },
  {
    name: 'HighBit',
    tag: 'x00280102',
    type: '1',
    enum: [0]
  },
];

/**
 * Get the default DICOM seg tags as an object.
 *
 * @returns {object} The default tags.
 */
dwv.dicom.getDefaultDicomSegJson = function () {
  var tags = {};
  for (var i = 0; i < dwv.dicom.requiredDicomSegTags.length; ++i) {
    var reqTag = dwv.dicom.requiredDicomSegTags[i];
    tags[reqTag.name] = reqTag.enum[0];
  }
  return tags;
};

/**
 * Check the dimension organization from a dicom element.
 *
 * @param {object} rootElement The root dicom element.
 * @returns {object} The dimension organizations and indices.
 */
dwv.dicom.getDimensionOrganization = function (rootElement) {
  // Dimension Organization Sequence (required)
  var orgSq = rootElement.getFromKey('x00209221', true);
  if (!orgSq || orgSq.length !== 1) {
    throw new Error('Unsupported dimension organization sequence length');
  }
  // Dimension Organization UID
  var orgUID = dwv.dicom.cleanString(orgSq[0].x00209164.value[0]);

  // Dimension Index Sequence (conditionally required)
  var indices = [];
  var indexSq = rootElement.getFromKey('x00209222', true);
  if (indexSq) {
    // expecting 2D index
    if (indexSq.length !== 2) {
      throw new Error('Unsupported dimension index sequence length');
    }
    var indexPointer;
    for (var i = 0; i < indexSq.length; ++i) {
      // Dimension Organization UID (required)
      var indexOrg = dwv.dicom.cleanString(indexSq[i].x00209164.value[0]);
      if (indexOrg !== orgUID) {
        throw new Error(
          'Dimension Index Sequence contains a unknown Dimension Organization');
      }
      // Dimension Index Pointer (required)
      indexPointer = dwv.dicom.cleanString(indexSq[i].x00209165.value[0]);

      var index = {
        DimensionOrganizationUID: indexOrg,
        DimensionIndexPointer: indexPointer
      };
      // Dimension Description Label (optional)
      if (typeof indexSq[i].x00209421 !== 'undefined') {
        index.DimensionDescriptionLabel =
          dwv.dicom.cleanString(indexSq[i].x00209421.value[0]);
      }
      // store
      indices.push(index);
    }
    // expecting Image Position at last position
    if (indexPointer !== '(0020,0032)') {
      throw new Error('Unsupported non image position as last index');
    }
  }

  return {
    organizations: {
      value: [
        {
          DimensionOrganizationUID: orgUID
        }
      ]
    },
    indices: {
      value: indices
    }
  };
};

/**
 * Get a code object from a dicom element.
 *
 * @param {object} element The dicom element.
 * @returns {object} A code object.
 */
dwv.dicom.getCode = function (element) {
  // meaning -> CodeMeaning (type1)
  var code = {
    meaning: dwv.dicom.cleanString(element.x00080104.value[0])
  };
  // value -> CodeValue (type1C)
  // longValue -> LongCodeValue (type1C)
  // urnValue -> URNCodeValue (type1C)
  if (element.x00080100) {
    code.value = element.x00080100.value[0];
  } else if (element.x00080119) {
    code.longValue = element.x00080119.value[0];
  } else if (element.x00080120) {
    code.urnValue = element.x00080120.value[0];
  } else {
    throw Error('Invalid code with no value, no long value and no urn value.');
  }
  // schemeDesignator -> CodingSchemeDesignator (type1C)
  if (typeof code.value !== 'undefined' ||
    typeof code.longValue !== 'undefined') {
    if (element.x00080102) {
      code.schemeDesignator = element.x00080102.value[0];
    } else {
      throw Error(
        'No coding sheme designator when code value or long value is present');
    }
  }
  return code;
};

/**
 * Get a segment object from a dicom element.
 *
 * @param {object} element The dicom element.
 * @returns {object} A segment object.
 */
dwv.dicom.getSegment = function (element) {
  // number -> SegmentNumber (type1)
  // label -> SegmentLabel (type1)
  // algorithmType -> SegmentAlgorithmType (type1)
  var segment = {
    number: element.x00620004.value[0],
    label: dwv.dicom.cleanString(element.x00620005.value[0]),
    algorithmType: dwv.dicom.cleanString(element.x00620008.value[0])
  };
  // algorithmName -> SegmentAlgorithmName (type1C)
  if (element.x00620009) {
    segment.algorithmName =
      dwv.dicom.cleanString(element.x00620009.value[0]);
  }
  // // required if type is not MANUAL
  // if (segment.algorithmType !== 'MANUAL' &&
  //   (typeof segment.algorithmName === 'undefined' ||
  //   segment.algorithmName.length === 0)) {
  //   throw new Error('Empty algorithm name for non MANUAL algorithm type.');
  // }
  // displayValue ->
  // - RecommendedDisplayGrayscaleValue
  // - RecommendedDisplayCIELabValue converted to RGB
  if (typeof element.x0062000C !== 'undefined') {
    segment.displayValue = element.x006200C.value;
  } else if (typeof element.x0062000D !== 'undefined') {
    var cielabElement = element.x0062000D.value;
    var rgb = dwv.utils.cielabToSrgb(dwv.utils.uintLabToLab({
      l: cielabElement[0],
      a: cielabElement[1],
      b: cielabElement[2]
    }));
    segment.displayValue = rgb;
  }
  // Segmented Property Category Code Sequence (type1, only one)
  if (typeof element.x00620003 !== 'undefined') {
    segment.propertyCategoryCode =
      dwv.dicom.getCode(element.x00620003.value[0]);
  } else {
    throw Error('Missing Segmented Property Category Code Sequence.');
  }
  // Segmented Property Type Code Sequence (type1)
  if (typeof element.x0062000F !== 'undefined') {
    segment.propertyTypeCode =
      dwv.dicom.getCode(element.x0062000F.value[0]);
  } else {
    throw Error('Missing Segmented Property Type Code Sequence.');
  }
  // tracking Id and UID (type1C)
  if (typeof element.x00620020 !== 'undefined') {
    segment.trackingId = element.x00620020.value[0];
    segment.trackingUid = element.x00620021.value[0];
  }

  return segment;
};

/**
 * Check if two segment objects are equal.
 *
 * @param {object} seg1 The first segment.
 * @param {object} seg2 The second segment.
 * @returns {boolean} True if both segment are equal.
 */
dwv.dicom.isEqualSegment = function (seg1, seg2) {
  // basics
  if (typeof seg1 === 'undefined' ||
    typeof seg2 === 'undefined' ||
    seg1 === null ||
    seg2 === null) {
    return false;
  }
  var isEqual = seg1.number === seg2.number &&
    seg1.label === seg2.label &&
    seg1.algorithmType === seg2.algorithmType;
  // rgb
  if (typeof seg1.displayValue.r !== 'undefined') {
    if (typeof seg2.displayValue.r === 'undefined') {
      isEqual = false;
    } else {
      isEqual = isEqual &&
        dwv.utils.isEqualRgb(seg1.displayValue, seg2.displayValue);
    }
  } else {
    isEqual = isEqual &&
      seg1.displayValue === seg2.displayValue;
  }
  // algorithmName
  if (typeof seg1.algorithmName !== 'undefined') {
    if (typeof seg2.algorithmName === 'undefined') {
      isEqual = false;
    } else {
      isEqual = isEqual &&
        seg1.algorithmName === seg2.algorithmName;
    }
  }

  return isEqual;
};

/**
 * Check if two segment objects are similar: either the
 * number or the displayValue are equal.
 *
 * @param {object} seg1 The first segment.
 * @param {object} seg2 The second segment.
 * @returns {boolean} True if both segment are similar.
 */
dwv.dicom.isSimilarSegment = function (seg1, seg2) {
  // basics
  if (typeof seg1 === 'undefined' ||
    typeof seg2 === 'undefined' ||
    seg1 === null ||
    seg2 === null) {
    return false;
  }
  var isSimilar = seg1.number === seg2.number;
  // rgb
  if (typeof seg1.displayValue.r !== 'undefined') {
    if (typeof seg2.displayValue.r === 'undefined') {
      isSimilar = false;
    } else {
      isSimilar = isSimilar ||
        dwv.utils.isEqualRgb(seg1.displayValue, seg2.displayValue);
    }
  } else {
    isSimilar = isSimilar ||
      seg1.displayValue === seg2.displayValue;
  }

  return isSimilar;
};

/**
 * Get a spacing object from a dicom measure element.
 *
 * @param {object} measure The dicom element.
 * @returns {dwv.image.Spacing} A spacing object.
 */
dwv.dicom.getSpacingFromMeasure = function (measure) {
  // Pixel Spacing
  if (typeof measure.x00280030 === 'undefined') {
    return null;
  }
  var pixelSpacing = measure.x00280030;
  var spacingValues = [
    parseFloat(pixelSpacing.value[0]),
    parseFloat(pixelSpacing.value[1])
  ];
  // Slice Thickness
  if (typeof measure.x00180050 !== 'undefined') {
    spacingValues.push(parseFloat(measure.x00180050.value[0]));
  } else if (typeof measure.x00180088 !== 'undefined') {
    // Spacing Between Slices
    spacingValues.push(parseFloat(measure.x00180088.value[0]));
  }
  return new dwv.image.Spacing(spacingValues);
};

/**
 * Get a frame information object from a dicom element.
 *
 * @param {object} groupItem The dicom element.
 * @returns {object} A frame information object.
 */
dwv.dicom.getSegmentFrameInfo = function (groupItem) {
  // Derivation Image Sequence
  var derivationImages = [];
  if (typeof groupItem.x00089124 !== 'undefined') {
    var derivationImageSq = groupItem.x00089124.value;
    // Source Image Sequence
    for (var i = 0; i < derivationImageSq.length; ++i) {
      var sourceImages = [];
      if (typeof derivationImageSq[i].x00082112 !== 'undefined') {
        var sourceImageSq = derivationImageSq[i].x00082112.value;
        for (var j = 0; j < sourceImageSq.length; ++j) {
          var sourceImage = {};
          // Referenced SOP Class UID
          if (typeof sourceImageSq[j].x00081150 !== 'undefined') {
            sourceImage.referencedSOPClassUID =
              sourceImageSq[j].x00081150.value[0];
          }
          // Referenced SOP Instance UID
          if (typeof sourceImageSq[j].x00081155 !== 'undefined') {
            sourceImage.referencedSOPInstanceUID =
              sourceImageSq[j].x00081155.value[0];
          }
          sourceImages.push(sourceImage);
        }
      }
      derivationImages.push(sourceImages);
    }
  }
  // Frame Content Sequence (required, only one)
  var frameContentSq = groupItem.x00209111.value;
  // Dimension Index Value
  var dimIndex = frameContentSq[0].x00209157.value;
  // Segment Identification Sequence (required, only one)
  var segmentIdSq = groupItem.x0062000A.value;
  // Referenced Segment Number
  var refSegmentNumber = segmentIdSq[0].x0062000B.value[0];
  // Plane Position Sequence (required, only one)
  var planePosSq = groupItem.x00209113.value;
  // Image Position (Patient) (conditionally required)
  var imagePosPat = planePosSq[0].x00200032.value;
  for (var p = 0; p < imagePosPat.length; ++p) {
    imagePosPat[p] = parseFloat(imagePosPat[p], 10);
  }
  var frameInfo = {
    dimIndex: dimIndex,
    imagePosPat: imagePosPat,
    derivationImages: derivationImages,
    refSegmentNumber: refSegmentNumber
  };
  // Plane Orientation Sequence
  if (typeof groupItem.x00209116 !== 'undefined') {
    var framePlaneOrientationSeq = groupItem.x00209116;
    if (framePlaneOrientationSeq.value.length !== 0) {
      // should only be one Image Orientation (Patient)
      var frameImageOrientation =
        framePlaneOrientationSeq.value[0].x00200037.value;
      if (typeof frameImageOrientation !== 'undefined') {
        frameInfo.imageOrientationPatient = frameImageOrientation;
      }
    }
  }
  // Pixel Measures Sequence
  if (typeof groupItem.x00289110 !== 'undefined') {
    var framePixelMeasuresSeq = groupItem.x00289110;
    if (framePixelMeasuresSeq.value.length !== 0) {
      // should only be one
      var frameSpacing =
        dwv.dicom.getSpacingFromMeasure(framePixelMeasuresSeq.value[0]);
      if (typeof frameSpacing !== 'undefined') {
        frameInfo.spacing = frameSpacing;
      }
    } else {
      dwv.logger.warn(
        'No shared functional group pixel measure sequence items.');
    }
  }

  return frameInfo;
};

/**
 * Mask {@link dwv.image.Image} factory.
 *
 * @class
 */
dwv.image.MaskFactory = function () {};

/**
 * Get an {@link dwv.image.Image} object from the read DICOM file.
 *
 * @param {object} dicomElements The DICOM tags.
 * @param {Array} pixelBuffer The pixel buffer.
 * @returns {dwv.image.Image} A new Image.
 */
dwv.image.MaskFactory.prototype.create = function (
  dicomElements, pixelBuffer) {
  // check required and supported tags
  for (var d = 0; d < dwv.dicom.requiredDicomSegTags.length; ++d) {
    dwv.dicom.checkTag(dicomElements, dwv.dicom.requiredDicomSegTags[d]);
  }

  // columns
  var columns = dicomElements.getFromKey('x00280011');
  if (!columns) {
    throw new Error('Missing or empty DICOM image number of columns');
  }
  // rows
  var rows = dicomElements.getFromKey('x00280010');
  if (!rows) {
    throw new Error('Missing or empty DICOM image number of rows');
  }
  var sliceSize = columns * rows;

  // frames
  var frames = dicomElements.getFromKey('x00280008');
  if (!frames) {
    frames = 1;
  } else {
    frames = parseInt(frames, 10);
  }

  if (frames !== pixelBuffer.length / sliceSize) {
    throw new Error(
      'Buffer and numberOfFrames meta are not equal.' +
      frames + ' ' + pixelBuffer.length / sliceSize);
  }

  // Dimension Organization and Index
  var dimension = dwv.dicom.getDimensionOrganization(dicomElements);

  // Segment Sequence
  var segSequence = dicomElements.getFromKey('x00620002', true);
  if (!segSequence || typeof segSequence === 'undefined') {
    throw new Error('Missing or empty segmentation sequence');
  }
  var segments = [];
  var storeAsRGB = false;
  for (var i = 0; i < segSequence.length; ++i) {
    var segment = dwv.dicom.getSegment(segSequence[i]);
    if (typeof segment.displayValue.r !== 'undefined' &&
      typeof segment.displayValue.g !== 'undefined' &&
      typeof segment.displayValue.b !== 'undefined') {
      // create rgb image
      storeAsRGB = true;
    }
    // store
    segments.push(segment);
  }

  // image size
  var size = dicomElements.getImageSize();

  // Shared Functional Groups Sequence
  var spacing;
  var imageOrientationPatient;
  var sharedFunctionalGroupsSeq = dicomElements.getFromKey('x52009229', true);
  if (sharedFunctionalGroupsSeq && sharedFunctionalGroupsSeq.length !== 0) {
    // should be only one
    var funcGroup0 = sharedFunctionalGroupsSeq[0];
    // Plane Orientation Sequence
    if (typeof funcGroup0.x00209116 !== 'undefined') {
      var planeOrientationSeq = funcGroup0.x00209116;
      if (planeOrientationSeq.value.length !== 0) {
        // should be only one
        imageOrientationPatient = planeOrientationSeq.value[0].x00200037.value;
      } else {
        dwv.logger.warn(
          'No shared functional group plane orientation sequence items.');
      }
    }
    // Pixel Measures Sequence
    if (typeof funcGroup0.x00289110 !== 'undefined') {
      var pixelMeasuresSeq = funcGroup0.x00289110;
      if (pixelMeasuresSeq.value.length !== 0) {
        // should be only one
        spacing = dwv.dicom.getSpacingFromMeasure(pixelMeasuresSeq.value[0]);
      } else {
        dwv.logger.warn(
          'No shared functional group pixel measure sequence items.');
      }
    }
  }

  var includesPosPat = function (arr, val) {
    return arr.some(function (arrVal) {
      return dwv.dicom.equalPosPat(val, arrVal);
    });
  };

  var findIndexPosPat = function (arr, val) {
    return arr.findIndex(function (arrVal) {
      return dwv.dicom.equalPosPat(val, arrVal);
    });
  };

  // Per-frame Functional Groups Sequence
  var perFrameFuncGroupSequence = dicomElements.getFromKey('x52009230', true);
  if (!perFrameFuncGroupSequence ||
    typeof perFrameFuncGroupSequence === 'undefined') {
    throw new Error('Missing or empty per frame functional sequence');
  }
  if (frames !== perFrameFuncGroupSequence.length) {
    throw new Error(
      'perFrameFuncGroupSequence meta and numberOfFrames are not equal.');
  }
  // create frame info object from per frame func
  var frameInfos = [];
  for (var j = 0; j < perFrameFuncGroupSequence.length; ++j) {
    frameInfos.push(
      dwv.dicom.getSegmentFrameInfo(perFrameFuncGroupSequence[j]));
  }

  // check frame infos
  var framePosPats = [];
  for (var ii = 0; ii < frameInfos.length; ++ii) {
    if (!includesPosPat(framePosPats, frameInfos[ii].imagePosPat)) {
      framePosPats.push(frameInfos[ii].imagePosPat);
    }
    // store orientation if needed, avoid multi
    if (typeof frameInfos[ii].imageOrientationPatient !== 'undefined') {
      if (typeof imageOrientationPatient === 'undefined') {
        imageOrientationPatient = frameInfos[ii].imageOrientationPatient;
      } else {
        if (!dwv.utils.arraySortEquals(
          imageOrientationPatient, frameInfos[ii].imageOrientationPatient)) {
          throw new Error('Unsupported multi orientation dicom seg.');
        }
      }
    }
    // store spacing if needed, avoid multi
    if (typeof frameInfos[ii].spacing !== 'undefined') {
      if (typeof spacing === 'undefined') {
        spacing = frameInfos[ii].spacing;
      } else {
        if (!spacing.equals(frameInfos[ii].spacing)) {
          throw new Error('Unsupported multi resolution dicom seg.');
        }
      }
    }
  }

  // check spacing and orientation
  if (typeof spacing === 'undefined') {
    throw new Error('No spacing found for DICOM SEG');
  }
  if (typeof imageOrientationPatient === 'undefined') {
    throw new Error('No imageOrientationPatient found for DICOM SEG');
  }

  // orientation
  var rowCosines = new dwv.math.Vector3D(
    parseFloat(imageOrientationPatient[0]),
    parseFloat(imageOrientationPatient[1]),
    parseFloat(imageOrientationPatient[2]));
  var colCosines = new dwv.math.Vector3D(
    parseFloat(imageOrientationPatient[3]),
    parseFloat(imageOrientationPatient[4]),
    parseFloat(imageOrientationPatient[5]));
  var normal = rowCosines.crossProduct(colCosines);
  /* eslint-disable array-element-newline */
  var orientationMatrix = new dwv.math.Matrix33([
    rowCosines.getX(), colCosines.getX(), normal.getX(),
    rowCosines.getY(), colCosines.getY(), normal.getY(),
    rowCosines.getZ(), colCosines.getZ(), normal.getZ()
  ]);

  // sort positions patient
  framePosPats.sort(dwv.dicom.getComparePosPat(orientationMatrix));

  var point3DFromArray = function (arr) {
    return new dwv.math.Point3D(arr[0], arr[1], arr[2]);
  };

  // frame origins
  var frameOrigins = [];
  for (var n = 0; n < framePosPats.length; ++n) {
    frameOrigins.push(point3DFromArray(framePosPats[n]));
  }

  // use calculated spacing
  var newSpacing = spacing;
  var geoSliceSpacing = dwv.image.getSliceGeometrySpacing(
    frameOrigins, orientationMatrix, false);
  var spacingValues = spacing.getValues();
  if (typeof geoSliceSpacing !== 'undefined' &&
    geoSliceSpacing !== spacingValues[2]) {
    spacingValues[2] = geoSliceSpacing;
    newSpacing = new dwv.image.Spacing(spacingValues);
  }

  // tmp geometry with correct spacing but only one slice
  var tmpGeometry = new dwv.image.Geometry(
    frameOrigins[0], size, newSpacing, orientationMatrix);

  // origin distance test
  var isNotSmall = function (value) {
    var res = value > dwv.math.REAL_WORLD_EPSILON;
    if (res) {
      // try larger epsilon
      res = value > dwv.math.REAL_WORLD_EPSILON * 10;
      if (!res) {
        // warn if epsilon < value < epsilon * 10
        dwv.logger.warn(
          'Using larger real world epsilon in SEG pos pat adding'
        );
      }
    }
    return res;
  };

  // add possibly missing posPats
  var posPats = [];
  posPats.push(framePosPats[0]);
  var sliceIndex = 0;
  for (var g = 1; g < framePosPats.length; ++g) {
    ++sliceIndex;
    var index = new dwv.math.Index([0, 0, sliceIndex]);
    var point = tmpGeometry.indexToWorld(index).get3D();
    var frameOrigin = frameOrigins[g];
    // check if more pos pats are needed
    var dist = frameOrigin.getDistance(point);
    var distPrevious = dist;
    // TODO: good threshold?
    while (isNotSmall(dist)) {
      dwv.logger.debug('Adding intermediate pos pats for DICOM seg at ' +
        point.toString());
      posPats.push([point.getX(), point.getY(), point.getZ()]);
      ++sliceIndex;
      index = new dwv.math.Index([0, 0, sliceIndex]);
      point = tmpGeometry.indexToWorld(index).get3D();
      dist = frameOrigin.getDistance(point);
      if (dist > distPrevious) {
        throw new Error(
          'Test distance is increasing when adding intermediate pos pats');
      }
    }
    // add frame pos pat
    posPats.push(framePosPats[g]);
  }

  // as many slices as posPats
  var numberOfSlices = posPats.length;

  // final geometry
  var geometry = new dwv.image.Geometry(
    frameOrigins[0], size, newSpacing, orientationMatrix);
  var uids = [0];
  for (var m = 1; m < numberOfSlices; ++m) {
    geometry.appendOrigin(point3DFromArray(posPats[m]), m);
    uids.push(m);
  }

  var getFindSegmentFunc = function (number) {
    return function (item) {
      return item.number === number;
    };
  };

  // create output buffer
  var mul = storeAsRGB ? 3 : 1;
  var buffer = new pixelBuffer.constructor(mul * sliceSize * numberOfSlices);
  buffer.fill(0);
  // merge frame buffers
  var sliceOffset = null;
  var frameOffset = null;
  for (var f = 0; f < frameInfos.length; ++f) {
    // get the slice index from the position in the posPat array
    sliceIndex = findIndexPosPat(posPats, frameInfos[f].imagePosPat);
    frameOffset = sliceSize * f;
    sliceOffset = sliceSize * sliceIndex;
    // get the frame display value
    var frameSegment = segments.find(
      getFindSegmentFunc(frameInfos[f].refSegmentNumber)
    );
    var pixelValue = frameSegment.displayValue;
    for (var l = 0; l < sliceSize; ++l) {
      if (pixelBuffer[frameOffset + l] !== 0) {
        var offset = mul * (sliceOffset + l);
        if (storeAsRGB) {
          buffer[offset] = pixelValue.r;
          buffer[offset + 1] = pixelValue.g;
          buffer[offset + 2] = pixelValue.b;
        } else {
          buffer[offset] = pixelValue;
        }
      }
    }
  }

  // create image
  var image = new dwv.image.Image(geometry, buffer, uids);
  if (storeAsRGB) {
    image.setPhotometricInterpretation('RGB');
  }
  // meta information
  var meta = dwv.dicom.getDefaultDicomSegJson();
  // Study
  meta.StudyDate = dicomElements.getFromKey('x00080020');
  meta.StudyTime = dicomElements.getFromKey('x00080030');
  meta.StudyInstanceUID = dicomElements.getFromKey('x0020000D');
  meta.StudyID = dicomElements.getFromKey('x00200010');
  // Series
  meta.SeriesInstanceUID = dicomElements.getFromKey('x0020000E');
  meta.SeriesNumber = dicomElements.getFromKey('x00200011');
  // ReferringPhysicianName
  meta.ReferringPhysicianName = dicomElements.getFromKey('x00080090');
  // patient info
  meta.PatientName =
    dwv.dicom.cleanString(dicomElements.getFromKey('x00100010'));
  meta.PatientID = dwv.dicom.cleanString(dicomElements.getFromKey('x00100020'));
  meta.PatientBirthDate = dicomElements.getFromKey('x00100030');
  meta.PatientSex =
    dwv.dicom.cleanString(dicomElements.getFromKey('x00100040'));
  // Enhanced General Equipment Module
  meta.Manufacturer = dicomElements.getFromKey('x00080070');
  meta.ManufacturerModelName = dicomElements.getFromKey('x00081090');
  meta.DeviceSerialNumber = dicomElements.getFromKey('x00181000');
  meta.SoftwareVersions = dicomElements.getFromKey('x00181020');
  // dicom seg dimension
  meta.DimensionOrganizationSequence = dimension.organizations;
  meta.DimensionIndexSequence = dimension.indices;
  // custom
  meta.custom = {
    segments: segments,
    frameInfos: frameInfos,
    SOPInstanceUID: dicomElements.getFromKey('x00080018')
  };

  // number of files: in this case equal to number slices,
  //   used to calculate buffer size
  meta.numberOfFiles = numberOfSlices;
  // FrameOfReferenceUID (optional)
  var frameOfReferenceUID = dicomElements.getFromKey('x00200052');
  if (frameOfReferenceUID) {
    meta.FrameOfReferenceUID = frameOfReferenceUID;
  }
  // LossyImageCompression (optional)
  var lossyImageCompression = dicomElements.getFromKey('x00282110');
  if (lossyImageCompression) {
    meta.LossyImageCompression = lossyImageCompression;
  }

  image.setMeta(meta);

  return image;
};