Skip to content

Commit

Permalink
JPEGXL support (#213)
Browse files Browse the repository at this point in the history
* Implemented JPEGXL support in ImageExporter and ImageImporter.
* Implemented JPEGXL support in TIFF based ImagePyramids
  • Loading branch information
smistad authored Nov 21, 2024
1 parent 2c8f7e1 commit ac554f7
Show file tree
Hide file tree
Showing 21 changed files with 532 additions and 122 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/CI-mac-arm64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ jobs:
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --target package -j 4

- name: Upload archive package
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Archive package (tar.xz)
path: ${{github.workspace}}/build/fast_*.tar.xz
if-no-files-found: error

- name: Upload Python wheel
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Python wheel
path: ${{github.workspace}}/build/python/dist/pyFAST-*.whl
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/CI-mac-x86_64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@ jobs:
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --target package -j 4

- name: Upload archive package
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Archive package (tar.xz)
path: ${{github.workspace}}/build/fast_*.tar.xz
if-no-files-found: error

- name: Upload Python wheel
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Python wheel
path: ${{github.workspace}}/build/python/dist/pyFAST-*.whl
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/CI-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,21 +168,21 @@ jobs:
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --target package -j 4

- name: Upload Windows installer
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Window installer
path: ${{github.workspace}}/build/fast_*.exe
if-no-files-found: error

- name: Upload archive package
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Archive package (zip)
path: ${{github.workspace}}/build/fast_*.zip
if-no-files-found: error

- name: Upload Python wheel
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Python wheel
path: ${{github.workspace}}/build/python/dist/pyFAST-*.whl
Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ endif()
project(FAST)

set(VERSION_MAJOR 4)
set(VERSION_MINOR 9)
set(VERSION_PATCH 3)
set(VERSION_MINOR 10)
set(VERSION_PATCH 0)
set(VERSION_SO 4) # SO version, should be incremented by 1 every time the existing API changes
set(FAST_VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")

Expand Down
3 changes: 2 additions & 1 deletion cmake/Depdendencies.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,12 @@ if(FAST_MODULE_Visualization)
add_definitions("-DFAST_MODULE_VISUALIZATION")
endif()

## External depedencies
## External dependencies
include(cmake/ExternalEigen.cmake)
add_definitions("-DEIGEN_MPL2_ONLY") # Avoid using LGPL code in eigen http://eigen.tuxfamily.org/index.php?title=Main_Page#License
include(cmake/ExternalZlib.cmake)
include(cmake/ExternalZip.cmake)
include(cmake/ImageCompressionDependencies.cmake)

# Optional modules
include(cmake/ModuleVTK.cmake)
Expand Down
28 changes: 28 additions & 0 deletions cmake/ImageCompressionDependencies.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Download dependencies required for various image compression
if(WIN32)
fast_download_dependency(jpegxl
0.11.0
51aec03201152d99ca9c6a561b3c28e0e1c1043d9488c9bd7b590b03412347e0
jxl.lib jxl_threads.lib
)
elseif(APPLE)
if(CMAKE_OSX_ARCHITECTURES STREQUAL "arm64")
fast_download_dependency(jpegxl
0.11.0
dc05ced17948ed02e2c646e9480758ed439aa061d70c9d820df3e3239ea1dadd
jxl.dylib jxl_threads.dylib
)
else()
fast_download_dependency(jpegxl
0.11.0
f743d0fb5cdb6b8d63028d14a633c5ae250dbeca8b4577ad9c55b98a024035b3
jxl.dylib jxl_threads.dylib
)
endif()
else()
fast_download_dependency(jpegxl
0.11.0
a9bdb10ceddceb57892e9f6526cd8f62ad4a469e9f6941a687f8af6eb425b172
libjxl.so libjxl_threads.so
)
endif()
10 changes: 10 additions & 0 deletions source/FAST/Algorithms/Compression/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
if(FAST_MODULE_WholeSlideImaging)
fast_add_sources(
#JPEGCompression.cpp
#JPEGCompression.hpp
#JPEG2000Compression.cpp
#JPEG2000Compression.hpp
JPEGXLCompression.cpp
JPEGXLCompression.hpp
)
endif()
158 changes: 158 additions & 0 deletions source/FAST/Algorithms/Compression/JPEGXLCompression.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#include "JPEGXLCompression.hpp"
#include <jxl/codestream_header.h>
#include <jxl/color_encoding.h>
#include <jxl/encode.h>
#include <jxl/encode_cxx.h>
#include <jxl/thread_parallel_runner.h>
#include <jxl/thread_parallel_runner_cxx.h>
#include <jxl/types.h>
#include <jxl/decode.h>
#include <jxl/decode_cxx.h>
#include <jxl/resizable_parallel_runner.h>
#include <jxl/resizable_parallel_runner_cxx.h>

namespace fast {

JPEGXLCompression::JPEGXLCompression() {

}

void* JPEGXLCompression::decompress(uchar *compressedData, std::size_t bytes, int* widthOut, int* heightOut, uchar* decompressedData) {
auto runner = JxlResizableParallelRunnerMake(nullptr);

auto decoder = JxlDecoderMake(nullptr);
if(JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(decoder.get(),
JXL_DEC_BASIC_INFO |
JXL_DEC_COLOR_ENCODING |
JXL_DEC_FULL_IMAGE))
throw Exception("JPEGXL: JxlDecoderSubscribeEvents failed");

if(JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(decoder.get(),
JxlResizableParallelRunner,
runner.get()))
throw Exception( "JPEGXL: JxlDecoderSetParallelRunner failed");

uint channels = 3;
JxlBasicInfo info;
JxlPixelFormat format = {channels, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0};

std::vector<uint8_t> ICCProfile;
size_t width = 0;
size_t height = 0;
JxlDecoderSetInput(decoder.get(), compressedData, bytes);
JxlDecoderCloseInput(decoder.get());

for (;;) {
JxlDecoderStatus status = JxlDecoderProcessInput(decoder.get());

if(status == JXL_DEC_ERROR) {
throw Exception( "JPEGXL: Decoder error");
} else if(status == JXL_DEC_NEED_MORE_INPUT) {
throw Exception( "JPEGXL: Already provided all input\n");
} else if(status == JXL_DEC_BASIC_INFO) {
if(JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(decoder.get(), &info))
throw Exception( "JPEGXL: JxlDecoderGetBasicInfo failed");
width = info.xsize;
height = info.ysize;
JxlResizableParallelRunnerSetThreads(
runner.get(),
JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize)
);
} else if(status == JXL_DEC_COLOR_ENCODING) {
size_t ICCSize;
if(JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize(
decoder.get(),
JXL_COLOR_PROFILE_TARGET_DATA,
&ICCSize))
throw Exception("JPEGXL: JxlDecoderGetICCProfileSize failed");

ICCProfile.resize(ICCSize);
if(JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile(
decoder.get(), JXL_COLOR_PROFILE_TARGET_DATA,
ICCProfile.data(), ICCProfile.size()))
throw Exception("JPEGXL: JxlDecoderGetColorAsICCProfile failed");
} else if(status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
size_t bufferSize;
if(JXL_DEC_SUCCESS != JxlDecoderImageOutBufferSize(decoder.get(), &format, &bufferSize))
throw Exception("JPEGXL: JxlDecoderImageOutBufferSize failed\n");
if(decompressedData == nullptr)
decompressedData = new uchar[width * height * channels];
if(bufferSize != width * height * channels)
throw Exception("JPEGXL: Invalid out buffer size " + std::to_string(bufferSize) + " " +
std::to_string(width * height * channels));
size_t pixelBufferSize = width * height * channels * sizeof(uchar);
if(JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(decoder.get(), &format,
decompressedData,
pixelBufferSize)) {
throw Exception("JPEGXL: JxlDecoderSetImageOutBuffer failed");
}
*widthOut = width;
*heightOut = height;
} else if (status == JXL_DEC_FULL_IMAGE) {
// Nothing to do. If the image is an animation, more frames may be decoded.
} else if (status == JXL_DEC_SUCCESS) {
// All decoding finished
return decompressedData;
} else {
throw Exception("JPEGXL: Unknown decoder status");
}
}
}

void JPEGXLCompression::compress(void *data, int width, int height, std::vector<uchar>* compressed) {
auto encoder = JxlEncoderMake(nullptr);
auto runner = JxlThreadParallelRunnerMake(
nullptr,
JxlThreadParallelRunnerDefaultNumWorkerThreads()
);
if(JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(encoder.get(),
JxlThreadParallelRunner,
runner.get()))
throw Exception("JPEGXL: JxlEncoderSetParallelRunner failed");

JxlPixelFormat pixelFormat = {3, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0};

JxlBasicInfo basicInfo;
JxlEncoderInitBasicInfo(&basicInfo);
basicInfo.xsize = width;
basicInfo.ysize = height;
int channels = 3;
basicInfo.bits_per_sample = 32;
basicInfo.exponent_bits_per_sample = 8;
basicInfo.uses_original_profile = JXL_FALSE;
if(JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(encoder.get(), &basicInfo))
throw Exception("JPEGXL: JxlEncoderSetBasicInfo failed");

JxlColorEncoding colorEncoding = {};
JXL_BOOL isGrayscale = TO_JXL_BOOL(pixelFormat.num_channels < 3);
JxlColorEncodingSetToSRGB(&colorEncoding, isGrayscale);
if(JXL_ENC_SUCCESS != JxlEncoderSetColorEncoding(encoder.get(), &colorEncoding))
throw Exception("JPEGXL: JxlEncoderSetColorEncoding failed");

auto frameSettings = JxlEncoderFrameSettingsCreate(encoder.get(), nullptr);

if(JXL_ENC_SUCCESS != JxlEncoderAddImageFrame(frameSettings, &pixelFormat,
static_cast<const void*>(data),
sizeof(uchar) * width*height*channels))
throw Exception("JPEGXL: JxlEncoderAddImageFrame failed");
JxlEncoderCloseInput(encoder.get());

compressed->resize(64);
uint8_t* next_out = compressed->data();
size_t avail_out = compressed->size() - (next_out - compressed->data());
JxlEncoderStatus processResult = JXL_ENC_NEED_MORE_OUTPUT;
while(processResult == JXL_ENC_NEED_MORE_OUTPUT) {
processResult = JxlEncoderProcessOutput(encoder.get(), &next_out, &avail_out);
if(processResult == JXL_ENC_NEED_MORE_OUTPUT) {
size_t offset = next_out - compressed->data();
compressed->resize(compressed->size() * 2);
next_out = compressed->data() + offset;
avail_out = compressed->size() - offset;
}
}
compressed->resize(next_out - compressed->data());
if(JXL_ENC_SUCCESS != processResult)
throw Exception( "JPEGXL: JxlEncoderProcessOutput failed");
}

}
28 changes: 28 additions & 0 deletions source/FAST/Algorithms/Compression/JPEGXLCompression.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#include <FAST/Data/DataTypes.hpp>

namespace fast {

/**
* @brief Class for JPEG XL image compression
*
* Only supports 8 bit RGB images for now.
*/
class JPEGXLCompression {
public:
JPEGXLCompression();
void compress(void* data, int width, int height, std::vector<uint8_t>* compressedData);
/**
* @brief Decompress
* @param compressedData
* @param bytes
* @param widthOut
* @param heightOut
* @param outputBuffer if nullptr this buffer will be used to store data
* @return decompressed data
*/
void* decompress(uchar* compressedData, std::size_t bytes, int* widthOut, int* heightOut, uchar* outputBuffer = nullptr);

};
}
35 changes: 35 additions & 0 deletions source/FAST/Data/Access/ImagePyramidAccess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <FAST/Algorithms/NeuralNetwork/TensorToImage.hpp>
#include <FAST/Algorithms/ImageCaster/ImageCaster.hpp>
#include <FAST/Algorithms/ImageResizer/ImageResizer.hpp>
#include <FAST/Algorithms/Compression/JPEGXLCompression.hpp>

namespace fast {

Expand Down Expand Up @@ -320,6 +321,8 @@ uint32_t ImagePyramidAccess::writeTileToTIFF(int level, int x, int y, uchar *dat
if(m_image->getCompression() == ImageCompression::NEURAL_NETWORK) {
auto image = Image::create(width, height, TYPE_UINT8, channels, data); // TODO this seems unnecessary
return writeTileToTIFF(level, x, y, image);
} else if(m_image->getCompression() == ImageCompression::JPEGXL) {
return writeTileToTIFFJPEGXL(level, x, y, data);
} else {
return writeTileToTIFF(level, x, y, data);
}
Expand All @@ -328,6 +331,9 @@ uint32_t ImagePyramidAccess::writeTileToTIFF(int level, int x, int y, uchar *dat
uint32_t ImagePyramidAccess::writeTileToTIFF(int level, int x, int y, Image::pointer image) {
if(m_image->getCompression() == ImageCompression::NEURAL_NETWORK) {
return writeTileToTIFFNeuralNetwork(level, x, y, image);
} else if(m_image->getCompression() == ImageCompression::JPEGXL) {
auto access = image->getImageAccess(ACCESS_READ);
return writeTileToTIFFJPEGXL(level, x, y, (uchar*)access->get());
} else {
auto access = image->getImageAccess(ACCESS_READ);
return writeTileToTIFF(level, x, y, (uchar*)access->get());
Expand All @@ -343,6 +349,19 @@ uint32_t ImagePyramidAccess::writeTileToTIFF(int level, int x, int y, uchar *dat
return tile_id;
}

uint32_t ImagePyramidAccess::writeTileToTIFFJPEGXL(int level, int x, int y, uchar *data) {
std::lock_guard<std::mutex> lock(m_readMutex);
setTIFFDirectory(level);
uint32_t tile_id = TIFFComputeTile(m_tiffHandle, x, y, 0, 0);
JPEGXLCompression jxl;
std::vector<uchar> compressed;
jxl.compress(data, m_image->getLevelTileWidth(level), m_image->getLevelTileHeight(level), &compressed);
TIFFSetWriteOffset(m_tiffHandle, 0); // Set write offset to 0, so that we dont appen data
TIFFWriteRawTile(m_tiffHandle, tile_id, (void *) compressed.data(), compressed.size()); // This appends data..
TIFFCheckpointDirectory(m_tiffHandle);
return tile_id;
}

uint32_t ImagePyramidAccess::writeTileToTIFFNeuralNetwork(int level, int x, int y, Image::pointer image) {
std::lock_guard<std::mutex> lock(m_readMutex);
TIFFSetDirectory(m_tiffHandle, level);
Expand Down Expand Up @@ -562,12 +581,28 @@ int ImagePyramidAccess::readTileFromTIFF(void *data, int x, int y, int level) {
jpeg_destroy_decompress( &cinfo );
throw Exception("JPEG error: " + std::string(e.what())); // or return an error code
}
} else if(m_compressionFormat == ImageCompression::JPEGXL) {
auto tileWidth = m_image->getLevelTileWidth(level);
auto tileHeight = m_image->getLevelTileHeight(level);
const auto channels = m_image->getNrOfChannels();
auto buffer = make_uninitialized_unique<char[]>(tileWidth*tileHeight*channels);
uint32_t tile_id = TIFFComputeTile(m_tiffHandle, x, y, 0, 0);
bytesRead = TIFFReadRawTile(m_tiffHandle, tile_id, buffer.get(), tileWidth*tileHeight*channels);

JPEGXLCompression jxl;
int width, height;
jxl.decompress((uchar*)buffer.get(), bytesRead, &width, &height, (uchar*)data);
} else {
bytesRead = TIFFReadTile(m_tiffHandle, data, x, y, 0, 0);
}
return bytesRead;
}
}

void ImagePyramidAccess::setTIFFDirectory(int level) {
if(TIFFCurrentDirectory(m_tiffHandle) != level)
TIFFSetDirectory(m_tiffHandle, level);
}


}
Loading

0 comments on commit ac554f7

Please sign in to comment.