Skip to content

Describe an Acquired Image

Where an Instrument describes the hardware, an Image describes what came out of it: the dimensions of the dataset, the size of a pixel, the channels acquired, and the time of each plane.

Prerequisites

This example uses the choice mechanism of Choice and Inline Elements to declare a metadata-only document.

Overview

An image description is a tree of four element types.

flowchart LR
    classDef root  fill:#00a8cc,stroke:#00a8cc,stroke-width:4px,color:#fff,font-size:13px,font-weight:bold
    classDef block fill:#00a8cc,stroke:#00a8cc,stroke-width:2px,color:#fff,font-size:12px
    classDef part  fill:#fff,stroke:#00a8cc,stroke-width:1.5px,color:#222,font-size:12px

    IM["Image<br/>identifier · name · stage position"]:::root
    PX["Pixels<br/>sizes · order · pixel type · calibration"]:::block
    CH["Channel<br/>one per channel"]:::part
    PL["Plane<br/>one per Z · C · T"]:::part

    IM -- "setPixels" --> PX
    PX -- "addToChannel_List" --> CH
    PX -- "addToPlane_List" --> PL

    linkStyle 0 stroke:#00a8cc,stroke-width:2px
    linkStyle 1 stroke:#00a8cc,stroke-width:1.5px
    linkStyle 2 stroke:#00a8cc,stroke-width:1.5px
Element Describes
Image The dataset as a whole: identifier, name, stage positions, acquisition date
Pixels The pixel block: dimension sizes, dimension order, pixel type, physical calibration
Channel One channel: name, imaging method, contrast method, light path
Plane One plane: its Z, C, and T indices, its timestamp, its exposure

Three points govern how the tree is filled.

  1. Dimension sizes are positive integers. SizeX through SizeT take PositiveInt_Type values, so a size of zero is rejected where it is set. DimensionOrder states how the planes are laid out, XYZTC for a Z-stack acquired channel by channel.
  2. Calibration is a physical quantity. PhysicalSizeX, PhysicalSizeY, and PhysicalSizeZ carry the size of one pixel or one Z step, with the unit as a separate attribute.
  3. Pixel data is optional. Pixels holds a choice between the binary alternatives — BinData, TiffData, NGFFData — and MetadataOnly. Selecting MetadataOnly states that the pixel data lives outside the document, which is the usual case when the metadata accompanies image files written by the acquisition software.

Planes carry the per-frame provenance: one Plane per Z, C, and T triplet makes the acquisition reconstructable afterwards, since the indices give the position in the block and the timestamp gives the moment of recording.

Note

The dimensions, calibration, and channel names here stand for the values your acquisition software reports. Replace them with the values read from your own acquisition.

Describe a dataset

# Values that would come from the acquisition software
SIZE_X, SIZE_Y, SIZE_Z, SIZE_C, SIZE_T = 2304, 2304, 5, 2, 3
PIXEL_SIZE_XY = 0.108  # micrometers
Z_STEP = 0.5           # micrometers

# Step 1: Describe the pixel block
pixels = inscoper_nbo.Pixels()

pixels_id = inscoper_nbo.PixelsID_Type()
pixels_id.set("Pixels:1")
pixels.setID(pixels_id)

dimension_order = inscoper_nbo.PixelsDimensionOrder_Type()
dimension_order.set("XYZTC")
pixels.setDimensionOrder(dimension_order)

for setter, value in [
    (pixels.setSizeX, SIZE_X),
    (pixels.setSizeY, SIZE_Y),
    (pixels.setSizeZ, SIZE_Z),
    (pixels.setSizeC, SIZE_C),
    (pixels.setSizeT, SIZE_T),
]:
    size = inscoper_nbo.PositiveInt_Type()
    size.set(value)
    setter(size)

pixel_type = inscoper_nbo.PixelType_Type()
pixel_type.set("uint16")
pixels.setPixelType(pixel_type)

# Step 2: Add the physical calibration
for setter, value in [
    (pixels.setPhysicalSizeX, PIXEL_SIZE_XY),
    (pixels.setPhysicalSizeY, PIXEL_SIZE_XY),
    (pixels.setPhysicalSizeZ, Z_STEP),
]:
    physical_size = inscoper_nbo.PositiveFloat_Type()
    physical_size.set(value)
    setter(physical_size)

# Step 3: Declare that the document carries metadata only
# Pixels holds a choice between the binary alternatives (BinData, TiffData,
# NGFFData) and MetadataOnly, used when the pixel data lives elsewhere.
pixels_choice = inscoper_nbo.Pixels_Choice()
metadata_only_list = inscoper_nbo.MetadataOnlyVector()
metadata_only_list.append(inscoper_nbo.MetadataOnly())
pixels_choice.addMetadataOnly(metadata_only_list)
pixels.setpixels_choice(pixels_choice)

# Step 4: Describe the channels
for index, (channel_name, imaging_method, contrast) in enumerate(
    [
        ("DAPI", "Wide-field_Fluorescence", "Fluorescence"),
        ("Brightfield", "Transmitted_Bright-field", "Brightfield"),
    ]
):
    channel = inscoper_nbo.Channel()

    channel_id = inscoper_nbo.ChannelID_Type()
    channel_id.set(f"Channel:{index}")
    channel.setID(channel_id)

    name = inscoper_nbo.Denomination_Type()
    name.set(channel_name)
    channel.setName(name)

    illumination_type = inscoper_nbo.ImagingMethodList_Type()
    illumination_type.set(imaging_method)
    channel.setIlluminationType(illumination_type)

    contrast_method = inscoper_nbo.ChannelContrastMethod_Type()
    contrast_method.set(contrast)
    channel.setContrastMethod(contrast_method)

    # The light path records the optics the channel was acquired through
    light_path = inscoper_nbo.LightPath()
    light_path_id = inscoper_nbo.LSID_Type()
    light_path_id.set(f"LightPath:{index}")
    light_path.setID(light_path_id)
    channel.setLightPath(light_path)

    pixels.addToChannel_List(channel)

# Step 5: Record per-plane timing
timestamp = 0.0
for the_t in range(SIZE_T):
    for the_c in range(SIZE_C):
        for the_z in range(SIZE_Z):
            plane = inscoper_nbo.Plane()

            plane_id = inscoper_nbo.PlaneID_Type()
            plane_id.set(f"Plane:{the_t}:{the_c}:{the_z}")
            plane.setID(plane_id)

            for setter, value in [
                (plane.setTheZ, the_z),
                (plane.setTheC, the_c),
                (plane.setTheT, the_t),
            ]:
                index_value = inscoper_nbo.NonNegativeInt_Type()
                index_value.set(value)
                setter(index_value)

            plane.setTimestamp(timestamp)
            timestamp += 0.05

            pixels.addToPlane_List(plane)

# Step 6: Attach the pixel block to an image and export
image = inscoper_nbo.Image()

image_id = inscoper_nbo.ImageID_Type()
image_id.set("Image:1")
image.setID(image_id)

image_name = inscoper_nbo.Denomination_Type()
image_name.set("Well A1 - Field 1")
image.setName(image_name)

image.setPixels(pixels)

stage_label = inscoper_nbo.StageLabel()
stage_label.setName("Well A1 - Field 1")
stage_label.setX(12500.0)
stage_label.setY(8300.0)
stage_label.setZ(4210.5)
image.addToStageLabel_List(stage_label)

output_path = os.path.join(tempfile.gettempdir(), "image_metadata.xml")
image.toXmlFile(output_path)
print(f"Image description written to {output_path}")
print(f"Planes: {len(image.getPixels().getPlane_List())}")
print(f"Channels: {[c.getName().get() for c in image.getPixels().getChannel_List()]}")

// Values that would come from the acquisition software
constexpr int SIZE_X = 2304;
constexpr int SIZE_Y = 2304;
constexpr int SIZE_Z = 5;
constexpr int SIZE_C = 2;
constexpr int SIZE_T = 3;
constexpr float PIXEL_SIZE_XY = 0.108f;  // micrometers
constexpr float Z_STEP = 0.5f;           // micrometers

int main() {
    // Step 1: Describe the pixel block
    NBO::PixelsPtr pixels = std::make_shared<NBO::Pixels>();

    NBO::PixelsID_TypePtr pixelsId = std::make_shared<NBO::PixelsID_Type>();
    pixelsId->set("Pixels:1");
    pixels->setID(pixelsId);

    NBO::PixelsDimensionOrder_TypePtr dimensionOrder =
        std::make_shared<NBO::PixelsDimensionOrder_Type>();
    dimensionOrder->set("XYZTC");
    pixels->setDimensionOrder(dimensionOrder);

    const std::vector<std::pair<std::function<void(const NBO::PositiveInt_TypePtr &)>, int>> sizes =
        {
            {[&](const NBO::PositiveInt_TypePtr &v) { pixels->setSizeX(v); }, SIZE_X},
            {[&](const NBO::PositiveInt_TypePtr &v) { pixels->setSizeY(v); }, SIZE_Y},
            {[&](const NBO::PositiveInt_TypePtr &v) { pixels->setSizeZ(v); }, SIZE_Z},
            {[&](const NBO::PositiveInt_TypePtr &v) { pixels->setSizeC(v); }, SIZE_C},
            {[&](const NBO::PositiveInt_TypePtr &v) { pixels->setSizeT(v); }, SIZE_T},
        };

    for (const auto &[setter, value] : sizes) {
        NBO::PositiveInt_TypePtr size = std::make_shared<NBO::PositiveInt_Type>();
        size->set(value);
        setter(size);
    }

    NBO::PixelType_TypePtr pixelType = std::make_shared<NBO::PixelType_Type>();
    pixelType->set("uint16");
    pixels->setPixelType(pixelType);

    // Step 2: Add the physical calibration
    const std::vector<std::pair<std::function<void(const NBO::PositiveFloat_TypePtr &)>, float>>
        calibration = {
            {[&](const NBO::PositiveFloat_TypePtr &v) { pixels->setPhysicalSizeX(v); },
             PIXEL_SIZE_XY},
            {[&](const NBO::PositiveFloat_TypePtr &v) { pixels->setPhysicalSizeY(v); },
             PIXEL_SIZE_XY},
            {[&](const NBO::PositiveFloat_TypePtr &v) { pixels->setPhysicalSizeZ(v); }, Z_STEP},
        };

    for (const auto &[setter, value] : calibration) {
        NBO::PositiveFloat_TypePtr physicalSize = std::make_shared<NBO::PositiveFloat_Type>();
        physicalSize->set(value);
        setter(physicalSize);
    }

    // Step 3: Declare that the document carries metadata only
    // Pixels holds a choice between the binary alternatives (BinData, TiffData,
    // NGFFData) and MetadataOnly, used when the pixel data lives elsewhere.
    NBO::Pixels_ChoicePtr pixelsChoice = std::make_shared<NBO::Pixels_Choice>();
    std::vector<NBO::MetadataOnlyPtr> metadataOnlyList{std::make_shared<NBO::MetadataOnly>()};
    pixelsChoice->addMetadataOnly(metadataOnlyList);
    pixels->setpixels_choice(pixelsChoice);

    // Step 4: Describe the channels
    const std::vector<std::tuple<std::string, std::string, std::string>> channels = {
        {"DAPI", "Wide-field_Fluorescence", "Fluorescence"},
        {"Brightfield", "Transmitted_Bright-field", "Brightfield"},
    };

    int channelIndex = 0;
    for (const auto &[channelName, imagingMethod, contrast] : channels) {
        NBO::ChannelPtr channel = std::make_shared<NBO::Channel>();

        NBO::ChannelID_TypePtr channelId = std::make_shared<NBO::ChannelID_Type>();
        channelId->set("Channel:" + std::to_string(channelIndex));
        channel->setID(channelId);

        NBO::Denomination_TypePtr name = std::make_shared<NBO::Denomination_Type>();
        name->set(channelName);
        channel->setName(name);

        NBO::ImagingMethodList_TypePtr illuminationType =
            std::make_shared<NBO::ImagingMethodList_Type>();
        illuminationType->set(imagingMethod);
        channel->setIlluminationType(illuminationType);

        NBO::ChannelContrastMethod_TypePtr contrastMethod =
            std::make_shared<NBO::ChannelContrastMethod_Type>();
        contrastMethod->set(contrast);
        channel->setContrastMethod(contrastMethod);

        // The light path records the optics the channel was acquired through
        NBO::LightPathPtr lightPath = std::make_shared<NBO::LightPath>();
        NBO::LSID_TypePtr lightPathId = std::make_shared<NBO::LSID_Type>();
        lightPathId->set("LightPath:" + std::to_string(channelIndex));
        lightPath->setID(lightPathId);
        channel->setLightPath(lightPath);

        pixels->addToChannel_List(channel);
        ++channelIndex;
    }

    // Step 5: Record per-plane timing
    float timestamp = 0.0f;
    for (int theT = 0; theT < SIZE_T; ++theT) {
        for (int theC = 0; theC < SIZE_C; ++theC) {
            for (int theZ = 0; theZ < SIZE_Z; ++theZ) {
                NBO::PlanePtr plane = std::make_shared<NBO::Plane>();

                NBO::PlaneID_TypePtr planeId = std::make_shared<NBO::PlaneID_Type>();
                planeId->set("Plane:" + std::to_string(theT) + ":" + std::to_string(theC) + ":"
                             + std::to_string(theZ));
                plane->setID(planeId);

                const std::vector<
                    std::pair<std::function<void(const NBO::NonNegativeInt_TypePtr &)>, int>>
                    indices = {
                        {[&](const NBO::NonNegativeInt_TypePtr &v) { plane->setTheZ(v); }, theZ},
                        {[&](const NBO::NonNegativeInt_TypePtr &v) { plane->setTheC(v); }, theC},
                        {[&](const NBO::NonNegativeInt_TypePtr &v) { plane->setTheT(v); }, theT},
                    };

                for (const auto &[setter, value] : indices) {
                    NBO::NonNegativeInt_TypePtr indexValue =
                        std::make_shared<NBO::NonNegativeInt_Type>();
                    indexValue->set(value);
                    setter(indexValue);
                }

                plane->setTimestamp(timestamp);
                timestamp += 0.05f;

                pixels->addToPlane_List(plane);
            }
        }
    }

    // Step 6: Attach the pixel block to an image and export
    NBO::Image image;

    NBO::ImageID_TypePtr imageId = std::make_shared<NBO::ImageID_Type>();
    imageId->set("Image:1");
    image.setID(imageId);

    NBO::Denomination_TypePtr imageName = std::make_shared<NBO::Denomination_Type>();
    imageName->set("Well A1 - Field 1");
    image.setName(imageName);

    image.setPixels(pixels);

    NBO::StageLabelPtr stageLabel = std::make_shared<NBO::StageLabel>();
    stageLabel->setName("Well A1 - Field 1");
    stageLabel->setX(12500.0f);
    stageLabel->setY(8300.0f);
    stageLabel->setZ(4210.5f);
    image.addToStageLabel_List(stageLabel);

    std::filesystem::path outputPath =
        std::filesystem::temp_directory_path() / "image_metadata.xml";
    image.toXmlFile(outputPath.string());
    std::cout << "Image description written to " << outputPath.string() << std::endl;
    std::cout << "Planes: " << image.getPixels()->getPlane_List().size() << std::endl;
    std::cout << "Channels: ";
    for (const auto &channel : image.getPixels()->getChannel_List()) {
        std::cout << channel->getName()->get() << " ";
    }
    std::cout << std::endl;

    return 0;
}

    // Values that would come from the acquisition software
    static final int SIZE_X = 2304;
    static final int SIZE_Y = 2304;
    static final int SIZE_Z = 5;
    static final int SIZE_C = 2;
    static final int SIZE_T = 3;
    static final float PIXEL_SIZE_XY = 0.108f;   // micrometers
    static final float Z_STEP = 0.5f;            // micrometers

    record ChannelSpec(String name, String imagingMethod, String contrast) {
    }

    public static void main(String[] args) {
        // Step 1: Describe the pixel block
        Pixels pixels = new Pixels();

        PixelsID_Type pixelsId = new PixelsID_Type();
        pixelsId.set("Pixels:1");
        pixels.setID(pixelsId);

        PixelsDimensionOrder_Type dimensionOrder = new PixelsDimensionOrder_Type();
        dimensionOrder.set("XYZTC");
        pixels.setDimensionOrder(dimensionOrder);

        setSize(pixels::setSizeX, SIZE_X);
        setSize(pixels::setSizeY, SIZE_Y);
        setSize(pixels::setSizeZ, SIZE_Z);
        setSize(pixels::setSizeC, SIZE_C);
        setSize(pixels::setSizeT, SIZE_T);

        PixelType_Type pixelType = new PixelType_Type();
        pixelType.set("uint16");
        pixels.setPixelType(pixelType);

        // Step 2: Add the physical calibration
        setPhysicalSize(pixels::setPhysicalSizeX, PIXEL_SIZE_XY);
        setPhysicalSize(pixels::setPhysicalSizeY, PIXEL_SIZE_XY);
        setPhysicalSize(pixels::setPhysicalSizeZ, Z_STEP);

        // Step 3: Declare that the document carries metadata only
        // Pixels holds a choice between the binary alternatives (BinData, TiffData,
        // NGFFData) and MetadataOnly, used when the pixel data lives elsewhere.
        Pixels_Choice pixelsChoice = new Pixels_Choice();
        MetadataOnlyVector metadataOnlyList = new MetadataOnlyVector();
        metadataOnlyList.add(new MetadataOnly());
        pixelsChoice.addMetadataOnly(metadataOnlyList);
        pixels.setpixels_choice(pixelsChoice);

        // Step 4: Describe the channels
        ChannelSpec[] channels = {
            new ChannelSpec("DAPI", "Wide-field_Fluorescence", "Fluorescence"),
            new ChannelSpec("Brightfield", "Transmitted_Bright-field", "Brightfield"),
        };

        int channelIndex = 0;
        for (ChannelSpec spec : channels) {
            Channel channel = new Channel();

            ChannelID_Type channelId = new ChannelID_Type();
            channelId.set("Channel:" + channelIndex);
            channel.setID(channelId);

            Denomination_Type name = new Denomination_Type();
            name.set(spec.name());
            channel.setName(name);

            ImagingMethodList_Type illuminationType = new ImagingMethodList_Type();
            illuminationType.set(spec.imagingMethod());
            channel.setIlluminationType(illuminationType);

            ChannelContrastMethod_Type contrastMethod = new ChannelContrastMethod_Type();
            contrastMethod.set(spec.contrast());
            channel.setContrastMethod(contrastMethod);

            // The light path records the optics the channel was acquired through
            LightPath lightPath = new LightPath();
            LSID_Type lightPathId = new LSID_Type();
            lightPathId.set("LightPath:" + channelIndex);
            lightPath.setID(lightPathId);
            channel.setLightPath(lightPath);

            pixels.addToChannel_List(channel);
            channelIndex++;
        }

        // Step 5: Record per-plane timing
        float timestamp = 0.0f;
        for (int theT = 0; theT < SIZE_T; theT++) {
            for (int theC = 0; theC < SIZE_C; theC++) {
                for (int theZ = 0; theZ < SIZE_Z; theZ++) {
                    Plane plane = new Plane();

                    PlaneID_Type planeId = new PlaneID_Type();
                    planeId.set("Plane:" + theT + ":" + theC + ":" + theZ);
                    plane.setID(planeId);

                    setIndex(plane::setTheZ, theZ);
                    setIndex(plane::setTheC, theC);
                    setIndex(plane::setTheT, theT);

                    plane.setTimestamp(timestamp);
                    timestamp += 0.05f;

                    pixels.addToPlane_List(plane);
                }
            }
        }

        // Step 6: Attach the pixel block to an image and export
        Image image = new Image();

        ImageID_Type imageId = new ImageID_Type();
        imageId.set("Image:1");
        image.setID(imageId);

        Denomination_Type imageName = new Denomination_Type();
        imageName.set("Well A1 - Field 1");
        image.setName(imageName);

        image.setPixels(pixels);

        StageLabel stageLabel = new StageLabel();
        stageLabel.setName("Well A1 - Field 1");
        stageLabel.setX(12500.0f);
        stageLabel.setY(8300.0f);
        stageLabel.setZ(4210.5f);
        image.addToStageLabel_List(stageLabel);

        Path outputPath = Paths.get(System.getProperty("java.io.tmpdir"), "image_metadata.xml");
        image.toXmlFile(outputPath.toString());
        System.out.println("Image description written to " + outputPath);
        System.out.println("Planes: " + image.getPixels().getPlane_List().size());
        System.out.print("Channels: ");
        for (Channel channel : image.getPixels().getChannel_List()) {
            System.out.print(channel.getName().get() + " ");
        }
        System.out.println();
    }

    // Dimension sizes, physical sizes, and plane indices are restricted types:
    // instantiate, set, and pass the object to the accessor.
    static void setSize(Consumer<PositiveInt_Type> setter, int value) {
        PositiveInt_Type size = new PositiveInt_Type();
        size.set(value);
        setter.accept(size);
    }

    static void setPhysicalSize(Consumer<PositiveFloat_Type> setter, float value) {
        PositiveFloat_Type physicalSize = new PositiveFloat_Type();
        physicalSize.set(value);
        setter.accept(physicalSize);
    }

    static void setIndex(Consumer<NonNegativeInt_Type> setter, int value) {
        NonNegativeInt_Type indexValue = new NonNegativeInt_Type();
        indexValue.set(value);
        setter.accept(indexValue);
    }

Linking the image to its instrument

An image description becomes traceable when it names the hardware that produced it. Describe the microscope once, as in Describe an Instrument or through Import a MicroMetaApp Configuration, and reference its identifiers from the settings elements of the image.