Skip to content

Commit

Permalink
Implemented proper quality parameter JPEG support in TIFF image pyramids
Browse files Browse the repository at this point in the history
  • Loading branch information
smistad committed Dec 13, 2024
1 parent 6b16870 commit 330d716
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 48 deletions.
6 changes: 2 additions & 4 deletions source/FAST/Algorithms/Compression/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
if(FAST_MODULE_WholeSlideImaging)
fast_add_sources(
#JPEGCompression.cpp
#JPEGCompression.hpp
#JPEG2000Compression.cpp
#JPEG2000Compression.hpp
JPEGCompression.cpp
JPEGCompression.hpp
JPEGXLCompression.cpp
JPEGXLCompression.hpp
)
Expand Down
90 changes: 90 additions & 0 deletions source/FAST/Algorithms/Compression/JPEGCompression.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include "JPEGCompression.hpp"
#include <jpeglib.h>

namespace fast {


inline void jpegErrorExit(j_common_ptr cinfo) {
char jpegLastErrorMsg[JMSG_LENGTH_MAX];
// Create message
( *( cinfo->err->format_message ) ) ( cinfo, jpegLastErrorMsg );
throw std::runtime_error( jpegLastErrorMsg );
}

void* JPEGCompression::decompress(uchar* compressedData, std::size_t bytes, int* widthOut, int* heightOut, uchar* outputBuffer) {
jpeg_decompress_struct cinfo;
jpeg_error_mgr jerr; //error handling
//jerr.error_exit = jpegErrorExit;
cinfo.err = jpeg_std_error(&jerr);
try {
jpeg_create_decompress(&cinfo);
jpeg_mem_src(&cinfo, compressedData, bytes);
int ret = jpeg_read_header(&cinfo, TRUE);
if(ret != JPEG_HEADER_OK) {
throw Exception("Unable to read JPEG header.");
}
jpeg_start_decompress(&cinfo); // output_width and output_height is available after this call

*widthOut = cinfo.output_width;
*heightOut = cinfo.output_height;
//cinfo.jpeg_color_space = JCS_YCbCr;
//cinfo.jpeg_color_space = JCS_RGB;
if(outputBuffer == nullptr)
outputBuffer = new uchar[(*widthOut) * (*heightOut) * 3];
unsigned char* line = (uchar*)outputBuffer;
while(cinfo.output_scanline < cinfo.output_height) {
jpeg_read_scanlines (&cinfo, &line, 1);
line += cinfo.output_components*cinfo.output_width;
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
} catch(std::exception &e) {
jpeg_destroy_decompress( &cinfo );
throw Exception("JPEG error: " + std::string(e.what())); // or return an error code
}
return outputBuffer;
}

void JPEGCompression::compress(void *data, int width, int height, std::vector<uint8_t> *compressedData, int quality) {
jpeg_compress_struct cinfo;
jpeg_error_mgr jerr;

cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);

// Set up memory
unsigned long resultSize = 0;
uchar* resultBuffer = nullptr;
jpeg_mem_dest(&cinfo, &resultBuffer, &resultSize);

// Set parameters
cinfo.image_width = width;
cinfo.image_height = height;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
// Use 4:4:4 subsampling, default is 4:2:0
//cinfo.comp_info[0].h_samp_factor = cinfo.comp_info[0].v_samp_factor = 1;

jpeg_start_compress(&cinfo, TRUE);

auto line = (uchar*)data;
while(cinfo.next_scanline < cinfo.image_height) {
jpeg_write_scanlines(&cinfo, &line, 1);
line += cinfo.image_width*cinfo.input_components;
}
jpeg_finish_compress(&cinfo);

// Copy buffer after jpeg_finish_compress
// TODO can we avoid this copy?
compressedData->resize(resultSize);
std::copy(resultBuffer, resultBuffer + resultSize, compressedData->begin());
free(resultBuffer);
jpeg_destroy_compress(&cinfo);
}
JPEGCompression::JPEGCompression() {

}

} // End namespace
28 changes: 28 additions & 0 deletions source/FAST/Algorithms/Compression/JPEGCompression.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 image compression
*
* Only supports 8 bit RGB images for now.
*/
class JPEGCompression {
public:
JPEGCompression();
void compress(void* data, int width, int height, std::vector<uint8_t>* compressedData, int quality = 90);
/**
* @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);

};
}
57 changes: 24 additions & 33 deletions source/FAST/Data/Access/ImagePyramidAccess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
#else
#include <openslide/openslide.h>
#endif
#include <jpeglib.h>
#include <tiffio.h>
#include <FAST/Data/Image.hpp>
#include <FAST/Algorithms/NeuralNetwork/NeuralNetwork.hpp>
#include <FAST/Algorithms/NeuralNetwork/TensorToImage.hpp>
#include <FAST/Algorithms/ImageCaster/ImageCaster.hpp>
#include <FAST/Algorithms/ImageResizer/ImageResizer.hpp>
#include <FAST/Algorithms/Compression/JPEGXLCompression.hpp>
#include <FAST/Algorithms/Compression/JPEGCompression.hpp>

namespace fast {

Expand Down Expand Up @@ -321,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::JPEG) {
return writeTileToTIFFJPEG(level, x, y, data);
} else if(m_image->getCompression() == ImageCompression::JPEGXL) {
return writeTileToTIFFJPEGXL(level, x, y, data);
} else {
Expand All @@ -334,6 +336,9 @@ uint32_t ImagePyramidAccess::writeTileToTIFF(int level, int x, int y, Image::poi
} else if(m_image->getCompression() == ImageCompression::JPEGXL) {
auto access = image->getImageAccess(ACCESS_READ);
return writeTileToTIFFJPEGXL(level, x, y, (uchar*)access->get());
} else if(m_image->getCompression() == ImageCompression::JPEG) {
auto access = image->getImageAccess(ACCESS_READ);
return writeTileToTIFFJPEG(level, x, y, (uchar*)access->get());
} else {
auto access = image->getImageAccess(ACCESS_READ);
return writeTileToTIFF(level, x, y, (uchar*)access->get());
Expand Down Expand Up @@ -362,6 +367,20 @@ uint32_t ImagePyramidAccess::writeTileToTIFFJPEGXL(int level, int x, int y, ucha
return tile_id;
}


uint32_t ImagePyramidAccess::writeTileToTIFFJPEG(int level, int x, int y, uchar *data) {
std::lock_guard<std::mutex> lock(m_readMutex);
TIFFSetDirectory(m_tiffHandle, level);
uint32_t tile_id = TIFFComputeTile(m_tiffHandle, x, y, 0, 0);
JPEGCompression jpeg;
std::vector<uchar> compressed;
jpeg.compress(data, m_image->getLevelTileWidth(level), m_image->getLevelTileHeight(level), &compressed, m_image->getCompressionQuality());
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 @@ -517,13 +536,6 @@ bool ImagePyramidAccess::isPatchInitialized(int level, int x, int y) {
return m_initializedPatchList.count(std::to_string(level) + "-" + std::to_string(tile)) > 0;
}

void jpegErrorExit(j_common_ptr cinfo) {
char jpegLastErrorMsg[JMSG_LENGTH_MAX];
// Create message
( *( cinfo->err->format_message ) ) ( cinfo, jpegLastErrorMsg );
throw std::runtime_error( jpegLastErrorMsg );
}

int ImagePyramidAccess::readTileFromTIFF(void *data, int x, int y, int level) {
const auto tileWidth = m_image->getLevelTileWidth(level);
const auto tileHeight = m_image->getLevelTileHeight(level);
Expand Down Expand Up @@ -561,35 +573,14 @@ int ImagePyramidAccess::readTileFromTIFF(void *data, int x, int y, int level) {
return bytesRead;
} else {
int bytesRead = 0;
if(m_compressionFormat == ImageCompression::JPEG && m_image->isOMETIFF()) {
if(m_compressionFormat == ImageCompression::JPEG /*&& m_image->isOMETIFF()*/) {
// Use libjpeg for decompression, as ome-tiff files doesn't seem to like tiff's internal jpeg
auto buffer = make_uninitialized_unique<char[]>(tileWidth*tileHeight*channels);
bytesRead = TIFFReadRawTile(m_tiffHandle, tile_id, buffer.get(), tileWidth*tileHeight*channels);

jpeg_decompress_struct cinfo;
jpeg_error_mgr jerr; //error handling
jpeg_source_mgr src_mem;
jerr.error_exit = jpegErrorExit;
cinfo.err = jpeg_std_error(&jerr);
try {
jpeg_create_decompress(&cinfo);
jpeg_mem_src(&cinfo, (uchar*)buffer.get(), bytesRead);
int ret = jpeg_read_header(&cinfo, false);
if(ret != JPEG_HEADER_OK) {
throw Exception("Unable to read JPEG header");
}
jpeg_start_decompress(&cinfo);
uchar* line = (uchar*)data;
while(cinfo.output_scanline < cinfo.output_height) {
jpeg_read_scanlines (&cinfo, &line, 1);
line += channels*cinfo.output_width;
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
} catch(std::exception &e) {
jpeg_destroy_decompress( &cinfo );
throw Exception("JPEG error: " + std::string(e.what())); // or return an error code
}
JPEGCompression jpeg;
int width, height;
jpeg.decompress((uchar*)buffer.get(), bytesRead, &width, &height, (uchar*)data);
} else if(m_compressionFormat == ImageCompression::JPEGXL) {
auto buffer = make_uninitialized_unique<char[]>(tileWidth*tileHeight*channels);
bytesRead = TIFFReadRawTile(m_tiffHandle, tile_id, buffer.get(), tileWidth*tileHeight*channels);
Expand Down
1 change: 1 addition & 0 deletions source/FAST/Data/Access/ImagePyramidAccess.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class FAST_EXPORT ImagePyramidAccess : Object {
uint32_t writeTileToTIFF(int level, int x, int y, uchar* data, int width, int height, int channels);
uint32_t writeTileToTIFF(int level, int x, int y, uchar* data);
uint32_t writeTileToTIFFJPEGXL(int level, int x, int y, uchar *data);
uint32_t writeTileToTIFFJPEG(int level, int x, int y, uchar *data);
uint32_t writeTileToTIFFNeuralNetwork(int level, int x, int y, std::shared_ptr<Image> image);
int readTileFromTIFF(void* data, int x, int y, int level);
void propagatePatch(std::shared_ptr<Image> patch, int level, int x, int y);
Expand Down
12 changes: 8 additions & 4 deletions source/FAST/Data/ImagePyramid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ namespace fast {

int ImagePyramid::m_counter = 0;

ImagePyramid::ImagePyramid(int width, int height, int channels, int patchWidth, int patchHeight, DataType dataType, ImageCompression compression, int compressionQuality) {
ImagePyramid::ImagePyramid(int width, int height, int channels, int patchWidth, int patchHeight, ImageCompression compression, int compressionQuality, DataType dataType) {
if(channels <= 0 || channels > 4)
throw Exception("Nr of channels must be between 1 and 4");

Expand Down Expand Up @@ -125,8 +125,11 @@ ImagePyramid::ImagePyramid(int width, int height, int channels, int patchWidth,
break;
case ImageCompression::JPEG:
TIFFSetField(tiff, TIFFTAG_COMPRESSION, COMPRESSION_JPEG);
//TIFFSetField(tiff, TIFFTAG_JPEGTABLESMODE, JPEGTABLESMODE_QUANT);
//TIFFSetField(tiff, TIFFTAG_JPEGQUALITY, m_compressionQuality); // Must be set after previous line // FIXME not working, only 75 gives ok results
/*
TIFFSetField(tiff, TIFFTAG_JPEGQUALITY, m_compressionQuality); // Must be set after previous line // FIXME not working, only 75 gives ok results
TIFFSetField(tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
TIFFSetField(tiff, TIFFTAG_JPEGTABLESMODE, JPEGTABLESMODE_QUANT);
*/
break;
case ImageCompression::JPEGXL:
TIFFSetField(tiff, TIFFTAG_COMPRESSION, COMPRESSION_JXL);
Expand All @@ -152,7 +155,7 @@ ImagePyramid::ImagePyramid(int width, int height, int channels, int patchWidth,
// We need to write the first tile for some reason... or we will get an error saying it is missing required
// TileOffsets
TIFFCheckpointDirectory(m_tiffHandle); // Need to check in the tile width and length tags, before writing a tile
if(m_compressionFormat == ImageCompression::JPEGXL) {
if(m_compressionFormat == ImageCompression::JPEGXL || m_compressionFormat == ImageCompression::JPEG) {
// TODO Not needed?
//auto data = std::make_unique<uchar[]>(samplesPerPixel); // Is initialized to zeros
//auto tileID = TIFFComputeTile(tiff, 0, 0, 0, 0);
Expand Down Expand Up @@ -407,6 +410,7 @@ ImagePyramid::ImagePyramid(TIFF *fileHandle, std::vector<ImagePyramidLevel> leve
}
} else {
throw Exception("Unsupported TIFF data type: " + std::to_string(sampleFormat) + " " + std::to_string(bitsPerSample) + " bits");
//m_dataType = TYPE_UINT8;
}
// Get compression
uint16_t compressionTag;
Expand Down
6 changes: 3 additions & 3 deletions source/FAST/Data/ImagePyramid.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ class FAST_EXPORT ImagePyramid : public SpatialDataObject {
* @param channels Nr of channels of image pyramid (3 == color (RGB), 1 == grayscale)
* @param patchWidth Width of each patch
* @param patchHeight Height of each patch
* @param dataType Data type
* @param compression Compression type to use when storing the data in the TIFF.
* @param compressionQuality Quality of compression when using lossy compression like JPEG and JPEGXL.
* 100 = best, 0 = worst.
* @param dataType Data type
* @return instance
*/
FAST_CONSTRUCTOR(ImagePyramid,
Expand All @@ -44,9 +44,9 @@ class FAST_EXPORT ImagePyramid : public SpatialDataObject {
int, channels,,
int, patchWidth, = 256,
int, patchHeight, = 256,
DataType, dataType, = TYPE_UINT8,
ImageCompression, compression, = ImageCompression::UNSPECIFIED,
int, compressionQuality, = 90
int, compressionQuality, = 90,
DataType, dataType, = TYPE_UINT8
);
#ifndef SWIG
FAST_CONSTRUCTOR(ImagePyramid, openslide_t*, fileHandle,, std::vector<ImagePyramidLevel>, levels,);
Expand Down
15 changes: 13 additions & 2 deletions source/FAST/Exporters/ImageExporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <QImage>
#include <FAST/Algorithms/Compression/JPEGXLCompression.hpp>
#include <fstream>
#include <FAST/Algorithms/Compression/JPEGCompression.hpp>

namespace fast {

Expand Down Expand Up @@ -46,8 +47,8 @@ void ImageExporter::execute() {
if(pos == std::string::npos)
throw Exception("ImageExporter filename had no extension");

std::string ext = m_filename.substr(pos + 1);
if(stringToLower("jxl") == ext) {
std::string ext = stringToLower(m_filename.substr(pos + 1));
if(ext == "jxl") {
JPEGXLCompression jxl;
auto access = input->getImageAccess(ACCESS_READ);
void * inputData = access->get();
Expand All @@ -57,6 +58,16 @@ void ImageExporter::execute() {
std::ofstream file(m_filename, std::fstream::binary);
std::copy(compressedData.begin(), compressedData.end(), std::ostreambuf_iterator<char>(file));
file.close();
/*} else if(ext == "jpg" || ext == "jpeg") {
JPEGCompression jpg;
auto access = input->getImageAccess(ACCESS_READ);
void * inputData = access->get();
// Compress pixels with JPEG and store to binary file
std::vector<uchar> compressedData;
jpg.compress(inputData, input->getWidth(), input->getHeight(), &compressedData, m_quality);
std::ofstream file(m_filename, std::fstream::binary);
std::copy(compressedData.begin(), compressedData.end(), std::ostreambuf_iterator<char>(file));
file.close();*/
} else {
auto format = QImage::Format_RGBA8888;
int Qchannels = 4;
Expand Down
14 changes: 12 additions & 2 deletions source/FAST/Importers/ImageImporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <utility>
#include <FAST/Algorithms/Compression/JPEGXLCompression.hpp>
#include <fstream>
#include <FAST/Algorithms/Compression/JPEGCompression.hpp>

namespace fast {

Expand All @@ -21,8 +22,8 @@ void ImageImporter::execute() {
if(pos == std::string::npos)
throw Exception("ImageImporter filename had no extension");

std::string ext = m_filename.substr(pos + 1);
if(stringToLower("jxl") == ext) {
std::string ext = stringToLower(m_filename.substr(pos + 1));
if(ext == "jxl") {
std::ifstream file(m_filename, std::ios::in | std::ios::binary);
std::vector<unsigned char> buffer(std::istreambuf_iterator<char>(file), {});
JPEGXLCompression jxl;
Expand All @@ -32,6 +33,15 @@ void ImageImporter::execute() {
std::unique_ptr<uchar[]> decompressedData((uchar*)data); // Use unique_ptr to avoid copy
auto image = Image::create(width, height, TYPE_UINT8, 3, std::move(decompressedData));
addOutputData(0, image);
/*} else if(ext == "jpg" || ext == "jpeg") {
std::ifstream file(m_filename, std::ios::in | std::ios::binary);
std::vector<unsigned char> buffer(std::istreambuf_iterator<char>(file), {});
JPEGCompression jpg;
int width, height;
void* data = jpg.decompress(buffer.data(), buffer.size()*sizeof(uchar), &width, &height);
std::unique_ptr<uchar[]> decompressedData((uchar*)data); // Use unique_ptr to avoid copy
auto image = Image::create(width, height, TYPE_UINT8, 3, std::move(decompressedData));
addOutputData(0, image);*/
} else {
uchar* convertedPixelData;
// Load image from disk using Qt
Expand Down

0 comments on commit 330d716

Please sign in to comment.