ZEISS/libczi

Sample Code: Alter scene coordinates in a .CZI File

MichaelDausmann opened this issue · 5 comments

Is your feature request related to a problem? Please describe.
I want to re-write my .czi files to merge scene coordinates so that there is only one scene in the image. I think that will permit me to import data into Omero as a single Image so that my pathology team can look at a single document in that system rather than multiple documents per slide. (full discussion here https://forum.image.sc/t/zoom-from-overview-to-detailed-scan-for-imported-czi-files/85002/13)

Describe the solution you'd like
A Clear and concise working example of code that will enable me to 're-write' a .czi file with altered scene metadata

Describe alternatives you've considered
Here is my best attempt. it runs but a corrupted file results. I took some leads from Src/libCZI_UnitTests/test_readerwriter.cpp. My test file is here (https://zenodo.org/record/8423633)

#include "inc_libCZI.h"
#include <iostream>
#include <optional>
#include <memory>
#include "../libCZI/libCZI_Utilities.h"
#include "../libCZI/libCZI_Write.h"
#include <string>
#include "../libCZI/libCZI_compress.h"

int main() {
    std::cout << "Opening input/output stream" << std::endl;
    const auto io_stream = libCZI::CreateInputOutputStreamForFile(LR"(../../../data/test.czi)");

    // create the reader-writer-object
    const auto reader_writer = libCZI::CreateCZIReaderWriter();
    
    // open the CZI-file
    reader_writer->Create(io_stream);

    // yes I know all files don't have 2156 blocks
    for (int idx = 0; idx <= 2156; idx++) {
        //echo out some information
        std::cout << "Index " << idx << std::endl;

        auto sbBlk = reader_writer->ReadSubBlock(idx);

        // approach copied from Src/libCZI_UnitTests/test_readerwriter.cpp
        libCZI::AddSubBlockInfoMemPtr addInfo;
        addInfo.Clear();
        addInfo.coordinate = sbBlk->GetSubBlockInfo().coordinate; //once I have this simple 'in and out' example working, I intend to mess with coordinates here...
        addInfo.mIndexValid = sbBlk->GetSubBlockInfo().mIndex != std::numeric_limits<int>::max();
        addInfo.mIndex = sbBlk->GetSubBlockInfo().mIndex;
        addInfo.x = sbBlk->GetSubBlockInfo().logicalRect.x;
        addInfo.y = sbBlk->GetSubBlockInfo().logicalRect.y;
        addInfo.logicalWidth = sbBlk->GetSubBlockInfo().logicalRect.w;
        addInfo.logicalHeight = sbBlk->GetSubBlockInfo().logicalRect.h;
        addInfo.physicalWidth = sbBlk->GetSubBlockInfo().physicalSize.w;
        addInfo.physicalHeight = sbBlk->GetSubBlockInfo().physicalSize.h;
        addInfo.PixelType = sbBlk->GetSubBlockInfo().pixelType;

        size_t sizeSbblkData;
        auto sblkdata = sbBlk->GetRawData(libCZI::ISubBlock::MemBlkType::Data, &sizeSbblkData);
        std::unique_ptr<void, decltype(free)*> pBuffer(malloc(sizeSbblkData), free);
        memcpy(pBuffer.get(), sblkdata.get(), sizeSbblkData);

        addInfo.ptrData = pBuffer.get();
        addInfo.dataSize = (uint32_t)sizeSbblkData;

        reader_writer->ReplaceSubBlock(idx, addInfo);
    };

    // close the file (important!)
    reader_writer->Close();
    
    return 0;
}

Additional context
I know that Omero is not necessarily your problem but this workaround would be a pragmatic solution to an important issue for my group and it seems it should be possible. FWIW, I thought the documentation on 'writing a CZI File' (https://zeiss.github.io/libczi/writing_czi.html) was a little thin

Hi,
this is an interesting use-case, thanks for sharing.
I downloaded the CZI-document you provided, and if I get this right, your goal is to remove the presence of "S-index" in the CZI. So, the document looks something like this
image
and you want to get rid of the S-coordinate.

What I put together is this

#include "../libCZI/libCZI.h"
#include <iostream>
#include <vector>

using namespace std;
using namespace libCZI;

static void RemoveSIndex(const std::shared_ptr<ICziReaderWriter>& reader_writer, int index, const SubBlockInfo& info)
{
    AddSubBlockInfo add_sub_block_info;
    add_sub_block_info.Clear();
    add_sub_block_info.coordinate = info.coordinate;
    add_sub_block_info.coordinate.Clear(DimensionIndex::S);

    // we also set the m-index to invalid - we would have to re-create an m-index scheme when removing the S-index,
    //  because "m-index is scene-scoped". So, in order to have a well-formed CZI, we'd need to re-create the m-index.
    //  We don't do this here (yet), and I reckon it is better to have no m-index than an invalid one.
    add_sub_block_info.mIndexValid = false;
    add_sub_block_info.mIndex = numeric_limits<int>::max();
    add_sub_block_info.x = info.logicalRect.x;
    add_sub_block_info.y = info.logicalRect.y;
    add_sub_block_info.logicalWidth = info.logicalRect.w;
    add_sub_block_info.logicalHeight = info.logicalRect.h;
    add_sub_block_info.physicalWidth = info.physicalSize.w;
    add_sub_block_info.physicalHeight = info.physicalSize.h;
    add_sub_block_info.PixelType = info.pixelType;
    add_sub_block_info.pyramid_type = info.pyramidType;

    // note: there is a bug in libCZI here (info.compressionModeRaw is not valid here, we work around
    //        this by using the compression mode of the sub-block which we have to read in any case)
    add_sub_block_info.compressionModeRaw = info.compressionModeRaw;

    auto subBlock = reader_writer->ReadSubBlock(index);
    size_t sub_block_data_size;
    auto sub_block_data = subBlock->GetRawData(ISubBlock::MemBlkType::Data, &sub_block_data_size);
    size_t sub_block_metadata_size;
    auto sub_block_metadata = subBlock->GetRawData(ISubBlock::MemBlkType::Metadata, &sub_block_metadata_size);
    size_t sub_block_attachment_size;
    auto sub_block_attachment = subBlock->GetRawData(ISubBlock::MemBlkType::Attachment, &sub_block_attachment_size);

    // workaround for above bug in libCZI
    add_sub_block_info.compressionModeRaw = subBlock->GetSubBlockInfo().compressionModeRaw;

    add_sub_block_info.sizeData = sub_block_data_size;
    add_sub_block_info.getData =
        [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
        {
            // TODO: validate that offset is is less than sub_block_data_size (and - offset should never be non-zero)
            ptr = static_cast<const uint8_t*>(sub_block_data.get()) + offset;
            size = sub_block_data_size - offset;
            return true;
        };

    add_sub_block_info.sizeMetadata = sub_block_metadata_size;
    if (add_sub_block_info.sizeMetadata > 0)
    {
        add_sub_block_info.getMetaData =
            [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
            {
                // TODO: validate offset
                ptr = static_cast<const uint8_t*>(sub_block_metadata.get()) + offset;
                size = sub_block_metadata_size - offset;
                return true;
            };
    }

    add_sub_block_info.sizeAttachment = sub_block_attachment_size;
    if (add_sub_block_info.sizeAttachment > 0)
    {
        add_sub_block_info.getAttachment =
            [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
            {
                // TODO: validate offset
                ptr = static_cast<const uint8_t*>(sub_block_attachment.get()) + offset;
                size = sub_block_attachment_size - offset;
                return true;
            };
    }

    reader_writer->ReplaceSubBlock(index, add_sub_block_info);
}

int main()
{
    std::cout << "Opening input/output stream" << std::endl;
    const auto io_stream = libCZI::CreateInputOutputStreamForFile(LR"(D:\Data\CZI\libczi#94\test.czi)");

    // create the reader-writer-object
    auto reader_writer = libCZI::CreateCZIReaderWriter();

    // open the CZI-file
    reader_writer->Create(io_stream);

    reader_writer->EnumerateSubBlocks(
        [&](int index, const SubBlockInfo& info) ->bool
    {
        RemoveSIndex(reader_writer, index, info);
        return true;
    });

    reader_writer->Close();

    return 0;
}

which seems to do the right thing - the processed CZI now looks like this:
image

Please find a more complete test-application over here - branch "jbl/#94-remove-S-index".

  • When putting this together, I ran into two bugs of libCZI. One bug was easy to work around (c.f. comments in the code), the other one needs to be changed in libCZI-code (c.f. here). I'll upstream fixes for this in the next days, in the meantime you need make the change in CziReaderWriter.cpp manually.
  • You chose to modify the CZI in-place, which is a bit arcane. Or, a more defensive approach would have been to copy into a new file - for an example of how this can be done, there is a good example here. At least I'd not recommend to do this in-place S-index-tweaking on the original data.
  • There are a couple of complications or things to be considered when "removing the S-index". First of all, the m-index (in the original document with S-index) is defined to be "scoped at scene-level". So, when removing the S-index, the m-index will have to be newly constructed. In above code, this is not done. I choose to remove the m-index altogether, rather than having duplicate m-indices or so. However - without m-index, this is not considered to be a well-formed CZI any more.
  • Next - the pyramid is also scoped at scenes, so after removing the scenes, the pyramid is no longer "valid". Then - the XML-metadata is not adapted (and still refers to the S-index).
  • So - for those reasons (and probably more), the resulting CZI should not be considered well-formed any more.

Having said this - maybe it works for your use-case nevertheless. But be aware that well-formedness of the CZIs which come out here is questionable.

Let me know if this does works for you. And - point taken regarding the documentation, thanks.

Thanks so much @ptahmose for this example. Re: my goal. yes, nearly right, rather than clear the S dimension, I want to set every S dimension to 0, effectively merging the scenes together.

I have managed to reproduce and refine a little your initial attempt and verify that the resulting file can be successfully imported.

here is my second attempt... still very crude I think..

#include "../libCZI/libCZI.h"
#include <iostream>
#include <vector>

using namespace std;
using namespace libCZI;

// I would call this instead 'MergeSIndex'
static void RemoveSIndex(const std::shared_ptr<ICziReaderWriter>& reader_writer, int index, const SubBlockInfo& info)
{
    AddSubBlockInfo add_sub_block_info;
    add_sub_block_info.Clear();
    add_sub_block_info.coordinate = info.coordinate;
    //add_sub_block_info.coordinate.Clear(DimensionIndex::S);
    add_sub_block_info.coordinate.Set(DimensionIndex::S,0); //rather than clear the S index, it seems to work better if I 'merge' all the scenes into one with S0

    // I have set the mIndex just using the enumeration index
    add_sub_block_info.mIndexValid = true;
    add_sub_block_info.mIndex = index;
    add_sub_block_info.x = info.logicalRect.x;
    add_sub_block_info.y = info.logicalRect.y;
    add_sub_block_info.logicalWidth = info.logicalRect.w;
    add_sub_block_info.logicalHeight = info.logicalRect.h;
    add_sub_block_info.physicalWidth = info.physicalSize.w;
    add_sub_block_info.physicalHeight = info.physicalSize.h;
    add_sub_block_info.PixelType = info.pixelType;
    add_sub_block_info.pyramid_type = info.pyramidType;

    // note: there is a bug in libCZI here (info.compressionModeRaw is not valid here, we work around
    //        this by using the compression mode of the sub-block which we have to read in any case)
    add_sub_block_info.compressionModeRaw = info.compressionModeRaw;

    auto subBlock = reader_writer->ReadSubBlock(index);
    size_t sub_block_data_size;
    auto sub_block_data = subBlock->GetRawData(ISubBlock::MemBlkType::Data, &sub_block_data_size);
    size_t sub_block_metadata_size;
    auto sub_block_metadata = subBlock->GetRawData(ISubBlock::MemBlkType::Metadata, &sub_block_metadata_size);
    size_t sub_block_attachment_size;
    auto sub_block_attachment = subBlock->GetRawData(ISubBlock::MemBlkType::Attachment, &sub_block_attachment_size);

    // workaround for above bug in libCZI
    add_sub_block_info.compressionModeRaw = subBlock->GetSubBlockInfo().compressionModeRaw;

    add_sub_block_info.sizeData = sub_block_data_size;
    add_sub_block_info.getData =
        [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
        {
            // TODO: validate that offset is is less than sub_block_data_size (and - offset should never be non-zero)
            ptr = static_cast<const uint8_t*>(sub_block_data.get()) + offset;
            size = sub_block_data_size - offset;
            return true;
        };

    add_sub_block_info.sizeMetadata = sub_block_metadata_size;
    if (add_sub_block_info.sizeMetadata > 0)
    {
        add_sub_block_info.getMetaData =
            [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
            {
                // TODO: validate offset
                ptr = static_cast<const uint8_t*>(sub_block_metadata.get()) + offset;
                size = sub_block_metadata_size - offset;
                return true;
            };
    }

    add_sub_block_info.sizeAttachment = sub_block_attachment_size;
    if (add_sub_block_info.sizeAttachment > 0)
    {
        add_sub_block_info.getAttachment =
            [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
            {
                // TODO: validate offset
                ptr = static_cast<const uint8_t*>(sub_block_attachment.get()) + offset;
                size = sub_block_attachment_size - offset;
                return true;
            };
    }

    reader_writer->ReplaceSubBlock(index, add_sub_block_info);
}

int main()
{
    std::cout << "Opening input/output stream" << std::endl;
    const auto io_stream = libCZI::CreateInputOutputStreamForFile(LR"(test.czi)");

    // create the reader-writer-object
    auto reader_writer = libCZI::CreateCZIReaderWriter();

    // open the CZI-file
    reader_writer->Create(io_stream);

    // I do an initial pass to 'chop the top off' the pyramids so that they all have the same number of levels.
    // If I don't do this, the 'odd' pyramid with less detail (the '1' in my sample file) does not appear in Omero.
    // This approach is very crude
    // a Slightly better approach would be to do an initial pass to establish the minimum zoom that is available on all the pyramids 
    // and then use that as a threshold.  in this example I have done it manually because I know my test file needs only one level chopped off.
    // Perhaps a more sophisticated approach would be to 'build up' the shorter pyramids by creating the upper levels based on the pixels of the ones below
    // but that would be more difficult.
    std::vector<int> indexesToRemove;
    reader_writer->EnumerateSubBlocks(
        [&](int index, const SubBlockInfo& info) ->bool
    {
        double zoom = info.GetZoom();
        if(zoom==0.015625){ 
            std::cout << "Removing Index " << index << " Zoom " << zoom << ": " << libCZI::Utils::DimCoordinateToString(&info.coordinate) << " Rect=" << info.logicalRect << " PhysicalSize=" << info.physicalSize << std::endl;
            indexesToRemove.push_back(index); 
        }
        return true;
    });

    // I do the actual removal out of the EnumerateSubBlocks loop to avoid a segmentation fault
    for(int index : indexesToRemove) {
        reader_writer->RemoveSubBlock(index);
    }

    reader_writer->EnumerateSubBlocks(
        [&](int index, const SubBlockInfo& info) ->bool
    {
        RemoveSIndex(reader_writer, index, info);
        return true;
    });

    reader_writer->Close();

    return 0;
}
'''

And a screenshot showing the file loaded into Omero.  I have been able to zoom into the tip of the '1' 

![Screenshot 2024-02-22 at 12 50 30 pm](https://github.com/ZEISS/libczi/assets/67457186/1224a107-8d40-4d31-83b1-e24337bef4e1)

I will continue to run more tests.  I may also attempt to open a PR onto your "jbl/https://github.com/ZEISS/libczi/issues/94-remove-S-index" branch.  

ok, did a bit more work, now it figures out where to 'chop' the pyramids by inspecting the zoom

#include "../libCZI/libCZI.h"
#include <iostream>
#include <vector>

using namespace std;
using namespace libCZI;

// I would call this instead 'MergeSIndex'
static void RemoveSIndex(const std::shared_ptr<ICziReaderWriter>& reader_writer, int index, const SubBlockInfo& info)
{
    AddSubBlockInfo add_sub_block_info;
    add_sub_block_info.Clear();
    add_sub_block_info.coordinate = info.coordinate;
    //add_sub_block_info.coordinate.Clear(DimensionIndex::S);
    add_sub_block_info.coordinate.Set(DimensionIndex::S,0); //rather than clear the S index, it seems to work better if I 'merge' all the scenes into one with S0

    // I have set the mIndex just using the enumeration index
    add_sub_block_info.mIndexValid = true;
    add_sub_block_info.mIndex = index;
    add_sub_block_info.x = info.logicalRect.x;
    add_sub_block_info.y = info.logicalRect.y;
    add_sub_block_info.logicalWidth = info.logicalRect.w;
    add_sub_block_info.logicalHeight = info.logicalRect.h;
    add_sub_block_info.physicalWidth = info.physicalSize.w;
    add_sub_block_info.physicalHeight = info.physicalSize.h;
    add_sub_block_info.PixelType = info.pixelType;
    add_sub_block_info.pyramid_type = info.pyramidType;

    // note: there is a bug in libCZI here (info.compressionModeRaw is not valid here, we work around
    //        this by using the compression mode of the sub-block which we have to read in any case)
    add_sub_block_info.compressionModeRaw = info.compressionModeRaw;

    auto subBlock = reader_writer->ReadSubBlock(index);
    size_t sub_block_data_size;
    auto sub_block_data = subBlock->GetRawData(ISubBlock::MemBlkType::Data, &sub_block_data_size);
    size_t sub_block_metadata_size;
    auto sub_block_metadata = subBlock->GetRawData(ISubBlock::MemBlkType::Metadata, &sub_block_metadata_size);
    size_t sub_block_attachment_size;
    auto sub_block_attachment = subBlock->GetRawData(ISubBlock::MemBlkType::Attachment, &sub_block_attachment_size);

    // workaround for above bug in libCZI
    add_sub_block_info.compressionModeRaw = subBlock->GetSubBlockInfo().compressionModeRaw;

    add_sub_block_info.sizeData = sub_block_data_size;
    add_sub_block_info.getData =
        [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
        {
            // TODO: validate that offset is is less than sub_block_data_size (and - offset should never be non-zero)
            ptr = static_cast<const uint8_t*>(sub_block_data.get()) + offset;
            size = sub_block_data_size - offset;
            return true;
        };

    add_sub_block_info.sizeMetadata = sub_block_metadata_size;
    if (add_sub_block_info.sizeMetadata > 0)
    {
        add_sub_block_info.getMetaData =
            [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
            {
                // TODO: validate offset
                ptr = static_cast<const uint8_t*>(sub_block_metadata.get()) + offset;
                size = sub_block_metadata_size - offset;
                return true;
            };
    }

    add_sub_block_info.sizeAttachment = sub_block_attachment_size;
    if (add_sub_block_info.sizeAttachment > 0)
    {
        add_sub_block_info.getAttachment =
            [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
            {
                // TODO: validate offset
                ptr = static_cast<const uint8_t*>(sub_block_attachment.get()) + offset;
                size = sub_block_attachment_size - offset;
                return true;
            };
    }

    reader_writer->ReplaceSubBlock(index, add_sub_block_info);
}

int main()
{
    std::cout << "Opening input/output stream" << std::endl;
    const auto io_stream = libCZI::CreateInputOutputStreamForFile(LR"(test2.czi)");

    // create the reader-writer-object
    auto reader_writer = libCZI::CreateCZIReaderWriter();


    // open the CZI-file
    reader_writer->Create(io_stream);

    // //take a peaky at the stats
    // auto statistics = reader_writer->GetStatistics();
    // statistics.dimBounds.EnumValidDimensions(
    //     [&](libCZI::DimensionIndex dim, int start, int size)->bool
    //     {
    //         std::cout << "DimensionIndex: " << static_cast<unsigned int>(dim)  << "Start: " << start << " Size=" << size << std::endl;
    //         return true;
    //     });

    std::map<int, double> minimumZoomPerScene;
    reader_writer->EnumerateSubBlocks(
        [&](int index, const SubBlockInfo& info) -> bool
    {
        libCZI::CDimCoordinate coord = info.coordinate;
        int sValue;
        if (coord.TryGetPosition(DimensionIndex::S, &sValue))
        {
            double zoom = info.GetZoom();
            // Check if sValue is already in minimumZoomPerScene and if zoom is smaller
            auto it = minimumZoomPerScene.find(sValue);
            if (it == minimumZoomPerScene.end() || it->second > zoom) // if not found or found with a larger zoom
            {
                minimumZoomPerScene[sValue] = zoom; // Update with the new smaller zoom
            }
        }
        return true;
    });

    double minimumCommonZoom = 0;
    for(const auto& pair : minimumZoomPerScene) {
        std::cout << "Scene: " << pair.first << ", Minimum Zoom: " << pair.second << std::endl;
        if(pair.second > minimumCommonZoom){
            minimumCommonZoom = pair.second;
        }
    }

    std::cout << "Minimum Common Zoom : " << minimumCommonZoom << std::endl;

    // I do an initial pass to 'chop the top off' the pyramids so that they all have the same number of levels.
    // If I don't do this, the 'odd' pyramid with less detail (the '1' in my sample file) does not appear in Omero.
    // This approach is very crude
    // a Slightly better approach would be to do an initial pass to establish the minimum zoom that is available on all the pyramids 
    // and then use that as a threshold.  in this example I have done it manually because I know my test file needs only one level chopped off.
    // Perhaps a more sophisticated approach would be to 'build up' the shorter pyramids by creating the upper levels based on the pixels of the ones below
    // but that would be more difficult.
    std::vector<int> indexesToRemove;
    reader_writer->EnumerateSubBlocks(
        [&](int index, const SubBlockInfo& info) ->bool
    {
        double zoom = info.GetZoom();
        if(zoom<minimumCommonZoom){ 
            std::cout << "Removing Index " << index << " Zoom " << zoom << ": " << libCZI::Utils::DimCoordinateToString(&info.coordinate) << " Rect=" << info.logicalRect << " PhysicalSize=" << info.physicalSize << std::endl;
            indexesToRemove.push_back(index); 
        }
        return true;
    });

    // I do the actual removal out of the EnumerateSubBlocks loop to avoid a segmentation fault
    for(int index : indexesToRemove) {
        reader_writer->RemoveSubBlock(index);
    }

    reader_writer->EnumerateSubBlocks(
        [&](int index, const SubBlockInfo& info) ->bool
    {
        RemoveSIndex(reader_writer, index, info);
        return true;
    });

    reader_writer->Close();

    return 0;
}

I ran czi check on a merged file and it is mostly green except for some document statistics...

./CZICheck -s test2_s0_mi_remtop.czi 
Test "check subblock's coordinates for 'consistent dimensions'" : OK
Test "SubBlock-Segment in SubBlockDirectory within file" : OK
Test "SubBlock-Segments in SubBlockDirectory are valid" : OK
Test "check subblock's coordinates being unique" : OK
Test "check whether the document uses the deprecated 'B-index'" : OK
Test "check that the subblocks of a channel have the same pixeltype" : OK
Test "Check that planes indices start at 0" : OK
Test "Check that planes have consecutive indices" : OK
Test "check if all subblocks have the M index" : OK
Test "Basic semantic checks of the XML-metadata" :DimensionIndex: 5StartStats: 0 StartMeta=0 EndStats=1EndMeta: 4

  For the following dimensions the start/size given in metadata differs from document statistics: S
 WARN
Test "check if subblocks at pyramid-layer 0 of different scenes are overlapping" : OK


Result: With Warnings

how would I fiddle with the S document Statistics? I think I need to use CSbBlkStatisticsUpdater but can't figure that out.

@ptahmose here is my 'final' version that addiitonally adjusts the document metadata so the merged file can pass CZICheck and opened in fiji..

#include "../libCZI/libCZI.h"
#include <iostream>
#include <vector>

using namespace std;
using namespace libCZI;

// I would call this instead 'MergeSIndex'
static void RemoveSIndex(const std::shared_ptr<ICziReaderWriter>& reader_writer, int index, const SubBlockInfo& info)
{
    AddSubBlockInfo add_sub_block_info;
    add_sub_block_info.Clear();
    add_sub_block_info.coordinate = info.coordinate;
    //add_sub_block_info.coordinate.Clear(DimensionIndex::S);
    add_sub_block_info.coordinate.Set(DimensionIndex::S,0); //rather than clear the S index, it seems to work better if I 'merge' all the scenes into one with S0

    // I have set the mIndex just using the enumeration index
    add_sub_block_info.mIndexValid = true;
    add_sub_block_info.mIndex = index;
    add_sub_block_info.x = info.logicalRect.x;
    add_sub_block_info.y = info.logicalRect.y;
    add_sub_block_info.logicalWidth = info.logicalRect.w;
    add_sub_block_info.logicalHeight = info.logicalRect.h;
    add_sub_block_info.physicalWidth = info.physicalSize.w;
    add_sub_block_info.physicalHeight = info.physicalSize.h;
    add_sub_block_info.PixelType = info.pixelType;
    add_sub_block_info.pyramid_type = info.pyramidType;

    // note: there is a bug in libCZI here (info.compressionModeRaw is not valid here, we work around
    //        this by using the compression mode of the sub-block which we have to read in any case)
    add_sub_block_info.compressionModeRaw = info.compressionModeRaw;

    auto subBlock = reader_writer->ReadSubBlock(index);
    size_t sub_block_data_size;
    auto sub_block_data = subBlock->GetRawData(ISubBlock::MemBlkType::Data, &sub_block_data_size);
    size_t sub_block_metadata_size;
    auto sub_block_metadata = subBlock->GetRawData(ISubBlock::MemBlkType::Metadata, &sub_block_metadata_size);
    size_t sub_block_attachment_size;
    auto sub_block_attachment = subBlock->GetRawData(ISubBlock::MemBlkType::Attachment, &sub_block_attachment_size);

    // workaround for above bug in libCZI
    add_sub_block_info.compressionModeRaw = subBlock->GetSubBlockInfo().compressionModeRaw;

    add_sub_block_info.sizeData = sub_block_data_size;
    add_sub_block_info.getData =
        [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
        {
            // TODO: validate that offset is is less than sub_block_data_size (and - offset should never be non-zero)
            ptr = static_cast<const uint8_t*>(sub_block_data.get()) + offset;
            size = sub_block_data_size - offset;
            return true;
        };

    add_sub_block_info.sizeMetadata = sub_block_metadata_size;
    if (add_sub_block_info.sizeMetadata > 0)
    {
        add_sub_block_info.getMetaData =
            [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
            {
                // TODO: validate offset
                ptr = static_cast<const uint8_t*>(sub_block_metadata.get()) + offset;
                size = sub_block_metadata_size - offset;
                return true;
            };
    }

    add_sub_block_info.sizeAttachment = sub_block_attachment_size;
    if (add_sub_block_info.sizeAttachment > 0)
    {
        add_sub_block_info.getAttachment =
            [&](int callCnt, size_t offset, const void*& ptr, size_t& size) -> bool
            {
                // TODO: validate offset
                ptr = static_cast<const uint8_t*>(sub_block_attachment.get()) + offset;
                size = sub_block_attachment_size - offset;
                return true;
            };
    }

    reader_writer->ReplaceSubBlock(index, add_sub_block_info);
}

int main()
{
    std::cout << "Opening input/output stream" << std::endl;
    const auto io_stream = libCZI::CreateInputOutputStreamForFile(LR"(test2.czi)");

    // create the reader-writer-object
    auto reader_writer = libCZI::CreateCZIReaderWriter();

    // open the CZI-file
    reader_writer->Create(io_stream);

    std::map<int, double> minimumZoomPerScene;
    reader_writer->EnumerateSubBlocks(
        [&](int index, const SubBlockInfo& info) -> bool
    {
        libCZI::CDimCoordinate coord = info.coordinate;
        int sValue;
        if (coord.TryGetPosition(DimensionIndex::S, &sValue))
        {
            double zoom = info.GetZoom();
            // Check if sValue is already in minimumZoomPerScene and if zoom is smaller
            auto it = minimumZoomPerScene.find(sValue);
            if (it == minimumZoomPerScene.end() || it->second > zoom) // if not found or found with a larger zoom
            {
                minimumZoomPerScene[sValue] = zoom; // Update with the new smaller zoom
            }
        }
        return true;
    });

    double minimumCommonZoom = 0;
    for(const auto& pair : minimumZoomPerScene) {
        std::cout << "Scene: " << pair.first << ", Minimum Zoom: " << pair.second << std::endl;
        if(pair.second > minimumCommonZoom){
            minimumCommonZoom = pair.second;
        }
    }

    std::cout << "Minimum Common Zoom : " << minimumCommonZoom << std::endl;

    // I do an initial pass to 'chop the top off' the pyramids so that they all have the same number of levels.
    // If I don't do this, the 'odd' pyramid with less detail (the '1' in my sample file) does not appear in Omero.
   
    // Perhaps a more sophisticated approach would be to 'build up' the shorter pyramids by creating the upper levels based on the pixels of the ones below
    // but that would be more difficult.
    std::vector<int> indexesToRemove;
    reader_writer->EnumerateSubBlocks(
        [&](int index, const SubBlockInfo& info) ->bool
    {
        double zoom = info.GetZoom();
        if(zoom<minimumCommonZoom){ 
            std::cout << "Removing Index " << index << " Zoom " << zoom << ": " << libCZI::Utils::DimCoordinateToString(&info.coordinate) << " Rect=" << info.logicalRect << " PhysicalSize=" << info.physicalSize << std::endl;
            indexesToRemove.push_back(index); 
        }
        return true;
    });

    // I do the actual removal out of the EnumerateSubBlocks loop to avoid a segmentation fault
    for(int index : indexesToRemove) {
        reader_writer->RemoveSubBlock(index);
    }

    reader_writer->EnumerateSubBlocks(
        [&](int index, const SubBlockInfo& info) ->bool
    {
        RemoveSIndex(reader_writer, index, info);
        return true;
    });


    // read the metadata-segment
    const auto metadata_segment = reader_writer->ReadMetadataSegment();

    // construct the metadata-object from the metadata-segment (use this to query information if required)
    const auto metadata = metadata_segment->CreateMetaFromMetadataSegment();

    // now, create a metadata-builder-object from the XML
    const auto metadata_builder = CreateMetadataBuilderFromXml(metadata->GetXml());

    // modify a node
    const auto comment_node = metadata_builder->GetRootNode()->GetOrCreateChildNode("Metadata/Information/Document/Comment");
    comment_node->SetValue("Scenes Merged");    //TODO - some useful information here about original scenes

    const auto size_s_node = metadata_builder->GetRootNode()->GetOrCreateChildNode("Metadata/Information/Image/SizeS");
    size_s_node->SetValue("1");

    // and now, write the modified metadata into the file
    WriteMetadataInfo metadata_info;
    metadata_info.Clear();
    const string source_metadata_xml = metadata_builder->GetXml();
    metadata_info.szMetadata = source_metadata_xml.c_str();
    metadata_info.szMetadataSize = source_metadata_xml.size();
    reader_writer->SyncWriteMetadata(metadata_info);

    reader_writer->Close();

    return 0;
}

@ptahmose I cleaned this up and opened a PR with my changes