npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bettermedicine/imeasure

v0.0.11

Published

This is a collection of TypeScript types and utility functions for working with Better Medicine's iMeasure API.

Readme

Imeasure SDK

This is a collection of TypeScript types and utility functions for working with Better Medicine's iMeasure API.

Table of Contents

You can find the primary documentation site at https://docs.imeasure.bettermedicine.ai. Module references mentioned in this document can be located under the @bettermedicine/namespaces sidebar route.

Installation

npm install @bettermedicine/imeasure

Usage

This package should prove useful if you're trying to integrate with Better Medicine's iMeasure API, to varying extent depending on what type of application you're building.

| Type of application | Request & response types | Crop region utils | Cropping utils | World metadata parsing utils | Request body formatting | | ---------------------------------- | ------------------------ | ----------------- | -------------- | ---------------------------- | ----------------------- | | OHIF-based application | ✅ | ✅ | ✅ | ✅ | ✅ | | Other vtk.js-based application | ✅ | ✅ | ✅ | | ✅ | | Other JavaScript-based application | ✅ | ✅ | | | ✅ |

For integration targets not written in JavaScript/TypeScript, you can still review this package as a reference implementation.

API request & response types

import { Types } from '@bettermedicine/imeasure';

type RequestMetadata = Types.IO.IStaticIMeasurePayload;
type Response = Types.IO.TIMeasure3DResponse

Please refer to the Types.IO module in the documentation for more details on the request and response types.

Cropping

To generate a segmentation and derive measurements from it, the iMeasure API requires a subset of the series' images' pixel data. Sending us the entire series' pixel data is not feasible so instead, we expect a cropped 3D cuboid of the series' voxel data to be supplied with each request, along with some metadata both about the cuboid and the series' coordinate space.

You can find more information regarding data format requirements and a walkthrough of how to calculate the cropping regions in the main unit of our API documentation.

Cropping region calulation

import { math, cropping } from '@bettermedicine/imeasure';

// This example assumes you're working in a callback for a Cornerstone measurement event.
// Here, we expect the `points` property to conform to [[x1,y1,z1], [x2,y2,z2]] in the world (not voxel) coordinate system.
const { measurement: { points }} = evt;

// In order to make the calculation, we need some information about the world coordinate space.
// In this example, we abstract away locating and loading the vtk.js imageData object but it should be noted that
// there's more options for deriving this.
// see the Affines section below for more details.
const voxelToWorldAffine = math.affines.getAffineMatrixFromVTKImageData(relevantVTKVolume)

// this gets us all the information we need to execute the crop.
const cropRegion = math.calculateStaticImeasureBoundingBox(
    points,
    voxelToWorldAffine
);

Executing the crop

// To continue the above example,
const imageCrop =  cropping.cropFromVTKVolumeByCropBoundaries(
    relevantVTKVolume,
    cropRegion.cropBoundariesVoxel
)

const {
    voxelValues, // a flattened array of voxel values from the cropped region
    volume, // the cropped vtk.js imageData object
    cropMeta // crop metadata you'll need to supply in the request
} = imageCrop;

:bulb: Note: In case the measurement we're generating the crop region around is near the edge of the volume or in case the measurement is long enough, the crop region may extend beyond the volume's boundaries. In such cases, the consumer is expected to provide a non-cuboid input volume but still preserve metadata about the originally intended crop region. The utilities provided in the cropping module will handle this case automatically by supplying cropBoundariesUnclippedVoxel and cropCuboidCenterVoxel corresponding to the original crop region, while the cropBoundariesVoxel, cropBoundariesWorld and shape attributes in TCropMetadata will be adjusted to the actual effective cropped region.

Working with data from GPU texture buffers

In modern versions of Cornerstone 3D, unless forced by configuration, the image volume's voxel data only exists in the GPU texture buffer. If the above function outputs empty voxelValues, we need to take a few additional steps to ensure the data is reachable by our code:

import { cache as csCache } from '@cornerstonejs/core';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';

// note: at least in the current version of OHIF, the volume ID conforms to
// `${volumeLoaderScheme}:${displaySetInstanceUID}`.
// as such, using standard volume viewports, you should be able to
// refer to `cornerstoneStreamingImageVolume:${displaySetInstanceUID}`
const volumeID = 'some-cs-volume-id';
const scalarsInstanceName = 'Scalars';


// 1. we get a reference to the Cornerstone volume from Cornerstone's own cache
const baseVolume = csCache.getVolume(volumeID);

if (!baseVolume) {
    throw new Error(`Volume with ID ${volumeID} not found in Cornerstone cache.`);
}

if (!baseVolume.voxelManager) {
    throw new Error(
        `Volume does not have voxel manager, ID ${volumeID}`
    );
}

// 2. we create a new vtkDataArray instance we'll be using as a container for the scalar data,
//    this time in RAM and accessible by the CPU
const da = vtkDataArray.newInstance({
    name: scalarsInstanceName,
    numberOfComponents: 1,
    // 3. This forces the data to be copied from the GPU texture buffer to the CPU memory.
    values: baseVolume.voxelManager.getCompleteScalarDataArray(),
});

const copiedVolume = vtkImageData.newInstance();
copiedVolume.setDimensions(baseVolume.getDimensions());
copiedVolume.setSpacing(baseVolume.getSpacing());
copiedVolume.setOrigin(baseVolume.getOrigin());
copiedVolume.setDirection(baseVolume.getDirection());
copiedVolume.getPointData().setScalars(da);

Given that image volumes are mostly static and that this process may be computationally expensive,we recommend caching the copied volume so you don't have to re-instantiate it every time you execute a new iMeasure request. As an example;


import { cache as csCache } from '@cornerstonejs/core';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';

type VolumeID = string;
const vtkImageDataCache = new Map<VolumeID, vtkImageData>();

function getVTKImageData(cornerstoneVolumeID: VolumeID): vtkImageData {
    if (vtkImageDataCache.has(volumeID)) {
        return vtkImageDataCache.get(volumeID);
    }

    // If the volume is not cached, we create it as shown above
    const baseVolume = csCache.getVolume(volumeID);
    if (!baseVolume) {
        throw new Error(`Volume with ID ${volumeID} not found in Cornerstone cache.`);
    }

    if (!baseVolume.voxelManager) {
    throw new Error(
        `Volume does not have voxel manager, ID ${volumeID}`
    );
}

    const da = vtkDataArray.newInstance({
        name: 'Scalars',
        numberOfComponents: 1,
        values: baseVolume.voxelManager.getCompleteScalarDataArray(),
    });

    const copiedVolume = vtkImageData.newInstance();
    copiedVolume.setDimensions(baseVolume.getDimensions());
    copiedVolume.setSpacing(baseVolume.getSpacing());
    copiedVolume.setOrigin(baseVolume.getOrigin());
    copiedVolume.setDirection(baseVolume.getDirection());
    copiedVolume.getPointData().setScalars(da);

    vtkImageDataCache.set(volumeID, copiedVolume);
    return copiedVolume;
}
Image data bit depth

DICOM CT HU values typically fit within the range allowance of a 16-bit signed integer. As such, this is what the iMeasure API expects as the scalar data type. Unless you're forcing 16-bit textures via OHIF config, the scalar data may be of the Float32Array type. You can slash the converted vtkImageData object's memory footprint by half by using our formatting.float32ArrayToInt16Array utility inlined into the above vtkDataArray instantiation:

import { formatting } from '@bettermedicine/imeasure';

const da = vtkDataArray.newInstance({
    ...
    values: formatting.float32ArrayToInt16Array(
        baseVolume.voxelManager.getCompleteScalarDataArray()
    ),
});

Affines

In order to accurately calculate the crop region, we need some more information about the the shape, orientation, origin and spacing of the series' world coordinate system. The standard way to express this is through an affine transformation matrix - a 4x4 matrix that describes how to transform points from the voxel coordinate system to the world coordinate system.

The math.affines module provides utilities for deriving this from a number of sources:

import { math } from '@bettermedicine/imeasure';
// 1. from a vtk.js imageData object
const vtkImageData = ...; // your vtk.js imageData object
const voxelToWorldMatrix = math.affines.getAffineMatrixFromVTKImageData(vtkImageData);


// 2. from a Cornerstone volume
const cornerstoneVolume = csCache.getVolume(volumeID);
const voxelToWorldMatrix = math.affines.getAffineMatrixFromCornerstoneVolume(cornerstoneVolume);

// 3. ...or from primitives you collect yourself

type TAffinePrimitivesInput = {
  spacing: vec3;
  origin: vec3;
  direction: mat3;
  extent: [number, number, number, number, number, number];
};

const voxelToWorldMatrix = math.affines.getAffineMatrixFromPrimitives(
  {
    spacing: [1, 1, 1],
    origin: [0, 0, 0],
    direction: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
    extent: [0, 100, 0, 100, 0, 100],
  } as TAffinePrimitivesInput
);

The resulting matrix can then be used to convert voxel coordinates to world coordinates:

import { math } from '@bettermedicine/imeasure';
const voxelToWorldMatrix = ...; // your affine matrix
const voxelPoint = [10, 20, 30]; // a point in voxel coordinates
const worldPoint = math.affines.affineTranslateVec3(voxelPoint, voxelToWorldMatrix)

Should the inverse operation be required, you can invert the matrix:

const worldToVoxelMatrix = math.affines.invertAffine(voxelToWorldMatrix);
const worldPoint = [10, 20, 30]; // a point in world coordinates
const voxelPoint = math.affines.affineTranslateVec3(worldPoint, worldToVoxelMatrix);

World metadata

Please refer to the IO module's documentation for the IStaticIMeasurePayload type.

Composing the request payload

Once you have the cropped image data and the necessary world metadata, you can compose the request payload as follows:

import { formatting  } from '@bettermedicine/imeasure';
const worldMetadata = ...
const cropMetadata = ...
const clicks = ...
const id = uuidv4();
// note: this should be an Int16Array - see the bit depth section above.
const imageVoxelValues = ... // the flattened array of voxel values from the cropped region

const requestPayload = formatting.formatStaticPayload(
    {
        id,
        clicks,
        ...worldMetadata,
        ...cropMetadata,
    },
    imageVoxelValues
);

// the above returns a FormData object you can POST to the iMeasure API (ideally, through an authenticating proxy):
const resp = await fetch('https://api.bettermedicine.com/imeasure', {
    method: 'POST',
    body: requestPayload,
    // note: setting content-type headers manually here is not recommended as `fetch`
    // should handle it correctly automatically.
});

const responseData = await resp.json();

Please see the Types.IO module for in-depth information on both the request and response types.