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.
- Dimension sizes are positive integers.
SizeXthroughSizeTtakePositiveInt_Typevalues, so a size of zero is rejected where it is set.DimensionOrderstates how the planes are laid out,XYZTCfor a Z-stack acquired channel by channel. - Calibration is a physical quantity.
PhysicalSizeX,PhysicalSizeY, andPhysicalSizeZcarry the size of one pixel or one Z step, with the unit as a separate attribute. - Pixel data is optional.
Pixelsholds a choice between the binary alternatives —BinData,TiffData,NGFFData— andMetadataOnly. SelectingMetadataOnlystates 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.