diff --git a/CMakeLists.txt b/CMakeLists.txt index 56351dd..d48ab5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,11 +8,10 @@ set(CMAKE_CXX_STANDARD 17) set_property(GLOBAL PROPERTY USE_FOLDERS ON) project(IOOMEZarrNGFF CXX C ASM_NASM ASM) - set(IOOMEZarrNGFF_LIBRARIES IOOMEZarrNGFF) if(NOT ITK_SOURCE_DIR) - find_package(ITK 5.0 REQUIRED) + find_package(ITK 5.4 REQUIRED) list(APPEND CMAKE_MODULE_PATH ${ITK_CMAKE_DIR}) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) include(ITKModuleExternal) diff --git a/include/itkOMEZarrNGFFCommon.h b/include/itkOMEZarrNGFFCommon.h new file mode 100644 index 0000000..f70b7e1 --- /dev/null +++ b/include/itkOMEZarrNGFFCommon.h @@ -0,0 +1,134 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#ifndef itkOMEZarrNGFFCommon_h +#define itkOMEZarrNGFFCommon_h +#include "IOOMEZarrNGFFExport.h" + +#include + +#include "itkIOCommon.h" + +#include "tensorstore/container_kind.h" +#include "tensorstore/context.h" +#include "tensorstore/index_space/dim_expression.h" +#include "tensorstore/open.h" +#include "tensorstore/index_space/index_domain.h" +#include "tensorstore/index_space/index_domain_builder.h" +#include "tensorstore/index_space/dim_expression.h" + +#include + +namespace itk +{ +/** \class OMEZarrNGFFAxis + * + * \brief Represent an OME-Zarr NGFF axis + * + * Open Microscopy Environment Zarr Next Generation File Format + * specification can be found at https://github.com/ome/ngff + * + * \ingroup IOOMEZarrNGFF + */ +struct IOOMEZarrNGFF_EXPORT OMEZarrAxis +{ + std::string name; + std::string type; + std::string unit; +}; + +// Update an existing "read" specification for an "http" driver to retrieve remote files. +// Note that an "http" driver specification may operate on an HTTP or HTTPS connection. +void +makeKVStoreHTTPDriverSpec(nlohmann::json & spec, const std::string & fullPath); + +tensorstore::DataType +itkToTensorstoreComponentType(const IOComponentEnum itkComponentType); + +// Returns TensorStore KvStore driver name appropriate for this path. +// Options are file, zip. TODO: http, gcs (GoogleCouldStorage), etc. +std::string +getKVstoreDriver(std::string path); + +// JSON file path, e.g. "C:/Dev/ITKIOOMEZarrNGFF/v0.4/cyx.ome.zarr/.zgroup" +void +writeJson(nlohmann::json json, std::string path, std::string driver); + +// JSON file path, e.g. "C:/Dev/ITKIOOMEZarrNGFF/v0.4/cyx.ome.zarr/.zattrs" +bool +jsonRead(const std::string path, nlohmann::json & result, std::string driver); + +// We call tensorstoreToITKComponentType for each type. +// Hopefully compiler will optimize it away via constant propagation and inlining. +#define READ_ELEMENT_IF(typeName) \ + else if (tensorstoreToITKComponentType(tensorstore::dtype_v) == this->GetComponentType()) \ + { \ + ReadFromStore(store, storeIORegion, reinterpret_cast(buffer)); \ + } + +// We need to specify dtype for opening. As dtype is dependent on component type, this macro is long. +#define ELEMENT_WRITE(typeName) \ + else if (tensorstoreToITKComponentType(tensorstore::dtype_v) == this->GetComponentType()) \ + { \ + if (sizeof(typeName) == 1) \ + { \ + dtype = "|"; \ + } \ + if (std::numeric_limits::is_integer) \ + { \ + if (std::numeric_limits::is_signed) \ + { \ + dtype += 'i'; \ + } \ + else \ + { \ + dtype += 'u'; \ + } \ + } \ + else \ + { \ + dtype += 'f'; \ + } \ + dtype += std::to_string(sizeof(typeName)); \ + \ + auto openFuture = tensorstore::Open( \ + { \ + { "driver", "zarr" }, \ + { "kvstore", { { "driver", driver }, { "path", this->m_FileName + "/" + path } } }, \ + { "metadata", \ + { \ + { "compressor", { { "id", "blosc" } } }, \ + { "dtype", dtype }, \ + { "shape", shape }, \ + } }, \ + }, \ + tsContext, \ + tensorstore::OpenMode::create | tensorstore::OpenMode::delete_existing, \ + tensorstore::ReadWriteMode::read_write); \ + TS_EVAL_CHECK(openFuture); \ + \ + auto writeStore = openFuture.value(); \ + auto * p = reinterpret_cast(buffer); \ + auto arr = tensorstore::Array(p, shape, tensorstore::c_order); \ + auto writeFuture = tensorstore::Write(tensorstore::UnownedToShared(arr), writeStore); \ + TS_EVAL_CHECK(writeFuture); \ + } + +} // end namespace itk + +#endif // itkOMEZarrNGFFCommon_h diff --git a/include/itkOMEZarrNGFFTransformIO.h b/include/itkOMEZarrNGFFTransformIO.h new file mode 100644 index 0000000..b7351a7 --- /dev/null +++ b/include/itkOMEZarrNGFFTransformIO.h @@ -0,0 +1,133 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#ifndef itkOMEZarrNGFFImageIO_h +#define itkOMEZarrNGFFImageIO_h +#include "IOOMEZarrNGFFExport.h" + + +#include +#include +#include + +#include "itkImageIOBase.h" + +#include "itkOMEZarrNGFFCommon.h" + +namespace itk +{ + +/** \class OMEZarrNGFFTransformIO + * + * \brief Read and write OME-Zarr coordinate transformations. + * + * Open Microscopy Environment Zarr Next Generation File Format + * specification can be found at https://github.com/ome/ngff + * + * \ingroup IOFilters + * \ingroup IOOMEZarrNGFF + */ +template +class ITK_TEMPLATE_EXPORT OMEZarrNGFFTransformIOTemplate : public TransformIOBaseTemplate +{ +public: + /** Standard class typedefs. */ + using Self = OMEZarrNGFFTransformIOTemplate; + using Superclass = TransformIOBaseTemplate; + using Pointer = SmartPointer; + using typename Superclass::TransformListType; + using typename Superclass::TransformPointer; + using typename Superclass::TransformType; + using ParametersType = typename TransformType::ParametersType; + using ParametersValueType = typename TransformType::ParametersValueType; + using FixedParametersType = typename TransformType::FixedParametersType; + using FixedParametersValueType = typename TransformType::FixedParametersValueType; + + using ConstTransformListType = typename TransformIOBaseTemplate::ConstTransformListType; + + /** Method for creation through the object factory. */ + itkNewMacro(Self); + + /** Run-time type information (and related methods). */ + itkTypeMacro(OMEZarrNGFFTransformIOTemplate, TransformIOBaseTemplate); + + static constexpr unsigned MaximumDimension = 5; // OME-NGFF specifies up to 5D data + static constexpr int INVALID_INDEX = -1; // for specifying enumerated axis slice indices + using AxesCollectionType = std::vector; + + /** Special in-memory zip interface. An address needs to be provided in + * the "file name", using pattern address.memory, where address is a + * decimal representation of BufferInfo's address. + * Sample filename: "12341234.memory". */ + using BufferInfo = struct + { + char * pointer; + size_t size; + }; + + /** Construct a "magic" file name from the provided bufferInfo. */ + static std::string + MakeMemoryFileName(const BufferInfo & bufferInfo) + { + size_t bufferInfoAddress = reinterpret_cast(&bufferInfo); + return std::to_string(bufferInfoAddress) + ".memory"; + } + + /*-------- This part of the interfaces deals with reading data. ----- */ + + /** Reads the data from disk into the memory buffer provided. */ + void + Read() override; + + bool + CanReadFile(const char *) override; + + /*-------- This part of the interfaces deals with writing data. ----- */ + + /** Determine the file type. Returns true if this TransformIO can write the + * file specified. */ + bool + CanWriteFile(const char *) override; + + void + Write() override; + +protected: + OMEZarrNGFFTransformIOTemplate(); + ~OMEZarrNGFFTransformIOTemplate() override = default; + + void + PrintSelf(std::ostream & os, Indent indent) const override; + + /** Read a single array and set relevant metadata. */ + void + ReadArrayMetadata(std::string path, std::string driver); + +private: + // An empty zip file consists of 22 bytes of "end of central directory" record. More: + // https://github.com/google/tensorstore/blob/45565464b9f9e2567144d780c3bef365ee3c125a/tensorstore/internal/compression/zip_details.h#L64-L76 + constexpr static unsigned m_EmptyZipSize = 22; + char m_EmptyZip[m_EmptyZipSize] = "PK\x05\x06"; // the rest is filled with zeroes + const BufferInfo m_EmptyZipBufferInfo{ m_EmptyZip, m_EmptyZipSize }; + const std::string m_EmptyZipFileName = MakeMemoryFileName(m_EmptyZipBufferInfo); + + ITK_DISALLOW_COPY_AND_MOVE(OMEZarrNGFFTransformIOTemplate); +}; +} // end namespace itk + +#endif // itkOMEZarrNGFFImageIO_h diff --git a/include/itkOMEZarrNGFFTransformIOFactory.h b/include/itkOMEZarrNGFFTransformIOFactory.h new file mode 100644 index 0000000..aea769e --- /dev/null +++ b/include/itkOMEZarrNGFFTransformIOFactory.h @@ -0,0 +1,71 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#ifndef itkOMEZarrNGFFTransformIOFactory_h +#define itkOMEZarrNGFFTransformIOFactory_h +#include "IOOMEZarrNGFFExport.h" + +#include "itkObjectFactoryBase.h" +#include "itkTransformIOBase.h" + +namespace itk +{ +/** \class OMEZarrNGFFTransformIOFactory + * \brief Create instances of OMEZarrNGFFTransformIO objects using an object factory. + * \ingroup ITKIOOMEZarrNGFF + */ +class IOOMEZarrNGFF_EXPORT OMEZarrNGFFTransformIOFactory : public ObjectFactoryBase +{ +public: + /** Standard class typedefs. */ + using Self = OMEZarrNGFFTransformIOFactory; + using Superclass = ObjectFactoryBase; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + /** Class methods used to interface with the registered factories. */ + const char * + GetITKSourceVersion() const override; + + const char * + GetDescription() const override; + + /** Method for class instantiation. */ + itkFactorylessNewMacro(Self); + + /** Run-time type information (and related methods). */ + itkTypeMacro(OMEZarrNGFFTransformIOFactory, ObjectFactoryBase); + + /** Register one factory of this type */ + static void + RegisterOneFactory() + { + OMEZarrNGFFTransformIOFactory::Pointer zarrFactory = OMEZarrNGFFTransformIOFactory::New(); + + ObjectFactoryBase::RegisterFactoryInternal(zarrFactory); + } + +protected: + OMEZarrNGFFTransformIOFactory(); + ~OMEZarrNGFFTransformIOFactory() override = default; + +private: + ITK_DISALLOW_COPY_AND_MOVE(OMEZarrNGFFTransformIOFactory); +}; +} // end namespace itk + +#endif diff --git a/itk-module.cmake b/itk-module.cmake index 6cddb51..8e96833 100644 --- a/itk-module.cmake +++ b/itk-module.cmake @@ -11,6 +11,7 @@ file(READ "${MY_CURRENT_DIR}/README.md" DOCUMENTATION) itk_module(IOOMEZarrNGFF DEPENDS ITKIOImageBase + ITKIOTransformBase ITKIOHDF5 ITKZLIB TEST_DEPENDS @@ -18,6 +19,7 @@ itk_module(IOOMEZarrNGFF ITKMetaIO FACTORY_NAMES ImageIO::OMEZarrNGFF + TransformIO::OMEZarrNGFF DESCRIPTION "IO for images stored in zarr-backed OME-NGFF" EXCLUDE_FROM_DEFAULT diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b94f289..be1a14a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,9 @@ set(IOOMEZarrNGFF_SRCS + itkOMEZarrNGFFCommon.cxx itkOMEZarrNGFFImageIO.cxx itkOMEZarrNGFFImageIOFactory.cxx + itkOMEZarrNGFFTransformIO.cxx + itkOMEZarrNGFFTransformIOFactory.cxx ) diff --git a/src/itkOMEZarrNGFFCommon.cxx b/src/itkOMEZarrNGFFCommon.cxx new file mode 100644 index 0000000..dd6860b --- /dev/null +++ b/src/itkOMEZarrNGFFCommon.cxx @@ -0,0 +1,248 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#include "itkOMEZarrNGFFCommon.h" +#include "itkIOCommon.h" +#include "itkIntTypes.h" +#include "itkByteSwapper.h" +#include "itkMacro.h" + +#include "tensorstore/container_kind.h" +#include "tensorstore/context.h" +#include "tensorstore/index_space/dim_expression.h" +#include "tensorstore/open.h" +#include "tensorstore/index_space/index_domain.h" +#include "tensorstore/index_space/index_domain_builder.h" +#include "tensorstore/index_space/dim_expression.h" + +#include + +namespace itk +{ + +thread_local tensorstore::Context tsContext = tensorstore::Context::Default(); + +// Update an existing "read" specification for an "http" driver to retrieve remote files. +// Note that an "http" driver specification may operate on an HTTP or HTTPS connection. +void +makeKVStoreHTTPDriverSpec(nlohmann::json & spec, const std::string & fullPath) +{ + // Decompose path into a base URL and reference subpath according to TensorStore HTTP KVStore driver spec + // https://google.github.io/tensorstore/kvstore/http/index.html + spec["kvstore"] = { { "driver", "http" } }; + + // Naively decompose the URL into "base" and "resource" components. + // Generally assumes that the spec will only be used once to access a specific resource. + // For example, the URL "http://localhost/path/to/resource.json" will be split + // into components "http://localhost/path/to" and "resource.json". + // + // Could be revisited for a better root "base_url" at the top level allowing acces + // to multiple subpaths. For instance, decomposing the example above into + // "http://localhost/" and "path/to/resource.json" would allow for a given HTTP spec + // to be more easily reused with different subpaths. + // + spec["kvstore"]["base_url"] = fullPath.substr(0, fullPath.find_last_of("/")); + spec["kvstore"]["path"] = fullPath.substr(fullPath.find_last_of("/") + 1); +} + +IOComponentEnum +tensorstoreToITKComponentType(const tensorstore::DataType dtype) +{ + switch (dtype.id()) + { + case tensorstore::DataTypeId::char_t: + case tensorstore::DataTypeId::int8_t: + return IOComponentEnum::CHAR; + + case tensorstore::DataTypeId::byte_t: + case tensorstore::DataTypeId::uint8_t: + return IOComponentEnum::UCHAR; + + case tensorstore::DataTypeId::int16_t: + return IOComponentEnum::SHORT; + + case tensorstore::DataTypeId::uint16_t: + return IOComponentEnum::USHORT; + + case tensorstore::DataTypeId::int32_t: + return IOComponentEnum::INT; + + case tensorstore::DataTypeId::uint32_t: + return IOComponentEnum::UINT; + + case tensorstore::DataTypeId::int64_t: + return IOComponentEnum::LONGLONG; + + case tensorstore::DataTypeId::uint64_t: + return IOComponentEnum::ULONGLONG; + + case tensorstore::DataTypeId::float32_t: + return IOComponentEnum::FLOAT; + + case tensorstore::DataTypeId::float64_t: + return IOComponentEnum::DOUBLE; + + default: + return IOComponentEnum::UNKNOWNCOMPONENTTYPE; + } +} + +tensorstore::DataType +itkToTensorstoreComponentType(const IOComponentEnum itkComponentType) +{ + switch (itkComponentType) + { + case IOComponentEnum::UNKNOWNCOMPONENTTYPE: + return tensorstore::dtype_v; + + case IOComponentEnum::CHAR: + return tensorstore::dtype_v; + + case IOComponentEnum::UCHAR: + return tensorstore::dtype_v; + + case IOComponentEnum::SHORT: + return tensorstore::dtype_v; + + case IOComponentEnum::USHORT: + return tensorstore::dtype_v; + + // "long" is a silly type because it basically guaranteed not to be + // cross-platform across 32-vs-64 bit machines, but we can figure out + // a cross-platform way of storing the information. + case IOComponentEnum::LONG: + if (4 == sizeof(long)) + { + return tensorstore::dtype_v; + } + else + { + return tensorstore::dtype_v; + } + + case IOComponentEnum::ULONG: + if (4 == sizeof(long)) + { + return tensorstore::dtype_v; + } + else + { + return tensorstore::dtype_v; + } + + case IOComponentEnum::INT: + return tensorstore::dtype_v; + + case IOComponentEnum::UINT: + return tensorstore::dtype_v; + + case IOComponentEnum::LONGLONG: + return tensorstore::dtype_v; + + case IOComponentEnum::ULONGLONG: + return tensorstore::dtype_v; + + case IOComponentEnum::FLOAT: + return tensorstore::dtype_v; + + case IOComponentEnum::DOUBLE: + return tensorstore::dtype_v; + + case IOComponentEnum::LDOUBLE: + return tensorstore::dtype_v; + + default: + return tensorstore::dtype_v; + } +} + +// Returns TensorStore KvStore driver name appropriate for this path. +// Options are file, zip. TODO: http, gcs (GoogleCouldStorage), etc. +std::string +getKVstoreDriver(std::string path) +{ + if (path.size() < 4) + { + return "file"; + } + if (path.substr(0, 4) == "http") + { // http or https + return "http"; + } + if (path.substr(path.size() - 4) == ".zip" || path.substr(path.size() - 7) == ".memory") + { + return "zip_memory"; + } + return "file"; +} + +// JSON file path, e.g. "C:/Dev/ITKIOOMEZarrNGFF/v0.4/cyx.ome.zarr/.zgroup" +void +writeJson(nlohmann::json json, std::string path, std::string driver) +{ + auto attrs_store = tensorstore::Open( + { { "driver", "json" }, { "kvstore", { { "driver", driver }, { "path", path } } } }, + tsContext, + tensorstore::OpenMode::create | tensorstore::OpenMode::delete_existing, + tensorstore::ReadWriteMode::read_write) + .result() + .value(); + auto writeFuture = tensorstore::Write(tensorstore::MakeScalarArray(json), attrs_store); // preferably pretty-print + + auto result = writeFuture.result(); + if (!result.ok()) + { + itkGenericExceptionMacro(<< "There was an error writing metadata to file '" << path + << ". Error details: " << result.status()); + } +} + +// JSON file path, e.g. "C:/Dev/ITKIOOMEZarrNGFF/v0.4/cyx.ome.zarr/.zattrs" +bool +jsonRead(const std::string path, nlohmann::json & result, std::string driver) +{ + // Reading JSON via TensorStore allows it to be in the cloud + nlohmann::json readSpec = { { "driver", "json" }, { "kvstore", { { "driver", driver }, { "path", path } } } }; + if (driver == "http") + { + makeKVStoreHTTPDriverSpec(readSpec, path); + } + + auto attrs_store = tensorstore::Open(readSpec, tsContext).result().value(); + + auto attrs_array_result = tensorstore::Read(attrs_store).result(); + + nlohmann::json attrs; + if (attrs_array_result.ok()) + { + result = attrs_array_result.value()(); + return true; + } + else if (absl::IsNotFound(attrs_array_result.status())) + { + result = nlohmann::json::object_t(); + return false; + } + else // another error + { + result = nlohmann::json::object_t(); + return false; + } +} + +} // end namespace itk diff --git a/src/itkOMEZarrNGFFImageIO.cxx b/src/itkOMEZarrNGFFImageIO.cxx index 8dc9167..8f003f3 100644 --- a/src/itkOMEZarrNGFFImageIO.cxx +++ b/src/itkOMEZarrNGFFImageIO.cxx @@ -79,33 +79,9 @@ ReadFromStore(const tensorstore::TensorStore<> & store, const ImageIORegion & st } } -// Update an existing "read" specification for an "http" driver to retrieve remote files. -// Note that an "http" driver specification may operate on an HTTP or HTTPS connection. -void -MakeKVStoreHTTPDriverSpec(nlohmann::json & spec, const std::string & fullPath) -{ - // Decompose path into a base URL and reference subpath according to TensorStore HTTP KVStore driver spec - // https://google.github.io/tensorstore/kvstore/http/index.html - spec["kvstore"] = { { "driver", "http" } }; - - // Naively decompose the URL into "base" and "resource" components. - // Generally assumes that the spec will only be used once to access a specific resource. - // For example, the URL "http://localhost/path/to/resource.json" will be split - // into components "http://localhost/path/to" and "resource.json". - // - // Could be revisited for a better root "base_url" at the top level allowing acces - // to multiple subpaths. For instance, decomposing the example above into - // "http://localhost/" and "path/to/resource.json" would allow for a given HTTP spec - // to be more easily reused with different subpaths. - // - spec["kvstore"]["base_url"] = fullPath.substr(0, fullPath.find_last_of("/")); - spec["kvstore"]["path"] = fullPath.substr(fullPath.find_last_of("/") + 1); -} } // namespace -thread_local tensorstore::Context tsContext = tensorstore::Context::Default(); - OMEZarrNGFFImageIO::OMEZarrNGFFImageIO() { this->AddSupportedWriteExtension(".zarr"); @@ -130,194 +106,6 @@ void OMEZarrNGFFImageIO::PrintSelf(std::ostream & os, Indent indent) const { Superclass::PrintSelf(os, indent); - os << indent << "DatasetIndex: " << m_DatasetIndex << std::endl; - os << indent << "TimeIndex: " << m_TimeIndex << std::endl; - os << indent << "ChannelIndex: " << m_ChannelIndex << std::endl; -} - -IOComponentEnum -tensorstoreToITKComponentType(const tensorstore::DataType dtype) -{ - switch (dtype.id()) - { - case tensorstore::DataTypeId::char_t: - case tensorstore::DataTypeId::int8_t: - return IOComponentEnum::CHAR; - - case tensorstore::DataTypeId::byte_t: - case tensorstore::DataTypeId::uint8_t: - return IOComponentEnum::UCHAR; - - case tensorstore::DataTypeId::int16_t: - return IOComponentEnum::SHORT; - - case tensorstore::DataTypeId::uint16_t: - return IOComponentEnum::USHORT; - - case tensorstore::DataTypeId::int32_t: - return IOComponentEnum::INT; - - case tensorstore::DataTypeId::uint32_t: - return IOComponentEnum::UINT; - - case tensorstore::DataTypeId::int64_t: - return IOComponentEnum::LONGLONG; - - case tensorstore::DataTypeId::uint64_t: - return IOComponentEnum::ULONGLONG; - - case tensorstore::DataTypeId::float32_t: - return IOComponentEnum::FLOAT; - - case tensorstore::DataTypeId::float64_t: - return IOComponentEnum::DOUBLE; - - default: - return IOComponentEnum::UNKNOWNCOMPONENTTYPE; - } -} - -tensorstore::DataType -itkToTensorstoreComponentType(const IOComponentEnum itkComponentType) -{ - switch (itkComponentType) - { - case IOComponentEnum::UNKNOWNCOMPONENTTYPE: - return tensorstore::dtype_v; - - case IOComponentEnum::CHAR: - return tensorstore::dtype_v; - - case IOComponentEnum::UCHAR: - return tensorstore::dtype_v; - - case IOComponentEnum::SHORT: - return tensorstore::dtype_v; - - case IOComponentEnum::USHORT: - return tensorstore::dtype_v; - - // "long" is a silly type because it basically guaranteed not to be - // cross-platform across 32-vs-64 bit machines, but we can figure out - // a cross-platform way of storing the information. - case IOComponentEnum::LONG: - if (4 == sizeof(long)) - { - return tensorstore::dtype_v; - } - else - { - return tensorstore::dtype_v; - } - - case IOComponentEnum::ULONG: - if (4 == sizeof(long)) - { - return tensorstore::dtype_v; - } - else - { - return tensorstore::dtype_v; - } - - case IOComponentEnum::INT: - return tensorstore::dtype_v; - - case IOComponentEnum::UINT: - return tensorstore::dtype_v; - - case IOComponentEnum::LONGLONG: - return tensorstore::dtype_v; - - case IOComponentEnum::ULONGLONG: - return tensorstore::dtype_v; - - case IOComponentEnum::FLOAT: - return tensorstore::dtype_v; - - case IOComponentEnum::DOUBLE: - return tensorstore::dtype_v; - - case IOComponentEnum::LDOUBLE: - return tensorstore::dtype_v; - - default: - return tensorstore::dtype_v; - } -} - -// Returns TensorStore KvStore driver name appropriate for this path. -// Options are file, zip. TODO: http, gcs (GoogleCouldStorage), etc. -std::string -getKVstoreDriver(std::string path) -{ - if (path.size() < 4) - { - return "file"; - } - if (path.substr(0, 4) == "http") - { // http or https - return "http"; - } - if (path.substr(path.size() - 4) == ".zip" || path.substr(path.size() - 7) == ".memory") - { - return "zip_memory"; - } - return "file"; -} - -// JSON file path, e.g. "C:/Dev/ITKIOOMEZarrNGFF/v0.4/cyx.ome.zarr/.zgroup" -void -writeJson(nlohmann::json json, std::string path, std::string driver) -{ - auto attrs_store = tensorstore::Open( - { { "driver", "json" }, { "kvstore", { { "driver", driver }, { "path", path } } } }, - tsContext, - tensorstore::OpenMode::create | tensorstore::OpenMode::delete_existing, - tensorstore::ReadWriteMode::read_write) - .result() - .value(); - auto writeFuture = tensorstore::Write(tensorstore::MakeScalarArray(json), attrs_store); // preferably pretty-print - - auto result = writeFuture.result(); - if (!result.ok()) - { - itkGenericExceptionMacro(<< "There was an error writing metadata to file '" << path - << ". Error details: " << result.status()); - } -} - -// JSON file path, e.g. "C:/Dev/ITKIOOMEZarrNGFF/v0.4/cyx.ome.zarr/.zattrs" -bool -jsonRead(const std::string path, nlohmann::json & result, std::string driver) -{ - // Reading JSON via TensorStore allows it to be in the cloud - nlohmann::json readSpec = { { "driver", "json" }, { "kvstore", { { "driver", driver }, { "path", path } } } }; - if (driver == "http") - { - MakeKVStoreHTTPDriverSpec(readSpec, path); - } - - auto attrs_store = tensorstore::Open(readSpec, tsContext).result().value(); - - auto attrs_array_result = tensorstore::Read(attrs_store).result(); - - nlohmann::json attrs; - if (attrs_array_result.ok()) - { - result = attrs_array_result.value()(); - return true; - } - else if (absl::IsNotFound(attrs_array_result.status())) - { - result = nlohmann::json::object_t(); - return false; - } - else // another error - { - result = nlohmann::json::object_t(); - return false; - } } bool @@ -372,7 +160,7 @@ OMEZarrNGFFImageIO::ReadArrayMetadata(std::string path, std::string driver) nlohmann::json readSpec = { { "driver", "zarr" }, { "kvstore", { { "driver", driver }, { "path", path } } } }; if (driver == "http") { - MakeKVStoreHTTPDriverSpec(readSpec, path); + makeKVStoreHTTPDriverSpec(readSpec, path); } auto openFuture = tensorstore::Open(readSpec, @@ -554,7 +342,7 @@ OMEZarrNGFFImageIO::ReadImageInformation() auto targetIt = m_StoreAxes.rbegin(); for (const auto & axis : json.at("axes")) { - *targetIt = (OMEZarrNGFFAxis{ axis.at("name"), axis.at("type"), (axis.contains("unit") ? axis.at("unit") : "") }); + *targetIt = (OMEZarrAxis{ axis.at("name"), axis.at("type"), (axis.contains("unit") ? axis.at("unit") : "") }); ++targetIt; } itkAssertOrThrowMacro(targetIt == m_StoreAxes.rend(), @@ -601,14 +389,6 @@ OMEZarrNGFFImageIO::ReadImageInformation() ReadArrayMetadata(std::string(this->GetFileName()) + "/" + path, driver); } -// We call tensorstoreToITKComponentType for each type. -// Hopefully compiler will optimize it away via constant propagation and inlining. -#define READ_ELEMENT_IF(typeName) \ - else if (tensorstoreToITKComponentType(tensorstore::dtype_v) == this->GetComponentType()) \ - { \ - ReadFromStore(store, storeIORegion, reinterpret_cast(buffer)); \ - } - void OMEZarrNGFFImageIO::Read(void * buffer) { @@ -707,54 +487,6 @@ OMEZarrNGFFImageIO::WriteImageInformation() } -// We need to specify dtype for opening. As dtype is dependent on component type, this macro is long. -#define ELEMENT_WRITE(typeName) \ - else if (tensorstoreToITKComponentType(tensorstore::dtype_v) == this->GetComponentType()) \ - { \ - if (sizeof(typeName) == 1) \ - { \ - dtype = "|"; \ - } \ - if (std::numeric_limits::is_integer) \ - { \ - if (std::numeric_limits::is_signed) \ - { \ - dtype += 'i'; \ - } \ - else \ - { \ - dtype += 'u'; \ - } \ - } \ - else \ - { \ - dtype += 'f'; \ - } \ - dtype += std::to_string(sizeof(typeName)); \ - \ - auto openFuture = tensorstore::Open( \ - { \ - { "driver", "zarr" }, \ - { "kvstore", { { "driver", driver }, { "path", this->m_FileName + "/" + path } } }, \ - { "metadata", \ - { \ - { "compressor", { { "id", "blosc" } } }, \ - { "dtype", dtype }, \ - { "shape", shape }, \ - } }, \ - }, \ - tsContext, \ - tensorstore::OpenMode::create | tensorstore::OpenMode::delete_existing, \ - tensorstore::ReadWriteMode::read_write); \ - TS_EVAL_CHECK(openFuture); \ - \ - auto writeStore = openFuture.value(); \ - auto * p = reinterpret_cast(buffer); \ - auto arr = tensorstore::Array(p, shape, tensorstore::c_order); \ - auto writeFuture = tensorstore::Write(tensorstore::UnownedToShared(arr), writeStore); \ - TS_EVAL_CHECK(writeFuture); \ - } - void OMEZarrNGFFImageIO::Write(const void * buffer) { diff --git a/src/itkOMEZarrNGFFTransformIO.cxx b/src/itkOMEZarrNGFFTransformIO.cxx new file mode 100644 index 0000000..e2cdcbb --- /dev/null +++ b/src/itkOMEZarrNGFFTransformIO.cxx @@ -0,0 +1,143 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#define ITK_TEMPLATE_EXPLICIT_WasmTransformIO +#include "itkOMEZarrNGFFTransformIO.h" +#include "itkIOCommon.h" +#include "itkIntTypes.h" +#include "itkByteSwapper.h" +#include "itkMacro.h" + +#include "tensorstore/container_kind.h" +#include "tensorstore/context.h" +#include "tensorstore/index_space/dim_expression.h" +#include "tensorstore/open.h" +#include "tensorstore/index_space/index_domain.h" +#include "tensorstore/index_space/index_domain_builder.h" +#include "tensorstore/index_space/dim_expression.h" + +#include + +namespace itk +{ + +thread_local tensorstore::Context tsContext = tensorstore::Context::Default(); + +template +OMEZarrNGFFTransformIOTemplate::~OMEZarrNGFFTransformIOTemplate() = default; + +template +OMEZarrNGFFTransformIOTemplate::OMEZarrNGFFTransformIO() +{ + this->AddSupportedWriteExtension(".zarr"); + this->AddSupportedWriteExtension(".zr2"); + this->AddSupportedWriteExtension(".zr3"); + this->AddSupportedWriteExtension(".zip"); + this->AddSupportedWriteExtension(".memory"); + + this->AddSupportedReadExtension(".zarr"); + this->AddSupportedReadExtension(".zr2"); + this->AddSupportedReadExtension(".zr3"); + this->AddSupportedReadExtension(".zip"); + this->AddSupportedWriteExtension(".memory"); + + this->Self::SetCompressor(""); + this->Self::SetMaximumCompressionLevel(9); + this->Self::SetCompressionLevel(2); +} + + +template +void +OMEZarrNGFFTransformIOTemplate +::PrintSelf(std::ostream & os, Indent indent) const +{ + Superclass::PrintSelf(os, indent); +} + +template +void +bool +OMEZarrNGFFTransformIO::CanReadFile(const char * filename) +{ + try + { + std::string driver = getKVstoreDriver(filename); + nlohmann::json json; + if (!jsonRead(std::string(filename) + "/.zgroup", json, driver)) + { + return false; + } + if (json.at("zarr_format").get() != 2) + { + return false; // unsupported zarr format + } + if (!jsonRead(std::string(filename) + "/.zattrs", json, driver)) + { + return false; + } + if (!json.at("multiscales").is_array()) + { + return false; // multiscales attribute array must be present + } + return true; + } + catch (...) + { + return false; + } + // return this->HasSupportedWriteExtension(filename, true); +} + +// Evaluate tensorstore future (statement) and error-check the result. +#define TS_EVAL_CHECK(statement) \ + { \ + auto result = statement.result(); \ + if (!result.ok()) /* error */ \ + { \ + itkExceptionMacro("tensorstore error: " << result.status()); \ + } \ + } \ + ITK_NOOP_STATEMENT + + +thread_local tensorstore::TensorStore<> store; // initialized by ReadTransformInformation/ReadArrayMetadata + +thread_local std::string path; // initialized by ReadTransformInformation + +template +void +OMEZarrNGFFTransformIOTemplate::Read() +{ +} + + +template +bool +OMEZarrNGFFTransformIOTemplate::CanWriteFile(const char * name) +{ +} + +template +void +OMEZarrNGFFTransformIOTemplate::Write() +{ +} + + +} // end namespace itk diff --git a/src/itkOMEZarrNGFFTransformIOFactory.cxx b/src/itkOMEZarrNGFFTransformIOFactory.cxx new file mode 100644 index 0000000..dc11db2 --- /dev/null +++ b/src/itkOMEZarrNGFFTransformIOFactory.cxx @@ -0,0 +1,66 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#include "itkOMEZarrNGFFTransformIOFactory.h" +#include "itkOMEZarrNGFFTransformIO.h" +#include "itkVersion.h" + +namespace itk +{ +OMEZarrNGFFTransformIOFactory::OMEZarrNGFFTransformIOFactory() +{ + this->RegisterOverride("itkTransformIOBaseTemplate", + "itkOMEZarrNGFFTransformIOTemplate", + "OMEZarrNGFF Transform float IO", + true, + CreateObjectFunction>::New()); + + this->RegisterOverride("itkTransformIOBaseTemplate", + "itkOMEZarrNGFFTransformIOTemplate", + "OMEZarrNGFF Transform double IO", + true, + CreateObjectFunction>::New()); +} + +const char * +OMEZarrNGFFTransformIOFactory::GetITKSourceVersion() const +{ + return ITK_SOURCE_VERSION; +} + +const char * +OMEZarrNGFFTransformIOFactory::GetDescription() const +{ + return "OMEZarrNGFF TransformIO Factory, allows the loading of images in Open Microscopy Environment Zarr Next Generation File Format"; +} + +// Undocumented API used to register during static initialization. +// DO NOT CALL DIRECTLY. + +static bool OMEZarrNGFFTransformIOFactoryHasBeenRegistered; + +void IOOMEZarrNGFF_EXPORT +OMEZarrNGFFTransformIOFactoryRegister__Private() +{ + if (!OMEZarrNGFFTransformIOFactoryHasBeenRegistered) + { + OMEZarrNGFFTransformIOFactoryHasBeenRegistered = true; + OMEZarrNGFFTransformIOFactory::RegisterOneFactory(); + } +} + +} // end namespace itk diff --git a/wrapping/CMakeLists.txt b/wrapping/CMakeLists.txt index b3eaae0..750e02b 100644 --- a/wrapping/CMakeLists.txt +++ b/wrapping/CMakeLists.txt @@ -1,3 +1,6 @@ itk_wrap_module(IOOMEZarrNGFF) +set(WRAPPER_SUBMODULE_ORDER + itkOMEZarrNGFFCommon +) itk_auto_load_submodules() itk_end_wrap_module() diff --git a/wrapping/itkOMEZarrNGFFCommon.wrap b/wrapping/itkOMEZarrNGFFCommon.wrap new file mode 100644 index 0000000..fd35ac9 --- /dev/null +++ b/wrapping/itkOMEZarrNGFFCommon.wrap @@ -0,0 +1,4 @@ +set(WRAPPER_AUTO_INCLUDE_HEADERS OFF) +itk_wrap_include("itkOMEZarrNGFFCommon.h") +itk_wrap_simple_class("itk::OMEZarrAxis") +set(WRAPPER_AUTO_INCLUDE_HEADERS ON) \ No newline at end of file