From c3a9d3eb6844452388a83a9176c9d137cce58443 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Tue, 27 Jun 2023 20:00:37 +0100 Subject: [PATCH 01/17] Autodetect file type --- iipsrv/src/IIPImage.cc | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/iipsrv/src/IIPImage.cc b/iipsrv/src/IIPImage.cc index f8e556a..9e5f5d1 100644 --- a/iipsrv/src/IIPImage.cc +++ b/iipsrv/src/IIPImage.cc @@ -40,6 +40,7 @@ #include #include +#include "openslide.h" using namespace std; @@ -121,16 +122,10 @@ void IIPImage::testImageType() throw(file_error) unsigned char lbigtiff[4] = {0x4D,0x4D,0x00,0x2B}; // Little Endian BigTIFF unsigned char bbigtiff[4] = {0x49,0x49,0x2B,0x00}; // Big Endian BigTIFF - - // Compare our header sequence to our magic byte signatures - if (suffix=="vtif" || - suffix=="svs" || - suffix=="ndpi" || - suffix=="mrxs" || - suffix=="vms" || - suffix=="scn" || - suffix=="bif") + const char * vendor = openslide_detect_vendor( path.c_str() ); + if ( vendor != NULL && !strcmp(vendor, "generic-tiff") ) format = OPENSLIDE; + // Compare our header sequence to our magic byte signatures else if( memcmp( header, j2k, 10 ) == 0 ) format = JPEG2000; else if( memcmp( header, stdtiff, 3 ) == 0 || memcmp( header, lsbtiff, 4 ) == 0 || memcmp( header, msbtiff, 4 ) == 0 From 3e27021bd72ad722b29051348f0e80b2a71a2760 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Thu, 29 Jun 2023 19:03:59 +0100 Subject: [PATCH 02/17] Oh no, strcmp != streq --- iipsrv/src/IIPImage.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iipsrv/src/IIPImage.cc b/iipsrv/src/IIPImage.cc index 9e5f5d1..a18887e 100644 --- a/iipsrv/src/IIPImage.cc +++ b/iipsrv/src/IIPImage.cc @@ -123,7 +123,7 @@ void IIPImage::testImageType() throw(file_error) unsigned char bbigtiff[4] = {0x49,0x49,0x2B,0x00}; // Big Endian BigTIFF const char * vendor = openslide_detect_vendor( path.c_str() ); - if ( vendor != NULL && !strcmp(vendor, "generic-tiff") ) + if ( vendor != NULL && strcmp(vendor, "generic-tiff") ) format = OPENSLIDE; // Compare our header sequence to our magic byte signatures else if( memcmp( header, j2k, 10 ) == 0 ) format = JPEG2000; From e7f2d4c1a6e5247dddf9eb4e3ec0ba7163d771a4 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Wed, 26 Jul 2023 19:51:43 +0100 Subject: [PATCH 03/17] Separate OpenSlide's building if we build it ourselves, better in a separate dockerfile or a separate bashscript --- Dockerfile | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/Dockerfile b/Dockerfile index 01f2dcc..9b65421 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:focal +FROM cgd30/openslide:v8main ### update ARG DEBIAN_FRONTEND=noninteractive @@ -8,11 +8,10 @@ RUN apt-get -q -y dist-upgrade RUN apt-get clean RUN apt-get -q update -RUN apt-get -q -y install git autoconf automake make libtool pkg-config cmake apache2 libapache2-mod-fcgid libfcgi0ldbl zlib1g-dev libpng-dev libjpeg-dev libtiff5-dev libgdk-pixbuf2.0-dev libxml2-dev libsqlite3-dev libcairo2-dev libglib2.0-dev g++ libmemcached-dev libjpeg-turbo8-dev +RUN apt-get -q -y install git autoconf automake make libtool pkg-config apache2 libapache2-mod-fcgid libfcgi0ldbl g++ libmemcached-dev libjpeg-turbo8-dev RUN a2enmod rewrite RUN a2enmod fcgid -RUN mkdir /root/src COPY . /root/src WORKDIR /root/src @@ -29,32 +28,6 @@ RUN ln -s /etc/apache2/mods-available/proxy.conf /etc/apache2/mods-enabled/proxy COPY apache2.conf /etc/apache2/apache2.conf COPY ports.conf /etc/apache2/ports.conf -WORKDIR /root/src - -### openjpeg version in ubuntu 14.04 is 1.3, too old and does not have openslide required chroma subsampled images support. download 2.1.0 from source and build -RUN git clone https://github.com/uclouvain/openjpeg.git --branch=v2.3.0 -RUN mkdir /root/src/openjpeg/build -WORKDIR /root/src/openjpeg/build -RUN cmake -DBUILD_JPIP=ON -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_CODEC=ON -DBUILD_PKGCONFIG_FILES=ON ../ -RUN make -RUN make install - -### Openslide -WORKDIR /root/src -## get my fork from openslide source cdoe -RUN git clone https://github.com/openslide/openslide.git - -## build openslide -WORKDIR /root/src/openslide -RUN git checkout tags/v3.4.1 -RUN autoreconf -i -#RUN ./configure --enable-static --enable-shared=no -# may need to set OPENJPEG_CFLAGS='-I/usr/local/include' and OPENJPEG_LIBS='-L/usr/local/lib -lopenjp2' -# and the corresponding TIFF flags and libs to where bigtiff lib is installed. -RUN ./configure -RUN make -RUN make install - ### iipsrv WORKDIR /root/src/iipsrv RUN ./autogen.sh From 4ae8ef6ee741f69bbca50936b1a58bd4582de951 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Wed, 26 Jul 2023 19:53:26 +0100 Subject: [PATCH 04/17] dcm format --- iipsrv/src/IIPImage.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/iipsrv/src/IIPImage.cc b/iipsrv/src/IIPImage.cc index a18887e..e4e3ba6 100644 --- a/iipsrv/src/IIPImage.cc +++ b/iipsrv/src/IIPImage.cc @@ -169,6 +169,7 @@ void IIPImage::testImageType() throw(file_error) suffix=="mrxs" || suffix=="vms" || suffix=="scn" || + suffix=="dcm" || suffix=="bif") format = OPENSLIDE; else if( suffix == "jp2" || suffix == "jpx" || suffix == "j2k" ) format = JPEG2000; From d22aa34ab42bfa726bae3c3d5fd63f13532cbb0e Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Sat, 29 Jul 2023 09:42:11 +0100 Subject: [PATCH 05/17] camicroscope/image-decoders:latest docker --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9b65421..9cc2f5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM cgd30/openslide:v8main +FROM camicroscope/image-decoders:latest ### update ARG DEBIAN_FRONTEND=noninteractive From e7e4fb8846d3a5a00b96242554086df70966bc24 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:26:06 +0100 Subject: [PATCH 06/17] env vars --- apache2-iipsrv-fcgid.conf | 2 ++ fcgid.conf | 2 ++ iip_httpd.conf | 2 ++ 3 files changed, 6 insertions(+) diff --git a/apache2-iipsrv-fcgid.conf b/apache2-iipsrv-fcgid.conf index ba17b3a..7eee54c 100644 --- a/apache2-iipsrv-fcgid.conf +++ b/apache2-iipsrv-fcgid.conf @@ -27,6 +27,8 @@ FcgidInitialEnv MAX_CVT "5000" #FcgidInitialEnv FILESYSTEM_PREFIX "/mnt/images/" FcgidInitialEnv LD_LIBRARY_PATH "/usr/local/lib" FcgidInitialEnv MAX_TILE_CACHE_SIZE "64" +FcgidInitialEnv BFBRIDGE_CACHEDIR "/tmp/" +FcgidInitialEnv BFBRIDGE_CLASSPATH "/usr/lib/java" # Define the idle timeout as unlimited and the number of # processes we want diff --git a/fcgid.conf b/fcgid.conf index c5fba9a..7f3a068 100644 --- a/fcgid.conf +++ b/fcgid.conf @@ -27,6 +27,8 @@ FcgidInitialEnv MAX_CVT "5000" FcgidInitialEnv LD_LIBRARY_PATH "/usr/local/lib" FcgidInitialEnv MAX_TILE_CACHE_SIZE "64" FcgidInitialEnv CORS "*" +FcgidInitialEnv BFBRIDGE_CACHEDIR "/tmp/" +FcgidInitialEnv BFBRIDGE_CLASSPATH "/usr/lib/java" # Define the idle timeout as unlimited and the number of # processes we want diff --git a/iip_httpd.conf b/iip_httpd.conf index c1d4c55..8e3709f 100644 --- a/iip_httpd.conf +++ b/iip_httpd.conf @@ -28,5 +28,7 @@ FastCgiServer /var/www/localhost/fcgi-bin/iipsrv.fcgi \ -initial-env JPEG_QUALITY=50 \ -initial-env MAX_CVT=3000 \ -initial-env CORS=* \ +-initial-env BFBRIDGE_CACHEDIR=/tmp/ \ +-initial-env BFBRIDGE_CLASSPATH=/usr/lib/java \ -listen-queue-depth 2048 \ -processes 1 From 2491fb1a500b338bfac106f8db1e5e47978f83f5 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:26:25 +0100 Subject: [PATCH 07/17] Create BioFormatsImage.cc --- iipsrv/src/BioFormatsImage.cc | 967 ++++++++++++++++++++++++++++++++++ 1 file changed, 967 insertions(+) create mode 100644 iipsrv/src/BioFormatsImage.cc diff --git a/iipsrv/src/BioFormatsImage.cc b/iipsrv/src/BioFormatsImage.cc new file mode 100644 index 0000000..2e947d3 --- /dev/null +++ b/iipsrv/src/BioFormatsImage.cc @@ -0,0 +1,967 @@ +#include "OpenSlideImage.h" +#include "Timer.h" +#include +#include +#include +#include + +#include +#include + +#include +// #define DEBUG_OSI 1 +using namespace std; + +extern std::ofstream logfile; + +/// Overloaded function for opening a TIFF image +void OpenSlideImage::openImage() throw(file_error) +{ + + string filename = getFileName(currentX, currentY); + + // get the file modification date/time. return false if not changed, return true if change compared to the stored info. + bool modified = updateTimestamp(filename); + + // if (modified && isSet) { +#ifdef DEBUG_OSI + Timer timer; + timer.start(); + + logfile << "OpenSlide :: openImage() :: start" << endl + << flush; + +#endif + // close previous + closeImage(); + + osr = openslide_open(filename.c_str()); + + const char *error = openslide_get_error(osr); +#ifdef DEBUG_OSI + logfile << "OpenSlide :: openImage() get error :: completed " << filename << endl + << flush; +#endif + + if (error) + { + logfile << "ERROR: encountered error: " << error << " while opening " << filename << " with OpenSlide: " << endl + << flush; + throw file_error(string("Error opening '" + filename + "' with OpenSlide, error " + error)); + } +#ifdef DEBUG_OSI + logfile << "OpenSlide :: openImage() :: " << timer.getTime() << " microseconds" << endl + << flush; +#endif + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: openImage() :: completed " << filename << endl + << flush; +#endif + if (osr == NULL) + { + logfile << "ERROR: can't open " << filename << " with OpenSlide" << endl + << flush; + throw file_error(string("Error opening '" + filename + "' with OpenSlide")); + } + + if (bpc == 0) + { + loadImageInfo(currentX, currentY); + } + + isSet = true; + // } else { + // #ifdef DEBUG_OSI + // logfile << "OpenSlide :: openImage() :: not newer. reuse openslide object." << endl << flush; + // #endif + // } +} + +/// given an open OSI file, get information from the image. +void OpenSlideImage::loadImageInfo(int x, int y) throw(file_error) +{ + +#ifdef DEBUG_OSI + logfile << "OpenSlideImage :: loadImageInfo()" << endl; + + if (!osr) + { + logfile << "loadImageInfo called before openImage()" << endl; + } +#endif + + int64_t w = 0, h = 0; + currentX = x; + currentY = y; + + const char *error; + +#ifdef DEBUG_OSI + const char *const *prop_names = openslide_get_property_names(osr); + error = openslide_get_error(osr); + if (error) + { + logfile << "ERROR: encountered error: " << error << " while getting property names: " << error << endl; + } + + int i = 0; + + logfile << "Properties:" << endl; + while (prop_names[i]) + { + logfile << "" << i << " : " << prop_names[i] << " = " << openslide_get_property_value(osr, prop_names[i]) << endl; + error = openslide_get_error(osr); + if (error) + { + logfile << "ERROR: encountered error: " << error << " while getting property: " << error << endl; + } + + ++i; + } +#endif + + const char *prop_val; + // // TODO: use actual tile width. default is 256 + // prop_val = openslide_get_property_value(osr, "openslide.level[0].tile-width"); + // error = openslide_get_error(osr); + // if (error) { + // logfile << "ERROR: encountered error: " << error << " while getting property: " << error << endl; + // } + // + // if (prop_val) tile_width = atoi(prop_val); + // else tile_width = 256; + tile_width = 256; // default to power of 2 to make downsample simpler. + + // prop_val = openslide_get_property_value(osr, "openslide.level[0].tile-height"); + // error = openslide_get_error(osr); + // if (error) { + // logfile << "ERROR: encountered error: " << error << " while getting property: " << error << endl; + // } + // if (prop_val) tile_height = atoi(prop_val); + // else tile_height = 256; + tile_height = 256; // default to power of 2 to make downsample simpler. + + openslide_get_level0_dimensions(osr, &w, &h); + error = openslide_get_error(osr); + if (error) + { + logfile << "ERROR: encountered error: " << error << " while getting level0 dim: " << error << endl; + } + +#ifdef DEBUG_OSI + logfile << "dimensions :" << w << " x " << h << endl; + // logfile << "comment : " << comment << endl; +#endif + + // TODO Openslide outputs 8 bit ABGR always. + channels = 3; + bpc = 8; + colourspace = sRGB; + + // const char* comment = openslide_get_comment(osr); + + // save the openslide dimensions. + std::vector openslide_widths, openslide_heights; + openslide_widths.clear(); + openslide_heights.clear(); + + int32_t openslide_levels = openslide_get_level_count(osr); + error = openslide_get_error(osr); + if (error) + { + logfile << "ERROR: encountered error: " << error << " while getting level count: " << error << endl; + } + +#ifdef DEBUG_OSI + logfile << "number of levels = " << openslide_levels << endl; + double tempdownsample; +#endif + int64_t ww, hh; + for (int32_t i = 0; i < openslide_levels; i++) + { + openslide_get_level_dimensions(osr, i, &ww, &hh); + error = openslide_get_error(osr); + if (error) + { + logfile << "ERROR: encountered error: " << error << " while getting level dims: " << error << endl; + } + + openslide_widths.push_back(ww); + openslide_heights.push_back(hh); +#ifdef DEBUG_OSI + tempdownsample = openslide_get_level_downsample(osr, i); + error = openslide_get_error(osr); + if (error) + { + logfile << "ERROR: encountered error: " << error << " while getting level downsamples: " << error << endl; + } + + logfile << "\tlevel " << i << "\t(w,h) = (" << ww << "," << hh << ")\tdownsample=" << tempdownsample << endl; +#endif + } + + openslide_widths.push_back(0); + openslide_heights.push_back(0); + + image_widths.clear(); + image_heights.clear(); + + //======== virtual levels because getTile specifies res as powers of 2. + // precompute and store addition info about the tiles + lastTileXDim.clear(); + lastTileYDim.clear(); + numTilesX.clear(); + numTilesY.clear(); + + openslide_level_to_use.clear(); + openslide_downsample_in_level.clear(); + unsigned int os_level = 0; + unsigned int os_downsample_in_level = 1; + + // store the original size. + image_widths.push_back(w); + image_heights.push_back(h); + lastTileXDim.push_back(w % tile_width); + lastTileYDim.push_back(h % tile_height); + numTilesX.push_back((w + tile_width - 1) / tile_width); + numTilesY.push_back((h + tile_height - 1) / tile_height); + openslide_level_to_use.push_back(os_level); + openslide_downsample_in_level.push_back(os_downsample_in_level); + + // what if there are openslide levels with dim smaller than this? + + // populate at 1/2 size steps + while ((w > tile_width) || (h > tile_height)) + { + // need a level that's has image completely inside 1 tile. + // (stop with both w and h less than tile_w/h, previous iteration divided by 2. + + w >>= 1; // divide by 2 and floor. losing 1 pixel from higher res. + h >>= 1; + + // compare to next level width and height. if smaller, next level is better for supporting the w/h + // so switch level. + if (w <= openslide_widths[os_level + 1] && h <= openslide_heights[os_level + 1]) + { + ++os_level; + os_downsample_in_level = 1; // just went to next smaller level, don't downsample internally yet. + } + else + { + os_downsample_in_level <<= 1; // next one, downsample internally by 2. + } + openslide_level_to_use.push_back(os_level); + openslide_downsample_in_level.push_back(os_downsample_in_level); + + image_widths.push_back(w); + image_heights.push_back(h); + lastTileXDim.push_back(w % tile_width); + lastTileYDim.push_back(h % tile_height); + numTilesX.push_back((w + tile_width - 1) / tile_width); + numTilesY.push_back((h + tile_height - 1) / tile_height); + +#ifdef DEBUG_OSI + logfile << "Create virtual layer : " << w << "x" << h << std::endl; +#endif + } + +#ifdef DEBUG_OSI + for (int t = 0; t < image_widths.size(); t++) + { + logfile << "virtual level " << t << " (w,h)=(" << image_widths[t] << "," << image_heights[t] << "),"; + logfile << " (last_tw,last_th)=(" << lastTileXDim[t] << "," << lastTileYDim[t] << "),"; + logfile << " (ntx,nty)=(" << numTilesX[t] << "," << numTilesY[t] << "),"; + logfile << " os level=" << openslide_level_to_use[t] << " downsample from os_level=" << openslide_downsample_in_level[t] << endl; + } +#endif + + numResolutions = numTilesX.size(); + + // only support bpp of 8 (255 max), and 3 channels + min.assign(channels, 0.0f); + max.assign(channels, 255.0f); +} + +/// Overloaded function for closing a TIFF image +void OpenSlideImage::closeImage() +{ +#ifdef DEBUG_OSI + Timer timer; + timer.start(); +#endif + + if (osr != NULL) + { + openslide_close(osr); + osr = NULL; + } + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: closeImage() :: " << timer.getTime() << " microseconds" << endl; +#endif +} + +// +//// TCP: support get region (internally, already doing it. +///// Overloaded function for returning a region for a given angle and resolution +///** Return a RawTile object: Overloaded by child class. +// \param ha horizontal angle +// \param va vertical angle +// \param res resolution +// \param layers number of quality layers to decode +// \param x x coordinate at resolution r +// \param y y coordinate at resolution r +// \param w width of region at resolution r +// \param h height of region at resolution r +// */ +// RawTile OpenSlideImage::getRegion( int ha, int va, unsigned int res, int layers, int x, int y, unsigned int w, unsigned int h ) { +// // ignore ha, va, layers. important things are r, x, y, w, and h. +// +// +// #ifdef DEBUG_OSI +// Timer timer; +// timer.start(); +// #endif +// +// if (res > (numResolutions-1)) { +// ostringstream tile_no; +// tile_no << "OpenSlide :: Asked for non-existant resolution: " << res; +// throw tile_no.str(); +// return 0; +// } +// +// // convert r to zoom +// +// // res is specified in opposite order from image levels: image level 0 has highest res, +// // image level nRes-1 has res of 0. +// uint32_t openslide_zoom = this->numResolutions - 1 - res; +// +// #ifdef DEBUG_OSI +// logfile << "OpenSlide :: getRegion() :: res=" << res << " pos " << x << "x" << y << " size " << w << "x" << h << " is_zoom= " << openslide_zoom << endl; +// +// #endif +// +// +// +// uint32_t lw, lh; +// int32_t lx, ly; +// +// unsigned int res_width = image_widths[openslide_zoom]; +// unsigned int res_height = image_heights[openslide_zoom]; +// +// // bound the x,y position +// lx = std::max(0, x); lx = std::min(lx, static_cast(res_width)); +// ly = std::max(0, y); ly = std::min(ly, static_cast(res_height)); +// +// // next bound the w and height. +// lw = std::min(w, res_width - lx); +// lh = std::min(h, res_height - ly); +// +// +// +// // convert x ,y, to the right coordinate system +// if (lw == 0 || lh == 0) { +// ostringstream tile_no; +// tile_no << "OpenSlideImage :: Asked for zero-sized region " << x << "x" << y << ", size " << w << "x" << h << ", res dim=" << res_width << "x" << res_height; +// throw tile_no.str(); +// } +// +// // Create our raw tile buffer and initialize some values +// RawTile region(0, res, ha, va, w, h, channels, bpp); +// region.dataLength = w * h * channels * sizeof(unsigned char); +// region.filename = getImagePath(); +// region.timestamp = timestamp; +// // new a block that is larger for openslide library to directly copy in. +// // then shuffle from BGRA to RGB. relying on delete [] to do the right thing. +// region.data = new unsigned char[w * h * 4 * sizeof(unsigned char)]; // TODO: avoid reallocation? +// region.memoryManaged = 1; // allocated data, so use this flag to indicate that it needs to be cleared on destruction +// //rawtile->padded = false; +// #ifdef DEBUG_OSI +// logfile << "Allocating tw * th * channels * sizeof(char) : " << w << " * " << h << " * " << channels << " * sizeof(char) " << endl << flush; +// #endif +// +// // call read +// read(openslide_zoom, w, h, x, y, region.data); +// +// +// #ifdef DEBUG_OSI +// logfile << "OpenSlide :: getRegion() :: " << timer.getTime() << " microseconds" << endl << flush; +// logfile << "REGION RENDERED" << std::endl; +// #endif +// +// return ( region ); +// +//} + +/// Overloaded function for getting a particular tile +/** \param x horizontal sequence angle (for microscopy, ignored.) + \param y vertical sequence angle (for microscopy, ignored.) + \param r resolution - specified as -log_2(mag factor), where mag_factor ~= highest res width / target res width. 0 to numResolutions - 1. + \param l number of quality layers to decode - for jpeg2000 + \param t tile number (within the resolution level.) specified as a sequential number = y * width + x; + */ +RawTilePtr OpenSlideImage::getTile(int seq, int ang, unsigned int iipres, int layers, unsigned int tile) throw(file_error) +{ + +#ifdef DEBUG_OSI + Timer timer; + timer.start(); +#endif + + if (iipres > (numResolutions - 1)) + { + ostringstream tile_no; + tile_no << "OpenSlide :: Asked for non-existant resolution: " << iipres; + throw file_error(tile_no.str()); + return 0; + } + + // res is specified in opposite order from openslide virtual image levels: image level 0 has highest res, + // image level nRes-1 has res of 0. + uint32_t osi_level = numResolutions - 1 - iipres; + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: getTile() :: res=" << iipres << " tile= " << tile << " is_zoom= " << osi_level << endl; + +#endif + + //======= get the dimensions in pixels and num tiles for the current resolution + + // int64_t layer_width = image_widths[openslide_zoom]; + // int64_t layer_height = image_heights[openslide_zoom]; + // openslide_get_layer_dimensions(osr, layers, &layer_width, &layer_height); + + // Calculate the number of tiles in each direction + size_t ntlx = numTilesX[osi_level]; + size_t ntly = numTilesY[osi_level]; + + if (tile >= ntlx * ntly) + { + ostringstream tile_no; + tile_no << "OpenSlideImage :: Asked for non-existant tile: " << tile; + throw file_error(tile_no.str()); + } + + // tile x. + size_t tx = tile % ntlx; + size_t ty = tile / ntlx; + + RawTilePtr ttt = getCachedTile(tx, ty, iipres); + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: getTile() :: total " << timer.getTime() << " microseconds" << endl + << flush; + logfile << "TILE RENDERED" << std::endl; +#endif + return ttt; // return cached instance. TileManager's job to copy it.. +} + +/** + * check if cache has tile. + * if yes, return it. + * if not, + * if a native layer, getNativeTile, + * else call halfsampleAndComposeTile + * @param res iipsrv's resolution id. openslide's level is inverted from this. + */ +RawTilePtr OpenSlideImage::getCachedTile(const size_t tilex, const size_t tiley, const uint32_t iipres) +{ + +#ifdef DEBUG_OSI + Timer timer; + timer.start(); +#endif + + assert(tileCache); + + // check if cache has tile + uint32_t osi_level = numResolutions - 1 - iipres; + uint32_t tid = tiley * numTilesX[osi_level] + tilex; + RawTilePtr ttt = tileCache->getObject(TileCache::getIndex(getImagePath(), iipres, tid, 0, 0, UNCOMPRESSED, 0)); + + // if cache has file, return it + if (ttt) + { +#ifdef DEBUG_OSI + logfile << "OpenSlide :: getCachedTile() :: Cache Hit " << tilex << "x" << tiley << "@" << iipres << " osi tile bounds: " << numTilesX[osi_level] << "x" << numTilesY[osi_level] << " " << timer.getTime() << " microseconds" << endl + << flush; +#endif + + return ttt; + } + // else caches does not have it. +#ifdef DEBUG_OSI + logfile << "OpenSlide :: getCachedTile() :: Cache Miss " << tilex << "x" << tiley << "@" << iipres << " osi tile bounds: " << numTilesX[osi_level] << "x" << numTilesY[osi_level] << " " << timer.getTime() << " microseconds" << endl + << flush; +#endif + + // is this a native layer? + if (openslide_downsample_in_level[osi_level] == 1) + { + // supported by native openslide layer + // tile manager will cache if needed + return getNativeTile(tilex, tiley, iipres); + } + else + { + // not supported by native openslide layer, so need to compose from next level up, + return halfsampleAndComposeTile(tilex, tiley, iipres); + + // tile manager will cache this one. + } +} + +/** + * read from file, color convert, store in cache, and return tile. + * + * @param res iipsrv's resolution id. openslide's level is inverted from this. + */ +RawTilePtr OpenSlideImage::getNativeTile(const size_t tilex, const size_t tiley, const uint32_t iipres) +{ + +#ifdef DEBUG_OSI + Timer timer; + timer.start(); +#endif + + if (!osr) + { + logfile << "openslide image not yet loaded " << endl; + } + + // compute the parameters (i.e. x and y offsets, w/h, and bestlayer to use. + uint32_t osi_level = numResolutions - 1 - iipres; + + // find the next layer to downsample to desired zoom level z + // + uint32_t bestLayer = openslide_level_to_use[osi_level]; + + size_t ntlx = numTilesX[osi_level]; + size_t ntly = numTilesY[osi_level]; + + // compute the correct width and height + size_t tw = tile_width; + size_t th = tile_height; + + // Get the width and height for last row and column tiles + size_t rem_x = this->lastTileXDim[osi_level]; + size_t rem_y = this->lastTileYDim[osi_level]; + + // Alter the tile size if it's in the rightmost column + if ((tilex == ntlx - 1) && (rem_x != 0)) + { + tw = rem_x; + } + // Alter the tile size if it's in the bottom row + if ((tiley == ntly - 1) && (rem_y != 0)) + { + th = rem_y; + } + + // create the RawTile object + RawTilePtr rt(new RawTile(tiley * ntlx + tilex, iipres, 0, 0, tw, th, channels, bpc)); + + // compute the size, etc + rt->dataLength = tw * th * channels * sizeof(unsigned char); + rt->filename = getImagePath(); + rt->timestamp = timestamp; + + // new a block that is larger for openslide library to directly copy in. + // then shuffle from BGRA to RGB. relying on delete [] to do the right thing. + rt->data = new unsigned char[tw * th * 4 * sizeof(unsigned char)]; + rt->memoryManaged = 1; // allocated data, so use this flag to indicate that it needs to be cleared on destruction + // rawtile->padded = false; +#ifdef DEBUG_OSI + logfile << "Allocating tw * th * channels * sizeof(char) : " << tw << " * " << th << " * " << 4 << " * sizeof(char) " << endl + << flush; +#endif + + // READ FROM file + + //======= next compute the x and y coordinates (top left corner) in level 0 coordinates + //======= expected by openslide_read_region. + size_t tx0 = (tilex * tile_width) << osi_level; // same as multiply by z power of 2 + size_t ty0 = (tiley * tile_height) << osi_level; + + openslide_read_region(osr, reinterpret_cast(rt->data), tx0, ty0, bestLayer, tw, th); + const char *error = openslide_get_error(osr); + if (error) + { + logfile << "ERROR: encountered error: " << error << " while reading region exact at " << tx0 << "x" << ty0 << " dim " << tw << "x" << th << " with OpenSlide: " << error << endl; + } + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: getNativeTile() :: read_region() :: " << tilex << "x" << tiley << "@" << iipres << " " << timer.getTime() << " microseconds" << endl + << flush; +#endif + + if (!rt->data) + throw string("FATAL : OpenSlideImage read_region => allocation memory ERROR"); + +#ifdef DEBUG_OSI + timer.start(); +#endif + + // COLOR CONVERT in place BGRA->RGB conversion + this->bgra2rgb(reinterpret_cast(rt->data), tw, th); + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: getNativeTile() :: bgra2rgb() :: " << timer.getTime() << " microseconds" << endl + << flush; +#endif + + // and return it. + return rt; +} + +/** + * @detail return from the local cache a tile. + * The tile may be native (directly from file), + * previously downsampled and cached, + * downsampled from existing cached tile (and cached now for later use), or + * downsampled from native tile (from file. both native and downsampled tiles will be cached for later use) + * + * note that tilex and tiley can be found by multiplying by 2 raised to power of the difference in levels. + * 2 versions - direct and recursive. direct should have slightly lower latency. + + * This function + * automatically downsample a region in the missing zoom level z, if needed. + * Arguments are exactly as what would be given to openslide_read_region(). + * Note that z is not the openslide layer, but the desired zoom level, because + * the slide may not have all the layers that correspond to all the + * zoom levels. The number of layers is equal or less than the number of + * zoom levels in an equivalent zoomify format. + * This downsampling method simply does area averaging. If interpolation is desired, + * an image processing library could be used. + + * go to next level in size, get 4 tiles, downsample and compose. + * + * call 4x (getCachedTile at next res, downsample, compose), + * store in cache, and return tile. (causes recursion, stops at native layer or in cache.) + */ +RawTilePtr OpenSlideImage::halfsampleAndComposeTile(const size_t tilex, const size_t tiley, const uint32_t iipres) +{ + // not in cache and not a native tile, so create one from higher sampling. +#ifdef DEBUG_OSI + Timer timer; + timer.start(); +#endif + + // compute the parameters (i.e. x and y offsets, w/h, and bestlayer to use. + uint32_t osi_level = numResolutions - 1 - iipres; + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: halfsampleAndComposeTile() :: zoom=" << osi_level << " from " << (osi_level - 1) << endl; +#endif + + size_t ntlx = numTilesX[osi_level]; + size_t ntly = numTilesY[osi_level]; + + // compute the correct width and height + size_t tw = tile_width; + size_t th = tile_height; + + // Get the width and height for last row and column tiles + size_t rem_x = this->lastTileXDim[osi_level]; + size_t rem_y = this->lastTileYDim[osi_level]; + + // Alter the tile size if it's in the rightmost column + if ((tilex == ntlx - 1) && (rem_x != 0)) + { + tw = rem_x; + } + // Alter the tile size if it's in the bottom row + if ((tiley == ntly - 1) && (rem_y != 0)) + { + th = rem_y; + } + + // allocate raw tile. + RawTilePtr rt(new RawTile(tiley * ntlx + tilex, iipres, 0, 0, tw, th, channels, bpc)); + + // compute the size, etc + rt->dataLength = tw * th * channels; + rt->filename = getImagePath(); + rt->timestamp = timestamp; + + // new a block that is larger for openslide library to directly copy in. + // then shuffle from BGRA to RGB. relying on delete [] to do the right thing. + rt->data = new unsigned char[rt->dataLength]; + rt->memoryManaged = 1; // allocated data, so use this flag to indicate that it needs to be cleared on destruction + // rawtile->padded = false; +#ifdef DEBUG_OSI + logfile << "Allocating tw * th * channels * sizeof(char) : " << tw << " * " << th << " * " << channels << " * sizeof(char) " << endl + << flush; +#endif + + // new iipres - next res. recall that larger res corresponds to higher mag, with largest res being max resolution. + uint32_t tt_iipres = iipres + 1; + RawTilePtr tt; + // temp storage. + uint8_t *tt_data = new uint8_t[(tile_width >> 1) * (tile_height >> 1) * channels]; + size_t tt_out_w, tt_out_h; + + // uses 4 tiles to create new. + for (int j = 0; j < 2; ++j) + { + + size_t tty = tiley * 2 + j; + + if (tty >= numTilesY[osi_level - 1]) + break; // at edge, this may not be a 2x2 block. + + for (int i = 0; i < 2; ++i) + { + // compute new tile x and y and iipres. + size_t ttx = tilex * 2 + i; + if (ttx >= numTilesX[osi_level - 1]) + break; // at edge, this may not be a 2x2 block. + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: halfsampleAndComposeTile() :: call getCachedTile " << endl + << flush; +#endif + + // get the tile + tt = getCachedTile(ttx, tty, tt_iipres); + + if (tt) + { + + // cache the next res tile + +#ifdef DEBUG_OSI + timer.start(); +#endif + + // cache it + tileCache->insert(tt); // copy is made? + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: halfsampleAndComoseTile() :: cache insert res " << tt_iipres << " " << ttx << "x" << tty << " :: " << timer.getTime() << " microseconds" << endl + << flush; +#endif + + // downsample into a temp storage. + halfsample_3(reinterpret_cast(tt->data), tt->width, tt->height, + tt_data, tt_out_w, tt_out_h); + + // compose into raw tile. note that tile 0,0 in a 2x2 block always have size tw/2 x th/2 + compose(tt_data, tt_out_w, tt_out_h, (tile_width / 2) * i, (tile_height / 2) * j, + reinterpret_cast(rt->data), tw, th); + } +#ifdef DEBUG_OSI + logfile << "OpenSlide :: halfsampleAndComposeTile() :: called getCachedTile " << endl + << flush; +#endif + } + } + delete[] tt_data; +#ifdef DEBUG_OSI + logfile << "OpenSlide :: halfsampleAndComposeTile() :: downsample " << osi_level << " from " << (osi_level - 1) << " :: " << timer.getTime() << " microseconds" << endl + << flush; +#endif + + // and return it. + return rt; +} + +// h is number of rows to process. w is number of columns to process. +void OpenSlideImage::bgra2rgb(uint8_t *data, const size_t w, const size_t h) +{ + // swap bytes in place. we can because we are going from 4 bytes to 3, and because we are using a register to bswap + // 0000111122223333 + // in: BGRABGRABGRA + // out: RGBRGBRGB + // 000111222333 + + // this version relies on compiler to generate bswap code. + // bswap is only very slightly slower than SSSE3 and AVX2 code, and about 2x faster than naive single byte copies on core i5 ivy bridge. + uint8_t *out = data; + uint32_t *in = reinterpret_cast(data); + uint32_t *end = in + w * h; + + uint32_t t; + + // remaining + for (; in < end; ++in) + { + *(reinterpret_cast(out)) = bgra2rgb_kernel(*in); + + out += channels; + } +} + +/** + * performs 1/2 size downsample on rgb 24bit images + * @details usingg the following property, + * (a + b) / 2 = ((a ^ b) >> 1) + (a & b) + * which does not cause overflow. + * + * mask with 0xFEFEFEFE to revent underflow during bitshift, allowing 4 compoents + * to be processed concurrently in same register without SSE instructions. + * + * output size is computed the same way as the virtual level dims - round down + * if not even. + * + * @param in + * @param in_w + * @param in_h + * @param out + * @param out_w + * @param out_h + * @param downSamplingFactor always power of 2. + */ +void OpenSlideImage::halfsample_3(const uint8_t *in, const size_t in_w, const size_t in_h, + uint8_t *out, size_t &out_w, size_t &out_h) +{ + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: halfsample_3() :: start :: in " << (void *)in << " out " << (void *)out << endl + << flush; + logfile << " :: in wxh " << in_w << "x" << in_h << endl + << flush; +#endif + + // do one 1/2 sample run + out_w = in_w >> 1; + out_h = in_h >> 1; + + if ((out_w == 0) || (out_h == 0)) + { + logfile << "OpenSlide :: halfsample_3() :: ERROR: zero output width or height " << endl + << flush; + return; + } + + if (!(in)) + { + logfile << "OpenSlide :: halfsample_3() :: ERROR: null input " << endl + << flush; + return; + } + + uint8_t const *row1, *row2; + uint8_t *dest = out; // if last recursion, put in out, else do it in place + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: halfsample_3() :: top " << endl + << flush; +#endif + + // walk through all pixels in output, except last. + size_t max_h = out_h - 1, + max_w = out_w; + size_t inRowStride = in_w * channels; + size_t inRowStride2 = 2 * inRowStride; + size_t inColStride2 = 2 * channels; + // skip last row, as the very last dest element may have overflow. + for (size_t j = 0; j < max_h; ++j) + { + // move row pointers forward 2 rows at a time - in_w may not be multiple of 2. + row1 = in + j * inRowStride2; + row2 = row1 + inRowStride; + + for (size_t i = 0; i < max_w; ++i) + { + *(reinterpret_cast(dest)) = halfsample_kernel_3(row1, row2); + // output is contiguous. + dest += channels; + row1 += inColStride2; + row2 += inColStride2; + } + } + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: halfsample_3() :: last row " << endl + << flush; +#endif + + // for last row, skip the last element + row1 = in + max_h * inRowStride2; + row2 = row1 + inRowStride; + + --max_w; + for (size_t i = 0; i < max_w; ++i) + { + *(reinterpret_cast(dest)) = halfsample_kernel_3(row1, row2); + dest += channels; + row1 += inColStride2; + row2 += inColStride2; + } + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: halfsample_3() :: last one " << endl + << flush; +#endif + + // for last pixel, use memcpy to avoid writing out of bounds. + uint32_t v = halfsample_kernel_3(row1, row2); + memcpy(dest, reinterpret_cast(&v), channels); + + // at this point, in has been averaged and stored . + // since we stride forward 2 col and rows at a time, we don't need to worry about overwriting an unread pixel. +#ifdef DEBUG_OSI + logfile << "OpenSlide :: halfsample_3() :: done" << endl + << flush; +#endif +} + +// in is contiguous, out will be when done. +void OpenSlideImage::compose(const uint8_t *in, const size_t in_w, const size_t in_h, + const size_t &xoffset, const size_t &yoffset, + uint8_t *out, const size_t &out_w, const size_t &out_h) +{ + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: compose() :: start " << endl + << flush; +#endif + + if ((in_w == 0) || (in_h == 0)) + { +#ifdef DEBUG_OSI + logfile << "OpenSlide :: compose() :: zero width or height " << endl + << flush; +#endif + return; + } + if (!(in)) + { +#ifdef DEBUG_OSI + logfile << "OpenSlide :: compose() :: nullptr input " << endl + << flush; +#endif + return; + } + + if (out_h < yoffset + in_h) + { + logfile << "COMPOSE ERROR: out_h, yoffset, in_h: " << out_h << "," << yoffset << "," << in_h << endl; + assert(out_h >= yoffset + in_h); + } + if (out_w < xoffset + in_w) + { + logfile << "COMPOSE ERROR: out_w, xoffset, in_w: " << out_w << "," << xoffset << "," << in_w << endl; + assert(out_w >= xoffset + in_w); + } + + size_t dest_stride = out_w * channels; + size_t src_stride = in_w * channels; + + uint8_t *dest = out + yoffset * dest_stride + xoffset * channels; + uint8_t const *src = in; + + for (int k = 0; k < in_h; ++k) + { + memcpy(dest, src, in_w * channels); + dest += dest_stride; + src += src_stride; + } + +#ifdef DEBUG_OSI + logfile << "OpenSlide :: compose() :: start " << endl + << flush; +#endif +} From 280a8d9178b312f58953961bfab97b42a8e9388a Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Sun, 27 Aug 2023 10:56:44 +0100 Subject: [PATCH 08/17] biofrmats --- README.md | 2 + iipsrv/src/BioFormatsImage.cc | 849 +++++++++++++++++++------------ iipsrv/src/BioFormatsImage.h | 175 +++++++ iipsrv/src/BioFormatsInstance.cc | 21 + iipsrv/src/BioFormatsInstance.h | 239 +++++++++ iipsrv/src/BioFormatsManager.cc | 3 + iipsrv/src/BioFormatsManager.h | 41 ++ iipsrv/src/BioFormatsThread.cc | 40 ++ iipsrv/src/BioFormatsThread.h | 40 ++ iipsrv/src/FIF.cc | 10 +- iipsrv/src/IIPImage.cc | 256 +++++++++- iipsrv/src/IIPImage.h | 2 +- iipsrv/src/Makefile.am | 20 +- 13 files changed, 1359 insertions(+), 339 deletions(-) create mode 100644 iipsrv/src/BioFormatsImage.h create mode 100644 iipsrv/src/BioFormatsInstance.cc create mode 100644 iipsrv/src/BioFormatsInstance.h create mode 100644 iipsrv/src/BioFormatsManager.cc create mode 100644 iipsrv/src/BioFormatsManager.h create mode 100644 iipsrv/src/BioFormatsThread.cc create mode 100644 iipsrv/src/BioFormatsThread.h diff --git a/README.md b/README.md index 6f93c40..2c3ef22 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Containerized IIP ## building and running +Unless using caMicroscope Distro Docker, [BFBridge](https://github.com/camicroscope/BFBridge) needs to be cloned and placed next to iipsrv/, so that this project's root iipimage/ has subfolders iipimage/ and BFBridge/ + docker build . -t iipsrv docker run iipsrv -d -p 4010:80 diff --git a/iipsrv/src/BioFormatsImage.cc b/iipsrv/src/BioFormatsImage.cc index 2e947d3..74a261e 100644 --- a/iipsrv/src/BioFormatsImage.cc +++ b/iipsrv/src/BioFormatsImage.cc @@ -1,7 +1,5 @@ -#include "OpenSlideImage.h" +#include "BioFormatsImage.h" #include "Timer.h" -#include -#include #include #include @@ -14,8 +12,7 @@ using namespace std; extern std::ofstream logfile; -/// Overloaded function for opening a TIFF image -void OpenSlideImage::openImage() throw(file_error) +void BioFormatsImage::openImage() throw(file_error) { string filename = getFileName(currentX, currentY); @@ -28,42 +25,32 @@ void OpenSlideImage::openImage() throw(file_error) Timer timer; timer.start(); - logfile << "OpenSlide :: openImage() :: start" << endl + logfile << "BioFormats :: openImage() :: start" << endl << flush; #endif // close previous closeImage(); - osr = openslide_open(filename.c_str()); + int code = bfi.open(filename); - const char *error = openslide_get_error(osr); -#ifdef DEBUG_OSI - logfile << "OpenSlide :: openImage() get error :: completed " << filename << endl - << flush; -#endif - - if (error) + if (code < 0) { - logfile << "ERROR: encountered error: " << error << " while opening " << filename << " with OpenSlide: " << endl + string error = bfi.get_error(); + + logfile << "ERROR: encountered error: " << error << " while opening " << filename << " with BioFormats: " << endl << flush; throw file_error(string("Error opening '" + filename + "' with OpenSlide, error " + error)); } #ifdef DEBUG_OSI - logfile << "OpenSlide :: openImage() :: " << timer.getTime() << " microseconds" << endl + logfile << "BioFormats :: openImage() :: " << timer.getTime() << " microseconds" << endl << flush; #endif #ifdef DEBUG_OSI - logfile << "OpenSlide :: openImage() :: completed " << filename << endl + logfile << "BioFormats :: openImage() :: completed " << filename << endl << flush; #endif - if (osr == NULL) - { - logfile << "ERROR: can't open " << filename << " with OpenSlide" << endl - << flush; - throw file_error(string("Error opening '" + filename + "' with OpenSlide")); - } if (bpc == 0) { @@ -71,138 +58,208 @@ void OpenSlideImage::openImage() throw(file_error) } isSet = true; - // } else { - // #ifdef DEBUG_OSI - // logfile << "OpenSlide :: openImage() :: not newer. reuse openslide object." << endl << flush; - // #endif - // } +} + +// get the power of the largest power of 2 smaller than the number +// 1027 -> 1024 -> returns 10 +// This function is defined for a >= 1 +static unsigned int getPowerOfTwoRoundDown(unsigned int a) +{ +#if (defined(__GNUC__) && __GNUC__ > 4) || (defined(__clang__) && __clang_major__ > 6) + // total bits minus leading zeros minus the largest bit + return sizeof(unsigned int) * 8 - __builtin_clz(a) - 1; +#else + unsigned int x = 0; + while (a >> 1) + { + x++; + } + return x - 1; +#endif } /// given an open OSI file, get information from the image. -void OpenSlideImage::loadImageInfo(int x, int y) throw(file_error) +void BioFormatsImage::loadImageInfo(int x, int y) throw(file_error) { #ifdef DEBUG_OSI - logfile << "OpenSlideImage :: loadImageInfo()" << endl; + logfile << "BioFormatsImage :: loadImageInfo()" << endl; - if (!osr) - { - logfile << "loadImageInfo called before openImage()" << endl; - } #endif - int64_t w = 0, h = 0; + int w = 0, h = 0; currentX = x; currentY = y; - const char *error; - -#ifdef DEBUG_OSI - const char *const *prop_names = openslide_get_property_names(osr); - error = openslide_get_error(osr); - if (error) + // choose power of 2 to make downsample simpler. + int suggested_width = bfi.get_optimal_tile_width(); + if (suggested_width > 0) { - logfile << "ERROR: encountered error: " << error << " while getting property names: " << error << endl; + suggested_width = 1 << getPowerOfTwoRoundDown(suggested_width); } - - int i = 0; - - logfile << "Properties:" << endl; - while (prop_names[i]) + if (suggested_width > 2048 || suggested_width < 128) { - logfile << "" << i << " : " << prop_names[i] << " = " << openslide_get_property_value(osr, prop_names[i]) << endl; - error = openslide_get_error(osr); - if (error) - { - logfile << "ERROR: encountered error: " << error << " while getting property: " << error << endl; - } - - ++i; + tile_width = 256; + } + else + { + tile_width = suggested_width; } -#endif - const char *prop_val; - // // TODO: use actual tile width. default is 256 - // prop_val = openslide_get_property_value(osr, "openslide.level[0].tile-width"); - // error = openslide_get_error(osr); - // if (error) { - // logfile << "ERROR: encountered error: " << error << " while getting property: " << error << endl; - // } - // - // if (prop_val) tile_width = atoi(prop_val); - // else tile_width = 256; - tile_width = 256; // default to power of 2 to make downsample simpler. + int suggested_height = bfi.get_optimal_tile_height(); + if (suggested_height > 0) + { + suggested_height = 1 << getPowerOfTwoRoundDown(suggested_height); + } + if (suggested_height > 2048 || suggested_height < 128) + { + tile_height = 256; + } + else + { + tile_height = suggested_height; + } - // prop_val = openslide_get_property_value(osr, "openslide.level[0].tile-height"); - // error = openslide_get_error(osr); - // if (error) { - // logfile << "ERROR: encountered error: " << error << " while getting property: " << error << endl; - // } - // if (prop_val) tile_height = atoi(prop_val); - // else tile_height = 256; - tile_height = 256; // default to power of 2 to make downsample simpler. + w = bfi.get_size_x(); + h = bfi.get_size_y(); - openslide_get_level0_dimensions(osr, &w, &h); - error = openslide_get_error(osr); - if (error) + if (w < 0 || h < 0) { - logfile << "ERROR: encountered error: " << error << " while getting level0 dim: " << error << endl; + string err = bfi.get_error(); + + logfile << "ERROR: encountered error: " << err << " while getting level0 dim" << endl; + throw file_error("Getting bioformats level0 dimensions: " + err); } #ifdef DEBUG_OSI logfile << "dimensions :" << w << " x " << h << endl; // logfile << "comment : " << comment << endl; + + // PLEASE NOTE: these can differ between resolution levels + cerr << "Parsing details" << endl; + cerr << "Optimal: " << tile_width << " " << tile_height << endl; + cerr << "rgbChannelCount: " << bfi.get_rgb_channel_count() << endl; // Number of colors returned with each openbytes call + cerr << "sizeC: " << bfi.get_size_c() << endl; + cerr << "effectiveSizeC: " << bfi.get_effective_size_c() << endl; // colors on separate planes. 1 if all on same plane + cerr << "sizeZ: " << bfi.get_size_z() << endl; + cerr << "sizeT: " << bfi.get_size_t() << endl; + cerr << "ImageCount: " << bfi.get_image_count() << endl; // number of planes in series + cerr << "isRGB: " << (int)bfi.is_rgb() << endl; // multiple colors per openbytes plane + cerr << "isInterleaved: " << (int)bfi.is_interleaved() << endl; + cerr << "isInterleaved: " << (int)bfi.is_interleaved() << endl; #endif - // TODO Openslide outputs 8 bit ABGR always. + // iipsrv takes 1 or 3 only. tell iip that we'll give it a 3-channel image channels = 3; + + // Note: this code assumes that the number of channels is the same among resolutions + // otherwise should be moved to getnativetile + channels_internal = bfi.get_rgb_channel_count(); + if (channels_internal != 3 && channels_internal != 4) + { + if (channels_internal > 0) + { + logfile << "Unimplemented: only support 3, 4 channels, not " << channels_internal << endl; + throw file_error("Unimplemented: only support 3, 4 channels, not " + std::to_string(channels_internal)); + } + else + { + string err = bfi.get_error(); + logfile << "Error while getting channel count: " << err << endl; + throw file_error("Error while getting channel count: " + err); + } + } + + if (bfi.get_effective_size_c() != 1) + { + // We need to find an example file to learn how to parse such files + logfile << "Unimplemented: get_effective_size_c is not one but " << bfi.get_effective_size_c() << endl; + throw file_error("Unimplemented: get_effective_size_c is not one but " + std::to_string(bfi.get_effective_size_c())); + } + + if (bfi.is_indexed_color() && !bfi.is_false_color()) + { + // We must read from the table + + /*To implement this, get8BitLookupTable() or get16BitLookupTable() + and then read from there.*/ + logfile << "Unimplemented: False color image" << endl; + throw file_error("Unimplemented: False color image"); + } + + if (bfi.get_dimension_order().length() && bfi.get_dimension_order()[2] != 'C') + { + logfile << "Unimplemented: unfamiliar dimension order " << bfi.get_dimension_order() << endl; + throw file_error("Unimplemented: unfamiliar dimension order " + std::string(bfi.get_dimension_order())); + } + + // bfi.get_bytes_per_pixel actually gives bits per channel per pixel, so don't divide by channels + int bytespc_internal = bfi.get_bytes_per_pixel(); bpc = 8; colourspace = sRGB; - // const char* comment = openslide_get_comment(osr); + if (bytespc_internal <= 0) + { + string err = bfi.get_error(); + logfile << "Error while getting bits per pixel: " << err << endl; + throw file_error("Error while getting bits per pixel: " + err); + } + +#define too_big (tile_width * tile_height * bytespc_internal * bfi.get_rgb_channel_count() > bfi_communication_buffer_len) + while (too_big) + { + tile_height >>= 1; + if (!too_big) + break; + tile_width >>= 1; + } +#undef too_big // save the openslide dimensions. - std::vector openslide_widths, openslide_heights; - openslide_widths.clear(); - openslide_heights.clear(); + std::vector bioformats_widths, bioformats_heights; + bioformats_widths.clear(); + bioformats_heights.clear(); + + int bioformats_levels = bfi.get_resolution_count(); - int32_t openslide_levels = openslide_get_level_count(osr); - error = openslide_get_error(osr); - if (error) + if (bioformats_levels <= 0) { - logfile << "ERROR: encountered error: " << error << " while getting level count: " << error << endl; + string err = bfi.get_error(); + logfile << "ERROR: encountered error: " << err << " while getting level count" << endl; + throw file_error("ERROR: encountered error: " + err + " while getting level count"); } #ifdef DEBUG_OSI - logfile << "number of levels = " << openslide_levels << endl; + logfile << "number of levels = " << bioformats_levels << endl; double tempdownsample; #endif - int64_t ww, hh; - for (int32_t i = 0; i < openslide_levels; i++) + + int ww, hh; + for (int i = 0; i < bioformats_levels; i++) { - openslide_get_level_dimensions(osr, i, &ww, &hh); - error = openslide_get_error(osr); - if (error) - { - logfile << "ERROR: encountered error: " << error << " while getting level dims: " << error << endl; - } + bfi.set_current_resolution(i); - openslide_widths.push_back(ww); - openslide_heights.push_back(hh); -#ifdef DEBUG_OSI - tempdownsample = openslide_get_level_downsample(osr, i); - error = openslide_get_error(osr); - if (error) + ww = bfi.get_size_x(); + hh = bfi.get_size_y(); + +#ifdef DEBUG_VERBOSE + fprintf(stderr, "resolution %d has x=%d y=%d", i, ww, hh); +#endif + if (ww <= 0 || hh <= 0) { - logfile << "ERROR: encountered error: " << error << " while getting level downsamples: " << error << endl; + logfile << "ERROR: encountered error: while getting level dims for level " << i << endl; + throw file_error("error while getting level dims for level " + i); } - + bioformats_widths.push_back(ww); + bioformats_heights.push_back(hh); +#ifdef DEBUG_OSI + tempdownsample = ((double)(w) / ww + (double)(h) / hh) / 2; logfile << "\tlevel " << i << "\t(w,h) = (" << ww << "," << hh << ")\tdownsample=" << tempdownsample << endl; #endif } - openslide_widths.push_back(0); - openslide_heights.push_back(0); + bioformats_widths.push_back(0); + bioformats_heights.push_back(0); image_widths.clear(); image_heights.clear(); @@ -214,45 +271,54 @@ void OpenSlideImage::loadImageInfo(int x, int y) throw(file_error) numTilesX.clear(); numTilesY.clear(); - openslide_level_to_use.clear(); - openslide_downsample_in_level.clear(); - unsigned int os_level = 0; - unsigned int os_downsample_in_level = 1; + bioformats_level_to_use.clear(); + bioformats_downsample_in_level.clear(); + unsigned int bf_level = 0; // which layer bioformats provides us + unsigned int bf_downsample_in_level = 1; // how much to scale internally that layer // store the original size. image_widths.push_back(w); image_heights.push_back(h); lastTileXDim.push_back(w % tile_width); lastTileYDim.push_back(h % tile_height); + // As far as I understand, this arithmetic means that + // last remainder has at least 0, at most n-1 columns/rows + // where n is tile_width or tile_height: numTilesX.push_back((w + tile_width - 1) / tile_width); numTilesY.push_back((h + tile_height - 1) / tile_height); - openslide_level_to_use.push_back(os_level); - openslide_downsample_in_level.push_back(os_downsample_in_level); + bioformats_level_to_use.push_back(bf_level); + bioformats_downsample_in_level.push_back(bf_downsample_in_level); - // what if there are openslide levels with dim smaller than this? + // what if there are ~~openslide~~ bioformats levels with dim smaller than this? // populate at 1/2 size steps - while ((w > tile_width) || (h > tile_height)) + while ((w > 256) || (h > 256)) { - // need a level that's has image completely inside 1 tile. + // need a level that has image completely inside 1 tile. // (stop with both w and h less than tile_w/h, previous iteration divided by 2. w >>= 1; // divide by 2 and floor. losing 1 pixel from higher res. h >>= 1; - // compare to next level width and height. if smaller, next level is better for supporting the w/h + // compare to next level width and height. if the calculated res is smaller, next level is better for supporting the w/h // so switch level. - if (w <= openslide_widths[os_level + 1] && h <= openslide_heights[os_level + 1]) + if (w <= bioformats_widths[bf_level + 1] && h <= bioformats_heights[bf_level + 1]) { - ++os_level; - os_downsample_in_level = 1; // just went to next smaller level, don't downsample internally yet. + ++bf_level; + bf_downsample_in_level = 1; // just went to next smaller level, don't downsample internally yet. + + // Handle duplicate levels + while (w <= bioformats_widths[bf_level + 1] && h <= bioformats_heights[bf_level + 1]) + { + ++bf_level; + } } else { - os_downsample_in_level <<= 1; // next one, downsample internally by 2. + bf_downsample_in_level <<= 1; // next one, downsample internally by 2. } - openslide_level_to_use.push_back(os_level); - openslide_downsample_in_level.push_back(os_downsample_in_level); + bioformats_level_to_use.push_back(bf_level); + bioformats_downsample_in_level.push_back(bf_downsample_in_level); image_widths.push_back(w); image_heights.push_back(h); @@ -262,6 +328,16 @@ void OpenSlideImage::loadImageInfo(int x, int y) throw(file_error) numTilesY.push_back((h + tile_height - 1) / tile_height); #ifdef DEBUG_OSI + cerr << "downsamplein levels:" << endl; + for (auto i : bioformats_downsample_in_level) + cerr << i << " "; + cerr << "\n"; + + cerr << "numtilex:" << endl; + for (auto i : numTilesX) + cerr << i << " "; + cerr << "\n"; + logfile << "Create virtual layer : " << w << "x" << h << std::endl; #endif } @@ -272,7 +348,7 @@ void OpenSlideImage::loadImageInfo(int x, int y) throw(file_error) logfile << "virtual level " << t << " (w,h)=(" << image_widths[t] << "," << image_heights[t] << "),"; logfile << " (last_tw,last_th)=(" << lastTileXDim[t] << "," << lastTileYDim[t] << "),"; logfile << " (ntx,nty)=(" << numTilesX[t] << "," << numTilesY[t] << "),"; - logfile << " os level=" << openslide_level_to_use[t] << " downsample from os_level=" << openslide_downsample_in_level[t] << endl; + logfile << " os level=" << bioformats_level_to_use[t] << " downsample from bf_level=" << bioformats_downsample_in_level[t] << endl; } #endif @@ -280,120 +356,24 @@ void OpenSlideImage::loadImageInfo(int x, int y) throw(file_error) // only support bpp of 8 (255 max), and 3 channels min.assign(channels, 0.0f); - max.assign(channels, 255.0f); + max.assign(channels, (float)(1 << bpc) - 1.0f); } -/// Overloaded function for closing a TIFF image -void OpenSlideImage::closeImage() +void BioFormatsImage::closeImage() { #ifdef DEBUG_OSI Timer timer; timer.start(); #endif - if (osr != NULL) - { - openslide_close(osr); - osr = NULL; - } + bfi.close(); #ifdef DEBUG_OSI - logfile << "OpenSlide :: closeImage() :: " << timer.getTime() << " microseconds" << endl; + logfile + << "BioFormats :: closeImage() :: " << timer.getTime() << " microseconds" << endl; #endif } -// -//// TCP: support get region (internally, already doing it. -///// Overloaded function for returning a region for a given angle and resolution -///** Return a RawTile object: Overloaded by child class. -// \param ha horizontal angle -// \param va vertical angle -// \param res resolution -// \param layers number of quality layers to decode -// \param x x coordinate at resolution r -// \param y y coordinate at resolution r -// \param w width of region at resolution r -// \param h height of region at resolution r -// */ -// RawTile OpenSlideImage::getRegion( int ha, int va, unsigned int res, int layers, int x, int y, unsigned int w, unsigned int h ) { -// // ignore ha, va, layers. important things are r, x, y, w, and h. -// -// -// #ifdef DEBUG_OSI -// Timer timer; -// timer.start(); -// #endif -// -// if (res > (numResolutions-1)) { -// ostringstream tile_no; -// tile_no << "OpenSlide :: Asked for non-existant resolution: " << res; -// throw tile_no.str(); -// return 0; -// } -// -// // convert r to zoom -// -// // res is specified in opposite order from image levels: image level 0 has highest res, -// // image level nRes-1 has res of 0. -// uint32_t openslide_zoom = this->numResolutions - 1 - res; -// -// #ifdef DEBUG_OSI -// logfile << "OpenSlide :: getRegion() :: res=" << res << " pos " << x << "x" << y << " size " << w << "x" << h << " is_zoom= " << openslide_zoom << endl; -// -// #endif -// -// -// -// uint32_t lw, lh; -// int32_t lx, ly; -// -// unsigned int res_width = image_widths[openslide_zoom]; -// unsigned int res_height = image_heights[openslide_zoom]; -// -// // bound the x,y position -// lx = std::max(0, x); lx = std::min(lx, static_cast(res_width)); -// ly = std::max(0, y); ly = std::min(ly, static_cast(res_height)); -// -// // next bound the w and height. -// lw = std::min(w, res_width - lx); -// lh = std::min(h, res_height - ly); -// -// -// -// // convert x ,y, to the right coordinate system -// if (lw == 0 || lh == 0) { -// ostringstream tile_no; -// tile_no << "OpenSlideImage :: Asked for zero-sized region " << x << "x" << y << ", size " << w << "x" << h << ", res dim=" << res_width << "x" << res_height; -// throw tile_no.str(); -// } -// -// // Create our raw tile buffer and initialize some values -// RawTile region(0, res, ha, va, w, h, channels, bpp); -// region.dataLength = w * h * channels * sizeof(unsigned char); -// region.filename = getImagePath(); -// region.timestamp = timestamp; -// // new a block that is larger for openslide library to directly copy in. -// // then shuffle from BGRA to RGB. relying on delete [] to do the right thing. -// region.data = new unsigned char[w * h * 4 * sizeof(unsigned char)]; // TODO: avoid reallocation? -// region.memoryManaged = 1; // allocated data, so use this flag to indicate that it needs to be cleared on destruction -// //rawtile->padded = false; -// #ifdef DEBUG_OSI -// logfile << "Allocating tw * th * channels * sizeof(char) : " << w << " * " << h << " * " << channels << " * sizeof(char) " << endl << flush; -// #endif -// -// // call read -// read(openslide_zoom, w, h, x, y, region.data); -// -// -// #ifdef DEBUG_OSI -// logfile << "OpenSlide :: getRegion() :: " << timer.getTime() << " microseconds" << endl << flush; -// logfile << "REGION RENDERED" << std::endl; -// #endif -// -// return ( region ); -// -//} - /// Overloaded function for getting a particular tile /** \param x horizontal sequence angle (for microscopy, ignored.) \param y vertical sequence angle (for microscopy, ignored.) @@ -401,7 +381,7 @@ void OpenSlideImage::closeImage() \param l number of quality layers to decode - for jpeg2000 \param t tile number (within the resolution level.) specified as a sequential number = y * width + x; */ -RawTilePtr OpenSlideImage::getTile(int seq, int ang, unsigned int iipres, int layers, unsigned int tile) throw(file_error) +RawTilePtr BioFormatsImage::getTile(int seq, int ang, unsigned int iipres, int layers, unsigned int tile) throw(file_error) { #ifdef DEBUG_OSI @@ -412,7 +392,7 @@ RawTilePtr OpenSlideImage::getTile(int seq, int ang, unsigned int iipres, int la if (iipres > (numResolutions - 1)) { ostringstream tile_no; - tile_no << "OpenSlide :: Asked for non-existant resolution: " << iipres; + tile_no << "BioFormats :: Asked for non-existant resolution: " << iipres; throw file_error(tile_no.str()); return 0; } @@ -422,16 +402,21 @@ RawTilePtr OpenSlideImage::getTile(int seq, int ang, unsigned int iipres, int la uint32_t osi_level = numResolutions - 1 - iipres; #ifdef DEBUG_OSI - logfile << "OpenSlide :: getTile() :: res=" << iipres << " tile= " << tile << " is_zoom= " << osi_level << endl; - + logfile << "BioFormats :: getTile() :: res=" << iipres << " tile= " << tile << " is_zoom= " << osi_level << endl; #endif //======= get the dimensions in pixels and num tiles for the current resolution - - // int64_t layer_width = image_widths[openslide_zoom]; - // int64_t layer_height = image_heights[openslide_zoom]; - // openslide_get_layer_dimensions(osr, layers, &layer_width, &layer_height); - + /* + int64_t layer_width = 0; + int64_t layer_height = 0; + bfi.set_current_resolution(osi_level); + layer_width = bfi.get_size_x(); + layer_height = bfi.get_size_y(); + + #ifdef DEBUG_VERBOSE + fprintf(stderr, "layer: %d layer_width: %d layer_height: %d", layers, layer_width, layer_height); + #endif + */ // Calculate the number of tiles in each direction size_t ntlx = numTilesX[osi_level]; size_t ntly = numTilesY[osi_level]; @@ -439,7 +424,7 @@ RawTilePtr OpenSlideImage::getTile(int seq, int ang, unsigned int iipres, int la if (tile >= ntlx * ntly) { ostringstream tile_no; - tile_no << "OpenSlideImage :: Asked for non-existant tile: " << tile; + tile_no << "BioFormatsImage :: Asked for non-existant tile: " << tile; throw file_error(tile_no.str()); } @@ -450,7 +435,7 @@ RawTilePtr OpenSlideImage::getTile(int seq, int ang, unsigned int iipres, int la RawTilePtr ttt = getCachedTile(tx, ty, iipres); #ifdef DEBUG_OSI - logfile << "OpenSlide :: getTile() :: total " << timer.getTime() << " microseconds" << endl + logfile << "BioFormats :: getTile() :: total " << timer.getTime() << " microseconds" << endl << flush; logfile << "TILE RENDERED" << std::endl; #endif @@ -465,7 +450,7 @@ RawTilePtr OpenSlideImage::getTile(int seq, int ang, unsigned int iipres, int la * else call halfsampleAndComposeTile * @param res iipsrv's resolution id. openslide's level is inverted from this. */ -RawTilePtr OpenSlideImage::getCachedTile(const size_t tilex, const size_t tiley, const uint32_t iipres) +RawTilePtr BioFormatsImage::getCachedTile(const size_t tilex, const size_t tiley, const uint32_t iipres) { #ifdef DEBUG_OSI @@ -484,7 +469,7 @@ RawTilePtr OpenSlideImage::getCachedTile(const size_t tilex, const size_t tiley, if (ttt) { #ifdef DEBUG_OSI - logfile << "OpenSlide :: getCachedTile() :: Cache Hit " << tilex << "x" << tiley << "@" << iipres << " osi tile bounds: " << numTilesX[osi_level] << "x" << numTilesY[osi_level] << " " << timer.getTime() << " microseconds" << endl + logfile << "BioFormats :: getCachedTile() :: Cache Hit " << tilex << "x" << tiley << "@" << iipres << " osi tile bounds: " << numTilesX[osi_level] << "x" << numTilesY[osi_level] << " " << timer.getTime() << " microseconds" << endl << flush; #endif @@ -492,12 +477,12 @@ RawTilePtr OpenSlideImage::getCachedTile(const size_t tilex, const size_t tiley, } // else caches does not have it. #ifdef DEBUG_OSI - logfile << "OpenSlide :: getCachedTile() :: Cache Miss " << tilex << "x" << tiley << "@" << iipres << " osi tile bounds: " << numTilesX[osi_level] << "x" << numTilesY[osi_level] << " " << timer.getTime() << " microseconds" << endl + logfile << "BioFormats :: getCachedTile() :: Cache Miss " << tilex << "x" << tiley << "@" << iipres << " osi tile bounds: " << numTilesX[osi_level] << "x" << numTilesY[osi_level] << " " << timer.getTime() << " microseconds" << endl << flush; #endif // is this a native layer? - if (openslide_downsample_in_level[osi_level] == 1) + if (bioformats_downsample_in_level[osi_level] == 1) { // supported by native openslide layer // tile manager will cache if needed @@ -512,12 +497,13 @@ RawTilePtr OpenSlideImage::getCachedTile(const size_t tilex, const size_t tiley, } } +#pragma GCC optimize("O3") /** * read from file, color convert, store in cache, and return tile. * * @param res iipsrv's resolution id. openslide's level is inverted from this. */ -RawTilePtr OpenSlideImage::getNativeTile(const size_t tilex, const size_t tiley, const uint32_t iipres) +RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley, const uint32_t iipres) { #ifdef DEBUG_OSI @@ -525,17 +511,13 @@ RawTilePtr OpenSlideImage::getNativeTile(const size_t tilex, const size_t tiley, timer.start(); #endif - if (!osr) - { - logfile << "openslide image not yet loaded " << endl; - } // compute the parameters (i.e. x and y offsets, w/h, and bestlayer to use. uint32_t osi_level = numResolutions - 1 - iipres; // find the next layer to downsample to desired zoom level z // - uint32_t bestLayer = openslide_level_to_use[osi_level]; + uint32_t bestLayer = bioformats_level_to_use[osi_level]; size_t ntlx = numTilesX[osi_level]; size_t ntly = numTilesY[osi_level]; @@ -559,58 +541,303 @@ RawTilePtr OpenSlideImage::getNativeTile(const size_t tilex, const size_t tiley, th = rem_y; } + /*if (tilex > ntlx || tiley > ntly) + { + cerr << "Inexistant tile!"; + throw file_error("inexistant"); + }*/ + // create the RawTile object - RawTilePtr rt(new RawTile(tiley * ntlx + tilex, iipres, 0, 0, tw, th, channels, bpc)); + RawTilePtr rt(new RawTile(tiley * ntlx + tilex, iipres, 0, 0, tw, th, 3, bpc)); // compute the size, etc - rt->dataLength = tw * th * channels * sizeof(unsigned char); + rt->dataLength = tw * th * 3 * sizeof(unsigned char); rt->filename = getImagePath(); rt->timestamp = timestamp; - // new a block that is larger for openslide library to directly copy in. - // then shuffle from BGRA to RGB. relying on delete [] to do the right thing. - rt->data = new unsigned char[tw * th * 4 * sizeof(unsigned char)]; + int allocate_length = rt->dataLength; + + if (bfi.set_current_resolution(bestLayer) < 0) + { + auto s = string("FATAL : bad resolution: " + std::to_string(bestLayer) + " rather than up to " + std::to_string(bfi.get_resolution_count() - 1)); + logfile << s; + throw file_error(s); + } + + // Note: Pixel formats are either the same for every resolution (see: channels_internal) + // or can differ between resolutions (see: should_interleave). + // Assuming the former saves lots of time. The latter must be called after set_current_resolution. + // 1 JNI call is less than 1ms according to https://stackoverflow.com/a/36141175 + char should_reduce_channels_from_4to3 = 0; + + // uncached: + /*int channels = bfi.get_rgb_channel_count(); + if (channels != 3 && channels != 4) + { + throw file_error("Channels not 3 or 4: " + std::to_string(channels)); + } + if (channels == 4) + { + should_reduce_channels_from_4to3 = 1; + allocate_length = tw * th * 4 * sizeof(unsigned char); + }*/ + + // cached: + should_reduce_channels_from_4to3 = channels_internal == 4; + + // Known to differ among resolutions + int should_interleave = !bfi.is_interleaved(); + + // Perhaps the next three can be cached + // https://github.com/ome/bioformats/blob/metadata54/components/formats-api/src/loci/formats/FormatTools.java#L76 + int pixel_type = bfi.get_pixel_type(); + int bytespc_internal = bfi.get_bytes_per_pixel(); + + int should_remove_sign = 1; + // added float and double for exclusion, because they are handled specially + if (pixel_type == 1 || pixel_type == 3 || pixel_type == 5 || pixel_type == 6 || pixel_type == 7 || pixel_type == 8) + { + should_remove_sign = 0; + } + + int should_convert_from_float = pixel_type == 6; + int should_convert_from_double = pixel_type == 7; + int should_convert_from_bit = pixel_type == 8; + + // new a block ... + // relying on delete [] to do the right thing. + rt->data = new unsigned char[allocate_length]; rt->memoryManaged = 1; // allocated data, so use this flag to indicate that it needs to be cleared on destruction // rawtile->padded = false; #ifdef DEBUG_OSI - logfile << "Allocating tw * th * channels * sizeof(char) : " << tw << " * " << th << " * " << 4 << " * sizeof(char) " << endl + logfile << "Allocating tw * th * channels * sizeof(char) : " << tw << " * " << th << " * " << channels << " * sizeof(char) " << endl << flush; + + cerr << "Parsing details FOR TILE" << endl; + cerr << "Optimal: " << tile_width << " " << tile_height << endl; + cerr << "rgbChannelCount: " << bfi.get_rgb_channel_count() << endl; // Number of colors returned with each openbytes call + cerr << "sizeC: " << bfi.get_size_c() << endl; + cerr << "effectiveSizeC: " << bfi.get_effective_size_c() << endl; // colors on separate planes. 1 if all on same plane + cerr << "sizeZ: " << bfi.get_size_z() << endl; + cerr << "sizeT: " << bfi.get_size_t() << endl; + cerr << "ImageCount: " << bfi.get_image_count() << endl; // number of planes in series + cerr << "isRGB: " << (int)bfi.is_rgb() << endl; // multiple colors per openbytes plane + cerr << "isInterleaved: " << (int)bfi.is_interleaved() << endl; + cerr << "isInterleaved: " << (int)bfi.is_interleaved() << endl; #endif // READ FROM file //======= next compute the x and y coordinates (top left corner) in level 0 coordinates - //======= expected by openslide_read_region. - size_t tx0 = (tilex * tile_width) << osi_level; // same as multiply by z power of 2 - size_t ty0 = (tiley * tile_height) << osi_level; - - openslide_read_region(osr, reinterpret_cast(rt->data), tx0, ty0, bestLayer, tw, th); - const char *error = openslide_get_error(osr); - if (error) - { - logfile << "ERROR: encountered error: " << error << " while reading region exact at " << tx0 << "x" << ty0 << " dim " << tw << "x" << th << " with OpenSlide: " << error << endl; - } + //======= expected by bfi.open_bytes. + int tx0 = tilex * tile_width; + int ty0 = tiley * tile_height; #ifdef DEBUG_OSI - logfile << "OpenSlide :: getNativeTile() :: read_region() :: " << tilex << "x" << tiley << "@" << iipres << " " << timer.getTime() << " microseconds" << endl - << flush; + cerr << "bfi.open_bytes params: " << bestLayer << " " << tx0 << " " << ty0 << " " << tw << " " << th << std::endl; + cerr << "downsample in level " << bioformats_downsample_in_level[osi_level] << endl; + + cerr << "this layer has resolution x=" << bfi.get_size_x() << " y=" << bfi.get_size_y() << endl; #endif if (!rt->data) - throw string("FATAL : OpenSlideImage read_region => allocation memory ERROR"); + throw file_error(string("FATAL : BioFormatsImage read_region => allocation memory ERROR")); #ifdef DEBUG_OSI - timer.start(); + auto start = std::chrono::high_resolution_clock::now(); #endif - - // COLOR CONVERT in place BGRA->RGB conversion - this->bgra2rgb(reinterpret_cast(rt->data), tw, th); + int bytes_received = bfi.open_bytes(tx0, ty0, tw, th); #ifdef DEBUG_OSI - logfile << "OpenSlide :: getNativeTile() :: bgra2rgb() :: " << timer.getTime() << " microseconds" << endl - << flush; + auto finish = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = finish - start; + milliseconds += elapsed.count(); + cerr << "Milliseconds: " << milliseconds << endl; +#endif + + if (bytes_received < 0) + { + string error = bfi.get_error(); + logfile << "ERROR: encountered error: " << error << " while reading region exact at " << tx0 << "x" << ty0 << " dim " << tw << "x" << th << " with BioFormats: " << error << endl; + throw file_error("ERROR: encountered error: " + error + " while reading region exact at " + std::to_string(tx0) + "x" + std::to_string(ty0) + " dim " + std::to_string(tw) + "x" + std::to_string(th) + " with BioFormats: " + error); + } + + if (bytes_received != channels_internal * bytespc_internal * tw * th) + { + cerr << "got an unexpected number of bytes\n"; + throw file_error("ERROR: expected len " + std::to_string(channels * bpc / 8 * tw * th) + " but got " + std::to_string(bytes_received)); + } + // Note: please don't copy anything more than + // bytes_received when it's positive as the rest contains junk from the past + + /* + Summary of next lines: + + var signed = ... + var bit = ... + + if (float) { + bswap if needed (reinterpret cast) + reinterpret same space, now fill with cast to int after scale 0 to 1 + signed = false + } else if (double) { + … + } + + // int cases + if (internalbpc != 8) { + + // truncate to 8 bits + // move to be consecutive bytes, bpc = 8 + + } else if (bit) { + scale + copy to three channels + } + + if (interleave) { + interleave, discard alpha + } else { + copy, either skip alpha or not + ] + + if (signed) { + read as signed, add -int_min, read as unsigned + } + + */ + + unsigned char *data_out = (unsigned char *)rt->data; + int pixels = rt->width * rt->height; + + // Truncate to 8 bits + // 1 for pick last byte, 0 for pick first byte. + // if data in le -> pick last, data be -> pick first. + // but there are two branches - if we cast from double/float + // the data's endianness depends on platform, otherwise + // on file format, so from bfi.is_little_endian + int pick_byte; + unsigned char *buf = (unsigned char *)bfi.communication_buffer(); + +// The common case for float and double branches, change later otherwise +#if !defined(__BYTE_ORDER) || __BYTE_ORDER == __LITTLE_ENDIAN + pick_byte = 1; +#else + pick_byte = 0; #endif + if (should_convert_from_float) + { + if (bfi.is_little_endian() != pick_byte) + { + unsigned int *buf_as_int = (unsigned int *)buf; + for (int i = 0; i < pixels * channels_internal; i++) + { + buf_as_int[i] = ((buf_as_int[i] & 0xFF000000) >> 24) | ((buf_as_int[i] & 0xFF0000) >> 8) | ((buf_as_int[i] & 0xFF00) << 8) | (buf_as_int[i] & 0xFF) << 24; + } + } + + float *buf_as_float = (float *)buf; + + for (int i = 0; i < pixels * channels_internal; i++) + { + buf[i] = (unsigned char)(buf_as_float[i] * 255.0); + } + bytespc_internal = 1; + } + else if (should_convert_from_double) + { + if (bfi.is_little_endian() != pick_byte) + { + unsigned long int *buf_as_long_int = (unsigned long int *)buf; + for (int i = 0; i < pixels * channels_internal; i++) + { + buf_as_long_int[i] = ((buf_as_long_int[i] >> 56) & 0xFFlu) | ((buf_as_long_int[i] >> 40) & 0xFF00lu) | ((buf_as_long_int[i] >> 24) & 0xFF0000lu) | ((buf_as_long_int[i] >> 8) & 0xFF000000lu) | ((buf_as_long_int[i] << 8) & 0xFF00000000lu) | ((buf_as_long_int[i] << 24) & 0xFF0000000000lu) | ((buf_as_long_int[i] << 40) & 0xFF000000000000lu) | ((buf_as_long_int[i] << 56) & 0xFF00000000000000lu); + } + } + + double *buf_as_double = (double *)buf; + + for (int i = 0; i < pixels * channels_internal; i++) + { + // Is it faster to cast to float then multiply or multiply directly? + buf[i] = (unsigned char)((float)(buf_as_double[i]) * 255.0); + } + bytespc_internal = 1; + } + + // int cases + if (bytespc_internal != 1) + { + pick_byte = bfi.is_little_endian(); + + int coefficient = bytespc_internal; + int offset = pick_byte ? (coefficient - 1) : 0; + for (int i = 0; i < pixels * channels_internal; i++) + { + // Unnecessary copy rather than considering these offset and coefficient + // variables when we'll already copy, but this allows readability + // and not making the common 8-bit reading slower + buf[i] = buf[coefficient * i + offset]; + } + + bytespc_internal = 1; + } + else if (should_convert_from_bit) + { + // TODO: maybe allow single channel bitmaps by repeating every array element thrice + for (int i = 0; i < pixels * channels_internal; i++) + { + // 0 -> 0 + // 1 -> 255 + buf[i] = (0 - buf[i]); + } + } + + if (should_interleave) + { + char *red = bfi.communication_buffer(); + char *green = &bfi.communication_buffer()[pixels]; + char *blue = &bfi.communication_buffer()[2 * pixels]; + + for (int i = 0; i < pixels; i++) + { + data_out[3 * i] = red[i]; + } + for (int i = 0; i < pixels; i++) + { + data_out[3 * i + 1] = green[i]; + } + for (int i = 0; i < pixels; i++) + { + data_out[3 * i + 2] = blue[i]; + } + } + else + { + if (should_reduce_channels_from_4to3) + { + for (int i = 0; i < pixels; i++) + { + data_out[3 * i] = bfi.communication_buffer()[4 * i]; + data_out[3 * i + 1] = bfi.communication_buffer()[4 * i + 1]; + data_out[3 * i + 2] = bfi.communication_buffer()[4 * i + 2]; + } + } + else + { + memcpy(rt->data, bfi.communication_buffer(), bytes_received); + } + } + + if (should_remove_sign) + { + for (int i = 0; i < pixels * 3; i++) + { + data_out[i] += 128; + } + } + // and return it. return rt; } @@ -627,7 +854,6 @@ RawTilePtr OpenSlideImage::getNativeTile(const size_t tilex, const size_t tiley, * This function * automatically downsample a region in the missing zoom level z, if needed. - * Arguments are exactly as what would be given to openslide_read_region(). * Note that z is not the openslide layer, but the desired zoom level, because * the slide may not have all the layers that correspond to all the * zoom levels. The number of layers is equal or less than the number of @@ -640,7 +866,7 @@ RawTilePtr OpenSlideImage::getNativeTile(const size_t tilex, const size_t tiley, * call 4x (getCachedTile at next res, downsample, compose), * store in cache, and return tile. (causes recursion, stops at native layer or in cache.) */ -RawTilePtr OpenSlideImage::halfsampleAndComposeTile(const size_t tilex, const size_t tiley, const uint32_t iipres) +RawTilePtr BioFormatsImage::halfsampleAndComposeTile(const size_t tilex, const size_t tiley, const uint32_t iipres) { // not in cache and not a native tile, so create one from higher sampling. #ifdef DEBUG_OSI @@ -652,7 +878,7 @@ RawTilePtr OpenSlideImage::halfsampleAndComposeTile(const size_t tilex, const si uint32_t osi_level = numResolutions - 1 - iipres; #ifdef DEBUG_OSI - logfile << "OpenSlide :: halfsampleAndComposeTile() :: zoom=" << osi_level << " from " << (osi_level - 1) << endl; + logfile << "BioFormats :: halfsampleAndComposeTile() :: zoom=" << osi_level << " from " << (osi_level - 1) << endl; #endif size_t ntlx = numTilesX[osi_level]; @@ -681,15 +907,15 @@ RawTilePtr OpenSlideImage::halfsampleAndComposeTile(const size_t tilex, const si RawTilePtr rt(new RawTile(tiley * ntlx + tilex, iipres, 0, 0, tw, th, channels, bpc)); // compute the size, etc - rt->dataLength = tw * th * channels; + rt->dataLength = tw * th * 3; rt->filename = getImagePath(); rt->timestamp = timestamp; // new a block that is larger for openslide library to directly copy in. - // then shuffle from BGRA to RGB. relying on delete [] to do the right thing. + // then do color operations. relying on delete [] to do the right thing. rt->data = new unsigned char[rt->dataLength]; rt->memoryManaged = 1; // allocated data, so use this flag to indicate that it needs to be cleared on destruction - // rawtile->padded = false; + // rawtile->padded = false; #ifdef DEBUG_OSI logfile << "Allocating tw * th * channels * sizeof(char) : " << tw << " * " << th << " * " << channels << " * sizeof(char) " << endl << flush; @@ -719,7 +945,7 @@ RawTilePtr OpenSlideImage::halfsampleAndComposeTile(const size_t tilex, const si break; // at edge, this may not be a 2x2 block. #ifdef DEBUG_OSI - logfile << "OpenSlide :: halfsampleAndComposeTile() :: call getCachedTile " << endl + logfile << "BioFormats :: halfsampleAndComposeTile() :: call getCachedTile " << endl << flush; #endif @@ -739,7 +965,7 @@ RawTilePtr OpenSlideImage::halfsampleAndComposeTile(const size_t tilex, const si tileCache->insert(tt); // copy is made? #ifdef DEBUG_OSI - logfile << "OpenSlide :: halfsampleAndComoseTile() :: cache insert res " << tt_iipres << " " << ttx << "x" << tty << " :: " << timer.getTime() << " microseconds" << endl + logfile << "BioFormats :: halfsampleAndComoseTile() :: cache insert res " << tt_iipres << " " << ttx << "x" << tty << " :: " << timer.getTime() << " microseconds" << endl << flush; #endif @@ -752,14 +978,14 @@ RawTilePtr OpenSlideImage::halfsampleAndComposeTile(const size_t tilex, const si reinterpret_cast(rt->data), tw, th); } #ifdef DEBUG_OSI - logfile << "OpenSlide :: halfsampleAndComposeTile() :: called getCachedTile " << endl + logfile << "BioFormats :: halfsampleAndComposeTile() :: called getCachedTile " << endl << flush; #endif } } delete[] tt_data; #ifdef DEBUG_OSI - logfile << "OpenSlide :: halfsampleAndComposeTile() :: downsample " << osi_level << " from " << (osi_level - 1) << " :: " << timer.getTime() << " microseconds" << endl + logfile << "BioFormats :: halfsampleAndComposeTile() :: downsample " << osi_level << " from " << (osi_level - 1) << " :: " << timer.getTime() << " microseconds" << endl << flush; #endif @@ -767,32 +993,6 @@ RawTilePtr OpenSlideImage::halfsampleAndComposeTile(const size_t tilex, const si return rt; } -// h is number of rows to process. w is number of columns to process. -void OpenSlideImage::bgra2rgb(uint8_t *data, const size_t w, const size_t h) -{ - // swap bytes in place. we can because we are going from 4 bytes to 3, and because we are using a register to bswap - // 0000111122223333 - // in: BGRABGRABGRA - // out: RGBRGBRGB - // 000111222333 - - // this version relies on compiler to generate bswap code. - // bswap is only very slightly slower than SSSE3 and AVX2 code, and about 2x faster than naive single byte copies on core i5 ivy bridge. - uint8_t *out = data; - uint32_t *in = reinterpret_cast(data); - uint32_t *end = in + w * h; - - uint32_t t; - - // remaining - for (; in < end; ++in) - { - *(reinterpret_cast(out)) = bgra2rgb_kernel(*in); - - out += channels; - } -} - /** * performs 1/2 size downsample on rgb 24bit images * @details usingg the following property, @@ -813,13 +1013,14 @@ void OpenSlideImage::bgra2rgb(uint8_t *data, const size_t w, const size_t h) * @param out_h * @param downSamplingFactor always power of 2. */ -void OpenSlideImage::halfsample_3(const uint8_t *in, const size_t in_w, const size_t in_h, - uint8_t *out, size_t &out_w, size_t &out_h) +void BioFormatsImage::halfsample_3(const uint8_t *in, const size_t in_w, const size_t in_h, + uint8_t *out, size_t &out_w, size_t &out_h) { #ifdef DEBUG_OSI - logfile << "OpenSlide :: halfsample_3() :: start :: in " << (void *)in << " out " << (void *)out << endl - << flush; + logfile + << "BioFormats :: halfsample_3() :: start :: in " << (void *)in << " out " << (void *)out << endl + << flush; logfile << " :: in wxh " << in_w << "x" << in_h << endl << flush; #endif @@ -830,14 +1031,14 @@ void OpenSlideImage::halfsample_3(const uint8_t *in, const size_t in_w, const si if ((out_w == 0) || (out_h == 0)) { - logfile << "OpenSlide :: halfsample_3() :: ERROR: zero output width or height " << endl + logfile << "BioFormats :: halfsample_3() :: ERROR: zero output width or height " << endl << flush; return; } if (!(in)) { - logfile << "OpenSlide :: halfsample_3() :: ERROR: null input " << endl + logfile << "BioFormats :: halfsample_3() :: ERROR: null input " << endl << flush; return; } @@ -846,7 +1047,7 @@ void OpenSlideImage::halfsample_3(const uint8_t *in, const size_t in_w, const si uint8_t *dest = out; // if last recursion, put in out, else do it in place #ifdef DEBUG_OSI - logfile << "OpenSlide :: halfsample_3() :: top " << endl + logfile << "BioFormats :: halfsample_3() :: top " << endl << flush; #endif @@ -874,7 +1075,7 @@ void OpenSlideImage::halfsample_3(const uint8_t *in, const size_t in_w, const si } #ifdef DEBUG_OSI - logfile << "OpenSlide :: halfsample_3() :: last row " << endl + logfile << "BioFormats :: halfsample_3() :: last row " << endl << flush; #endif @@ -892,7 +1093,7 @@ void OpenSlideImage::halfsample_3(const uint8_t *in, const size_t in_w, const si } #ifdef DEBUG_OSI - logfile << "OpenSlide :: halfsample_3() :: last one " << endl + logfile << "BioFormats :: halfsample_3() :: last one " << endl << flush; #endif @@ -903,26 +1104,26 @@ void OpenSlideImage::halfsample_3(const uint8_t *in, const size_t in_w, const si // at this point, in has been averaged and stored . // since we stride forward 2 col and rows at a time, we don't need to worry about overwriting an unread pixel. #ifdef DEBUG_OSI - logfile << "OpenSlide :: halfsample_3() :: done" << endl + logfile << "BioFormats :: halfsample_3() :: done" << endl << flush; #endif } // in is contiguous, out will be when done. -void OpenSlideImage::compose(const uint8_t *in, const size_t in_w, const size_t in_h, - const size_t &xoffset, const size_t &yoffset, - uint8_t *out, const size_t &out_w, const size_t &out_h) +void BioFormatsImage::compose(const uint8_t *in, const size_t in_w, const size_t in_h, + const size_t &xoffset, const size_t &yoffset, + uint8_t *out, const size_t &out_w, const size_t &out_h) { #ifdef DEBUG_OSI - logfile << "OpenSlide :: compose() :: start " << endl + logfile << "BioFormats :: compose() :: start " << endl << flush; #endif if ((in_w == 0) || (in_h == 0)) { #ifdef DEBUG_OSI - logfile << "OpenSlide :: compose() :: zero width or height " << endl + logfile << "BioFormats :: compose() :: zero width or height " << endl << flush; #endif return; @@ -930,7 +1131,7 @@ void OpenSlideImage::compose(const uint8_t *in, const size_t in_w, const size_t if (!(in)) { #ifdef DEBUG_OSI - logfile << "OpenSlide :: compose() :: nullptr input " << endl + logfile << "BioFormats :: compose() :: nullptr input " << endl << flush; #endif return; @@ -961,7 +1162,7 @@ void OpenSlideImage::compose(const uint8_t *in, const size_t in_w, const size_t } #ifdef DEBUG_OSI - logfile << "OpenSlide :: compose() :: start " << endl + logfile << "BioFormats :: compose() :: start " << endl << flush; #endif } diff --git a/iipsrv/src/BioFormatsImage.h b/iipsrv/src/BioFormatsImage.h new file mode 100644 index 0000000..b84c209 --- /dev/null +++ b/iipsrv/src/BioFormatsImage.h @@ -0,0 +1,175 @@ +/* + * File: BioFormatsImage.h + * Based on OpenSlideImage.h + * + * + */ + +#ifndef BIOFORMATSIMAGE_H +#define BIOFORMATSIMAGE_H + +#include "IIPImage.h" +#include "BioFormatsManager.h" +#include +#include +#include +#include +#include +#include +#include + +#include "Cache.h" + +#define throw(a) + +class BioFormatsImage : public IIPImage +{ +private: + BioFormatsInstance bfi; + + TileCache *tileCache; + + std::vector numTilesX, numTilesY; + std::vector lastTileXDim, lastTileYDim; + std::vector bioformats_level_to_use, bioformats_downsample_in_level; + + int channels_internal; + int pick_byte = 0; // 0 for pick first (from big endian serialized), 1 for pick last +#ifdef DEBUG_OSI + int milliseconds = 0; +#endif DEBUG_OSI + + // Unimplemented methods in line with OpenslideImage.h: + // void read(...); + // void downsample_region(...); + + /** + * @brief get cached tile. + * @detail return from the local cache a tile. + * The tile may be native (directly from file), + * previously cached, + * or downsampled (recursively called. + * note that tilex and tiley can be found by multiplying by 2 raised to power of the difference in levels. + * 2 versions - direct and recursive. direct should have slightly lower latency. + * + * @param tilex + * @param tiley + * @param res + * @return + */ + /// check if cache has tile. if yes, return it. if not, and is a native layer, getNativeTile, else call halfsampleAndComposeTile + RawTilePtr getCachedTile(const size_t tilex, const size_t tiley, const uint32_t iipres); + + /// read from file, color convert, store in cache, and return tile. + RawTilePtr getNativeTile(const size_t tilex, const size_t tiley, const uint32_t iipres); + + /// call 4x (getCachedTile at next res, downsample, compose), store in cache, and return tile. (causes recursion, stops at native layer or in cache.) + RawTilePtr halfsampleAndComposeTile(const size_t tilex, const size_t tiley, const uint32_t iipres); + + /// average 2 rows, then average 2 pixels + inline uint32_t halfsample_kernel_3(const uint8_t *r1, const uint8_t *r2) + { + uint64_t a = *(reinterpret_cast(r1)); + uint64_t c = *(reinterpret_cast(r2)); + uint64_t t1 = (((a ^ c) & 0xFEFEFEFEFEFEFEFE) >> 1) + (a & c); + uint32_t t2 = t1 & 0x0000000000FFFFFF; + uint32_t t3 = (t1 >> 24) & 0x0000000000FFFFFF; + return (((t3 ^ t2) & 0xFEFEFEFE) >> 1) + (t3 & t2); + } + + /// downsample by 2 + void halfsample_3(const uint8_t *in, const size_t in_w, const size_t in_h, + uint8_t *out, size_t &out_w, size_t &out_h); + + void compose(const uint8_t *in, const size_t in_w, const size_t in_h, + const size_t &xoffset, const size_t &yoffset, + uint8_t *out, const size_t &out_w, const size_t &out_h); + + /// Constructor + BioFormatsImage() : IIPImage() + { + bfi = BioFormatsManager::get_new(); + }; + +public: + /// Constructor + /** \param path image path + */ + BioFormatsImage(const std::string &path, TileCache *tile_cache) : IIPImage(path), tileCache(tile_cache) + { + bfi = BioFormatsManager::get_new(); + // set tile width on loadimage, not here + }; + + /// Copy Constructor + + /** \param image IIPImage object + */ + BioFormatsImage(const IIPImage &image, TileCache *tile_cache) : IIPImage(image), tileCache(tile_cache) + { + bfi = BioFormatsManager::get_new(); + }; + + /** \param image IIPImage object + */ + /*explicit BioFormatsImage(const BioFormatsImage &image) : IIPImage(image), + // Copy everything including JVM pointers for moving here. + tileCache(image.tileCache), + bfi(image.bfi), + numTilesX(image.numTilesX), + numTilesY(image.numTilesY), + lastTileXDim(image.lastTileXDim), + lastTileYDim(image.lastTileYDim), + bioformats_level_to_use(image.bioformats_level_to_use), + bioformats_downsample_in_level(image.bioformats_downsample_in_level), + receive_buffer(image.receive_buffer) + { + throw runtime_error("BioFormatsImage.h: Copy constructor not allowed as JVM classes cannot be cloned"); + + if (!receive_buffer) + { + throw runtime_error("Unitialized receive buffer found!"); + } + };*/ + + // This isn't necessary + // We would otherwise need to copy the class, so for this, + // Take the open file location from the "image" and make a new class + // with it where we open this image and copy the remaining members of this class. + // Restore the current resolution also if needed. + explicit BioFormatsImage(const BioFormatsImage &image) = delete; + + + /// Destructor + + virtual ~BioFormatsImage() + { + BioFormatsManager::free(std::move(bfi)); + }; + + virtual void openImage() throw(file_error); + + /// Overloaded function for loading image information + /** \param x horizontal sequence angle + \param y vertical sequence angle + */ + virtual void loadImageInfo(int x, int y) throw(file_error); + + virtual void closeImage(); + + /// Overloaded function for getting a particular tile + /** \param x horizontal sequence angle + \param y vertical sequence angle + \param r resolution + \param l number of quality layers to decode + \param t tile number + */ + virtual RawTilePtr getTile(int x, int y, unsigned int r, int l, unsigned int t) throw(file_error); + + // Unimplemented with OpenSlideImage.h: + // virtual RawTile getRegion(...); + + // void readProperties(...); +}; + +#endif /* BIOFORMATSIMAGE_H */ diff --git a/iipsrv/src/BioFormatsInstance.cc b/iipsrv/src/BioFormatsInstance.cc new file mode 100644 index 0000000..613a878 --- /dev/null +++ b/iipsrv/src/BioFormatsInstance.cc @@ -0,0 +1,21 @@ +#include "BioFormatsInstance.h" +#include "BioFormatsThread.h" +#include + +BioFormatsThread BioFormatsInstance::thread; + +BioFormatsInstance::BioFormatsInstance() +{ + // Expensive function being used from a header-only library. + // Shouldn't be called from a header file + bfbridge_error_t *error = + bfbridge_make_instance( + &bfinstance, + &thread.bfthread, + new char[bfi_communication_buffer_len], + bfi_communication_buffer_len); + if (error) + { + throw runtime_error("BioFormatsInstance.cc error: " + std::string(error->description)); + } +} diff --git a/iipsrv/src/BioFormatsInstance.h b/iipsrv/src/BioFormatsInstance.h new file mode 100644 index 0000000..4c56e2f --- /dev/null +++ b/iipsrv/src/BioFormatsInstance.h @@ -0,0 +1,239 @@ +/* + * File: BioFormatsInstance.h + */ + +#ifndef BIOFORMATSINSTANCE_H +#define BIOFORMATSINSTANCE_H + +#include +#include +#include +#include +#include +#include +#include +#include "BioFormatsThread.h" + +/* +To use this library, do: +BioFormatsInstance bfi = BioFormatsManager::get_new(); +bfi.bf_open(filename); // or any other methods +... +and when you're done: +BioFormatsManager::free(std::move(bfi)); +*/ + +// Allow 2048*2048 four channels of 16 bits +#define bfi_communication_buffer_len 33554432 + +class BioFormatsInstance +{ +public: + // std::unique_ptr or shared ptr would also work + static BioFormatsThread thread; + + bfbridge_instance_t bfinstance; + + BioFormatsInstance(); + + // We want to keep alive 1 BioFormats instance + // Copying is not allowed as it's not possible + // Move semantics instead. + // An alternative would be using unique pointer + // Another would be reference counting, but destroying the previous is easier. + // (https://ps.uci.edu/~cyu/p231C/LectureNotes/lecture13:referenceCounting/lecture13.pdf) + // Our move semantics should replace the previous object's pointers with null pointers. + // This is because the previous object will likely be destroyed (as they are already unusable) + // so their destructor mustnt't free Java structures. So we set them to NULL + // and make the destructor free them only if they're not NULL. + // This way, only one copy is kept alive. + + BioFormatsInstance(const BioFormatsInstance &) = delete; + BioFormatsInstance(BioFormatsInstance &&other) + { + // Copy both the Java instance class and the communication buffer + // Moving removes the java class pointer from the previous + // so that the destruction of it doesn't break the newer class + bfbridge_move_instance(&bfinstance, &other.bfinstance); + } + BioFormatsInstance &operator=(const BioFormatsInstance &) = delete; + BioFormatsInstance &operator=(BioFormatsInstance &&other) + { + bfbridge_move_instance(&bfinstance, &other.bfinstance); + return *this; + } + + char *communication_buffer() + { + return bfbridge_instance_get_communication_buffer(&bfinstance, NULL); + } + + ~BioFormatsInstance() + { + char *buffer = communication_buffer(); + if (buffer) + { + delete[] buffer; + } + + bfbridge_free_instance(&bfinstance, &thread.bfthread); + } + + // changed ownership: user opened new file, etc. + void refresh() + { +#ifdef OSI_DEBUG + cerr << "calling refresh\n"; +#endif + // Here is an example of calling a method manually without the C wrapper + thread.bfthread.env->CallVoidMethod(bfinstance.bfbridge, thread.bfthread.BFClose); +#ifdef OSI_DEBUG + cerr << "called refresh\n"; +#endif + } + + // To be called only just after a function returns an error code + std::string get_error() + { + std::string err; + char *buffer = communication_buffer(); + err.assign(communication_buffer(), + bf_get_error_length(&bfinstance, &thread.bfthread)); + return err; + } + + int is_compatible(std::string filepath) + { + return bf_is_compatible(&bfinstance, &thread.bfthread, &filepath[0], filepath.length()); + } + + int open(std::string filepath) + { + return bf_open(&bfinstance, &thread.bfthread, &filepath[0], filepath.length()); + } + + int close() + { + return bf_close(&bfinstance, &thread.bfthread); + } + + int get_resolution_count() + { + return bf_get_resolution_count(&bfinstance, &thread.bfthread); + } + + int set_current_resolution(int res) + { + return bf_set_current_resolution(&bfinstance, &thread.bfthread, res); + } + + int get_size_x() + { + return bf_get_size_x(&bfinstance, &thread.bfthread); + } + + int get_size_y() + { + return bf_get_size_y(&bfinstance, &thread.bfthread); + } + + int get_size_z() + { + return bf_get_size_z(&bfinstance, &thread.bfthread); + } + + int get_size_c() + { + return bf_get_size_c(&bfinstance, &thread.bfthread); + } + + int get_size_t() + { + return bf_get_size_t(&bfinstance, &thread.bfthread); + } + + int get_effective_size_c() + { + return bf_get_size_c(&bfinstance, &thread.bfthread); + } + + int get_optimal_tile_width() + { + return bf_get_optimal_tile_width(&bfinstance, &thread.bfthread); + } + + int get_optimal_tile_height() + { + return bf_get_optimal_tile_height(&bfinstance, &thread.bfthread); + } + + int get_pixel_type() + { + return bf_get_pixel_type(&bfinstance, &thread.bfthread); + } + + int get_bytes_per_pixel() + { + return bf_get_bytes_per_pixel(&bfinstance, &thread.bfthread); + } + + int get_rgb_channel_count() + { + return bf_get_rgb_channel_count(&bfinstance, &thread.bfthread); + } + + int get_image_count() + { + return bf_get_rgb_channel_count(&bfinstance, &thread.bfthread); + } + + int is_rgb() + { + return bf_is_rgb(&bfinstance, &thread.bfthread); + } + + int is_interleaved() + { + return bf_is_interleaved(&bfinstance, &thread.bfthread); + } + + int is_little_endian() + { + return bf_is_little_endian(&bfinstance, &thread.bfthread); + } + + int is_false_color() + { + return bf_is_false_color(&bfinstance, &thread.bfthread); + } + + int is_indexed_color() + { + return bf_is_indexed_color(&bfinstance, &thread.bfthread); + } + + std::string get_dimension_order() + { + int len = bf_get_dimension_order(&bfinstance, &thread.bfthread); + if (len < 0) + { + return ""; + } + std::string s; + char *buffer = communication_buffer(); + s.assign(buffer, len); + return s; + } + + int is_order_certain() + { + return bf_is_order_certain(&bfinstance, &thread.bfthread); + } + + int open_bytes(int x, int y, int w, int h) + { + return bf_open_bytes(&bfinstance, &thread.bfthread, 0, x, y, w, h); + } +}; + +#endif /* BIOFORMATSINSTANCE_H */ diff --git a/iipsrv/src/BioFormatsManager.cc b/iipsrv/src/BioFormatsManager.cc new file mode 100644 index 0000000..69ba22b --- /dev/null +++ b/iipsrv/src/BioFormatsManager.cc @@ -0,0 +1,3 @@ +#include "BioFormatsManager.h" + +std::vector BioFormatsManager::free_list; diff --git a/iipsrv/src/BioFormatsManager.h b/iipsrv/src/BioFormatsManager.h new file mode 100644 index 0000000..daa98c8 --- /dev/null +++ b/iipsrv/src/BioFormatsManager.h @@ -0,0 +1,41 @@ +/* + * File: BioFormatsManager.h + */ + +#ifndef BIOFORMATSMANAGER_H +#define BIOFORMATSMANAGER_H + +#include +#include "BioFormatsInstance.h" +#include + +class BioFormatsManager +{ +private: + static std::vector free_list; + +public: + // call me with std::move - I think? + static void free(BioFormatsInstance &&graal_isolate) + { + free_list.push_back(std::move(graal_isolate)); + free_list.back().refresh(); + } + + static BioFormatsInstance get_new() + { + // Make a new one if needed + if (free_list.size() == 0) + { + BioFormatsInstance bfi; + free_list.push_back(std::move(bfi)); + } + + // Pop one from the array + BioFormatsInstance bfi = std::move(free_list.back()); + free_list.pop_back(); // calls destructuor + return bfi; + } +}; + +#endif /* BIOFORMATSMANAGER_H */ diff --git a/iipsrv/src/BioFormatsThread.cc b/iipsrv/src/BioFormatsThread.cc new file mode 100644 index 0000000..121bab6 --- /dev/null +++ b/iipsrv/src/BioFormatsThread.cc @@ -0,0 +1,40 @@ +/* + * File: BioFormatsThread.cc + */ + +#include "BioFormatsThread.h" + +BioFormatsThread::BioFormatsThread() +{ + // In our Docker caMicroscpe deployment we pass these using fcgid.conf + // and other conf files + // Required: + char *cpdir = getenv("BFBRIDGE_CLASSPATH"); + if (!cpdir || cpdir[0] == '\0') + { + cerr << "Please set BFBRIDGE_CLASSPATH to a single directory where jar files can be found.\n"; + throw runtime_error("Please set BFBRIDGE_CLASSPATH to a single directory where jar files can be found.\n") + } + + // Optional: + char *cachedir = getenv("BFBRIDGE_CACHEDIR"); + if (cachedir && cachedir[0] == '\0') + { + cachedir = NULL; + } + bfbridge_error_t *error = bfbridge_make_vm(&bfvm, cpdir, cachedir); + if (error) + { + fprintf(stderr, "BioFormatsThread.cc bfbridge_make_vm error: %s\n", error->description); + throw "BioFormatsThread.cc bfbridge_make_vm error: \n" + std::string(error->description); + } + + // Expensive function being used from a header-only library. + // Shouldn't be called from a header file + error = bfbridge_make_thread(&bfthread, &bfvm); + if (error) + { + fprintf(stderr, "BioFormatsThread.cc bfbridge_make_thread error: %s\n", error->description); + throw "BioFormatsThread.cc bfbridge_make_thread error: \n" + std::string(error->description); + } +} diff --git a/iipsrv/src/BioFormatsThread.h b/iipsrv/src/BioFormatsThread.h new file mode 100644 index 0000000..7394cf3 --- /dev/null +++ b/iipsrv/src/BioFormatsThread.h @@ -0,0 +1,40 @@ +/* + * File: BioFormatsThread.h + */ + +#ifndef BIOFORMATSTHREAD_H +#define BIOFORMATSTHREAD_H + +#include +#include +#include +#define BFBRIDGE_INLINE +#define BFBRIDGE_KNOW_BUFFER_LEN +#include "../BFBridge/bfbridge_basiclib.h" + +class BioFormatsThread +{ +public: + bfbridge_vm_t bfvm; + bfbridge_thread_t bfthread; + + BioFormatsThread(); + + // Copying a BioFormatsThread means copying a JVM and this is not + // possible. The attempt to do that is a sign of faulty code + // so a compile time error. + BioFormatsThread(const BioFormatsThread &other) = delete; + BioFormatsThread &operator=(const BioFormatsThread &other) = delete; + + ~BioFormatsThread() + { + bfbridge_free_thread(&bfthread); + + // Must run only once, on app termination + // Any other time, it breaks JVM and won't run again + // Therefore even if JVM is unused for some time, it must be kept alive + bfbridge_free_vm(&bfvm); + } +}; + +#endif /* BIOFORMATSTHREAD_H */ diff --git a/iipsrv/src/FIF.cc b/iipsrv/src/FIF.cc index 5f2f2f8..dfada0c 100644 --- a/iipsrv/src/FIF.cc +++ b/iipsrv/src/FIF.cc @@ -21,6 +21,7 @@ #include #include "OpenSlideImage.h" +#include "BioFormatsImage.h" #include #include @@ -125,7 +126,14 @@ void FIF::run( Session* session, const string& src ){ if( session->loglevel >= 2 ) *(session->logfile) << "FIF :: OpenSlide image detected" << endl; temp = IIPImagePtr(new OpenSlideImage( test, session->tileCache )); } - #ifdef HAVE_KAKADU +#pragma mark Adding in basic bioformats functionality + else if (format == BIOFORMATS) + { + if (session->loglevel >= 2) + *(session->logfile) << "FIF :: BioFormats image detected" << endl; + temp = IIPImagePtr(new BioFormatsImage(test, session->tileCache)); + } +#ifdef HAVE_KAKADU else if( format == JPEG2000 ){ if( session->loglevel >= 2 ) *(session->logfile) << "FIF :: JPEG2000 image detected" << endl; temp = IIPImagePtr(new KakaduImage( test )); diff --git a/iipsrv/src/IIPImage.cc b/iipsrv/src/IIPImage.cc index e4e3ba6..762bdad 100644 --- a/iipsrv/src/IIPImage.cc +++ b/iipsrv/src/IIPImage.cc @@ -41,6 +41,7 @@ #include #include "openslide.h" +#include "BioFormatsManager.h" using namespace std; @@ -122,18 +123,50 @@ void IIPImage::testImageType() throw(file_error) unsigned char lbigtiff[4] = {0x4D,0x4D,0x00,0x2B}; // Little Endian BigTIFF unsigned char bbigtiff[4] = {0x49,0x49,0x2B,0x00}; // Big Endian BigTIFF - const char * vendor = openslide_detect_vendor( path.c_str() ); - if ( vendor != NULL && strcmp(vendor, "generic-tiff") ) - format = OPENSLIDE; - // Compare our header sequence to our magic byte signatures - else if( memcmp( header, j2k, 10 ) == 0 ) format = JPEG2000; - else if( memcmp( header, stdtiff, 3 ) == 0 - || memcmp( header, lsbtiff, 4 ) == 0 || memcmp( header, msbtiff, 4 ) == 0 - || memcmp( header, lbigtiff, 4 ) == 0 || memcmp( header, bbigtiff, 4 ) == 0 ){ - format = TIF; + // OpenSlide + { + const char * vendor = openslide_detect_vendor( path.c_str() ); + if ( vendor != NULL ) { + if ( !strcmp(vendor, "generic-tiff") ) { + // Have generic TIFF, so use iipsrv reader + format = TIF + return; + } + // OpenSlide but not generic tiff + format = OPENSLIDE; + return; + } + } + + // BioFormats + { + BioFormatsInstance bfi = BioFormatsManager::get_new(); + int code = bfi.is_compatible( path ); + // 1 -> compatible + // 0 -> incompatible + // -1 -> error + if ( code == 1 ) { + format = BIOFORMATS; + return; + } + BioFormatsManager::free( std::move(bfi) ); + } + + // IIPsrv builtin + { + if( memcmp( header, j2k, 10 ) == 0 ) { + format = JPEG2000; + return; + } + else if( memcmp( header, stdtiff, 3 ) == 0 + || memcmp( header, lsbtiff, 4 ) == 0 || memcmp( header, msbtiff, 4 ) == 0 + || memcmp( header, lbigtiff, 4 ) == 0 || memcmp( header, bbigtiff, 4 ) == 0 ){ + format = TIF; + return; + } } - else format = UNSUPPORTED; + format = UNSUPPORTED; } else{ @@ -172,6 +205,209 @@ void IIPImage::testImageType() throw(file_error) suffix=="dcm" || suffix=="bif") format = OPENSLIDE; + else if ( + suffix = "v3draw" || + suffix == "ano" || + suffix == "cfg" || + suffix == "csv" || + suffix == "htm" || + suffix == "rec" || + suffix == "tim" || + suffix == "zpo" || + suffix == "tif" || + suffix == "dic" || + suffix == "dcm" || + suffix == "dicom" || + suffix == "jp2" || + suffix == "j2ki" || + suffix == "j2kr" || + suffix == "raw" || + suffix == "ima" || + suffix == "cr2" || + suffix == "crw" || + suffix == "jpg" || + suffix == "thm" || + suffix == "wav" || + suffix == "tiff" || + suffix == "dv" || + suffix == "r3d" || + suffix == "r3d_d3d" || + suffix == "log" || + suffix == "mvd2" || + suffix == "aisf" || + suffix == "aiix" || + suffix == "dat" || + suffix == "atsf" || + suffix == "tf2" || + suffix == "tf8" || + suffix == "btf" || + suffix == "pbm" || + suffix == "pgm" || + suffix == "ppm" || + suffix == "xdce" || + suffix == "xml" || + suffix == "xlog" || + suffix == "apl" || + suffix == "tnb" || + suffix == "mtb" || + suffix == "im" || + suffix == "mea" || + suffix == "res" || + suffix == "aim" || + suffix == "arf" || + suffix == "psd" || + suffix == "al3d" || + suffix == "gel" || + suffix == "am" || + suffix == "amiramesh" || + suffix == "grey" || + suffix == "hx" || + suffix == "labels" || + suffix == "img" || + suffix == "hdr" || + suffix == "sif" || + suffix == "afi" || + suffix == "svs" || + suffix == "exp" || + suffix == "h5" || + suffix == "1sc" || + suffix == "pic" || + suffix == "scn" || + suffix == "ims" || + suffix == "ch5" || + suffix == "vsi" || + suffix == "ets" || + suffix == "pnl" || + suffix == "htd" || + suffix == "c01" || + suffix == "dib" || + suffix == "cxd" || + suffix == "v" || + suffix == "eps" || + suffix == "epsi" || + suffix == "ps" || + suffix == "flex" || + suffix == "xlef" || + suffix == "fits" || + suffix == "fts" || + suffix == "dm2" || + suffix == "dm3" || + suffix == "dm4" || + suffix == "naf" || + suffix == "his" || + suffix == "ndpi" || + suffix == "ndpis" || + suffix == "vms" || + suffix == "txt" || + suffix == "i2i" || + suffix == "hed" || + suffix == "mod" || + suffix == "inr" || + suffix == "ipl" || + suffix == "ipm" || + suffix == "fff" || + suffix == "ics" || + suffix == "ids" || + suffix == "seq" || + suffix == "ips" || + suffix == "ipw" || + suffix == "frm" || + suffix == "par" || + suffix == "j2k" || + suffix == "jpf" || + suffix == "jpk" || + suffix == "jpx" || + suffix == "klb" || + suffix == "xv" || + suffix == "bip" || + suffix == "sxm" || + suffix == "fli" || + suffix == "lim" || + suffix == "msr" || + suffix == "lif" || + suffix == "lof" || + suffix == "lei" || + suffix == "l2d" || + suffix == "mnc" || + suffix == "stk" || + suffix == "nd" || + suffix == "scan" || + suffix == "vff" || + suffix == "mrw" || + suffix == "stp" || + suffix == "mng" || + suffix == "nii" || + suffix == "nrrd" || + suffix == "nhdr" || + suffix == "nd2" || + suffix == "nef" || + suffix == "obf" || + suffix == "omp2info" || + suffix == "oib" || + suffix == "oif" || + suffix == "pty" || + suffix == "lut" || + suffix == "oir" || + suffix == "sld" || + suffix == "spl" || + suffix == "liff" || + suffix == "top" || + suffix == "pcoraw" || + suffix == "pcx" || + suffix == "pict" || + suffix == "pct" || + suffix == "df3" || + suffix == "im3" || + suffix == "qptiff" || + suffix == "bin" || + suffix == "env" || + suffix == "spe" || + suffix == "afm" || + suffix == "sm2" || + suffix == "sm3" || + suffix == "spc" || + suffix == "set" || + suffix == "sdt" || + suffix == "spi" || + suffix == "xqd" || + suffix == "xqf" || + suffix == "db" || + suffix == "vws" || + suffix == "pst" || + suffix == "inf" || + suffix == "tfr" || + suffix == "ffr" || + suffix == "zfr" || + suffix == "zfp" || + suffix == "2fl" || + suffix == "tga" || + suffix == "pr3" || + suffix == "dti" || + suffix == "fdf" || + suffix == "hdf" || + suffix == "bif" || + suffix == "xys" || + suffix == "html" || + suffix == "acff" || + suffix == "wat" || + suffix == "bmp" || + suffix == "wpi" || + suffix == "czi" || + suffix == "lms" || + suffix == "lsm" || + suffix == "mdb" || + suffix == "zvi" || + suffix == "mrc" || + suffix == "st" || + suffix == "ali" || + suffix == "map" || + suffix == "mrcs" || + suffix == "jpeg" || + suffix == "png" || + suffix == "gif" || + suffix == "ptif" + ) + format = BIOFORMATS; else if( suffix == "jp2" || suffix == "jpx" || suffix == "j2k" ) format = JPEG2000; else if( suffix == "ptif" || suffix == "tif" || suffix == "tiff" ) format = TIF; else format = UNSUPPORTED; diff --git a/iipsrv/src/IIPImage.h b/iipsrv/src/IIPImage.h index 9e399cf..e842601 100644 --- a/iipsrv/src/IIPImage.h +++ b/iipsrv/src/IIPImage.h @@ -47,7 +47,7 @@ class file_error : public std::runtime_error { // Supported image formats -enum ImageFormat { TIF, JPEG2000, OPENSLIDE, UNSUPPORTED }; +enum ImageFormat { TIF, JPEG2000, OPENSLIDE, BIOFORMATS, UNSUPPORTED }; diff --git a/iipsrv/src/Makefile.am b/iipsrv/src/Makefile.am index 6169e88..eb14ee1 100644 --- a/iipsrv/src/Makefile.am +++ b/iipsrv/src/Makefile.am @@ -4,9 +4,15 @@ noinst_PROGRAMS = iipsrv.fcgi INCLUDES = @INCLUDES@ @LIBFCGI_INCLUDES@ @JPEG_INCLUDES@ @TIFF_INCLUDES@ -LIBS = @LIBS@ @LIBFCGI_LIBS@ @DL_LIBS@ @JPEG_LIBS@ @TIFF_LIBS@ -lm -lopenslide -lopenjp2 -AM_LDFLAGS = @LIBFCGI_LDFLAGS@ -AM_CPPFLAGS = -I/usr/local/include/openslide + +# -Wl,-rpath,$(JAVA_HOME)/lib/server +LIBS = @LIBS@ @LIBFCGI_LIBS@ @DL_LIBS@ @JPEG_LIBS@ @TIFF_LIBS@ -lm -lopenslide -lopenjp2 -ljvm -L$(JAVA_HOME)/lib/server +AM_LDFLAGS = @LIBFCGI_LDFLAGS@ -rpath $(JAVA_HOME)/lib/server +AM_CPPFLAGS = -I/usr/local/include/openslide -DBFBRIDGE_INLINE + +# jni-md.h should also be included hence the platform paths, see link in https://stackoverflow.com/a/37029528 +# https://github.com/openjdk/jdk/blob/6e3cc131daa9f3b883164333bdaad7aa3a6ca018/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/utilities/PlatformInfo.java#L32 +AM_CPPFLAGS += -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux -I$(JAVA_HOME)/include/darwin -I$(JAVA_HOME)/include/win32 -I$(JAVA_HOME)/include/bsd -DBFBRIDGE_INLINE iipsrv_fcgi_LDADD = Main.o @@ -31,6 +37,12 @@ iipsrv_fcgi_SOURCES = \ TPTImage.cc \ OpenSlideImage.h \ OpenSlideImage.cc \ + BioFormatsImage.h \ + BioFormatsImage.cc \ + BioFormatsInstance.h \ + BioFormatsInstance.cc \ + BioFormatsThread.h \ + BioFormatsThread.cc \ JPEGCompressor.h \ JPEGCompressor.cc \ RawTile.h \ @@ -41,6 +53,8 @@ iipsrv_fcgi_SOURCES = \ Tokenizer.h \ IIPResponse.h \ IIPResponse.cc \ + BioFormatsManager.h \ + BioFormatsManager.cc \ View.h \ View.cc \ Transforms.h \ From ab9d2a7f0a79684d1c8881f790982a8d38930ae8 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Sun, 27 Aug 2023 21:20:10 +0100 Subject: [PATCH 09/17] update --- Dockerfile | 3 ++ iipsrv/src/BioFormatsImage.cc | 88 +++++++++++--------------------- iipsrv/src/BioFormatsImage.h | 4 +- iipsrv/src/BioFormatsInstance.cc | 2 +- iipsrv/src/BioFormatsThread.cc | 12 +++-- iipsrv/src/BioFormatsThread.h | 12 +++-- iipsrv/src/IIPImage.cc | 5 +- iipsrv/src/Makefile.am | 4 +- 8 files changed, 58 insertions(+), 72 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9cc2f5d..8cd2dac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,9 @@ RUN ln -s /etc/apache2/mods-available/proxy.conf /etc/apache2/mods-enabled/proxy COPY apache2.conf /etc/apache2/apache2.conf COPY ports.conf /etc/apache2/ports.conf +## Print BioFormats errors, etc. to Docker console (stderr) +#RUN ln -sf /proc/self/fd/1 /var/log/apache2/error.log + ### iipsrv WORKDIR /root/src/iipsrv RUN ./autogen.sh diff --git a/iipsrv/src/BioFormatsImage.cc b/iipsrv/src/BioFormatsImage.cc index 74a261e..70de64d 100644 --- a/iipsrv/src/BioFormatsImage.cc +++ b/iipsrv/src/BioFormatsImage.cc @@ -8,6 +8,7 @@ #include // #define DEBUG_OSI 1 +#include using namespace std; extern std::ofstream logfile; @@ -70,11 +71,11 @@ static unsigned int getPowerOfTwoRoundDown(unsigned int a) return sizeof(unsigned int) * 8 - __builtin_clz(a) - 1; #else unsigned int x = 0; - while (a >> 1) + while (a >>= 1) { x++; } - return x - 1; + return x; #endif } @@ -92,33 +93,17 @@ void BioFormatsImage::loadImageInfo(int x, int y) throw(file_error) currentY = y; // choose power of 2 to make downsample simpler. - int suggested_width = bfi.get_optimal_tile_width(); - if (suggested_width > 0) - { - suggested_width = 1 << getPowerOfTwoRoundDown(suggested_width); - } - if (suggested_width > 2048 || suggested_width < 128) - { + // make it a square, rectangles have been associated with problems + tile_width = bfi.get_optimal_tile_width(); + if (tile_width > 0) { + tile_width = 1 << getPowerOfTwoRoundDown(tile_width); + } if (tile_width < 128) { tile_width = 256; - } - else - { - tile_width = suggested_width; + } else if (tile_width > 512) { + tile_width = 512; } - int suggested_height = bfi.get_optimal_tile_height(); - if (suggested_height > 0) - { - suggested_height = 1 << getPowerOfTwoRoundDown(suggested_height); - } - if (suggested_height > 2048 || suggested_height < 128) - { - tile_height = 256; - } - else - { - tile_height = suggested_height; - } + tile_height = tile_width; w = bfi.get_size_x(); h = bfi.get_size_y(); @@ -137,7 +122,8 @@ void BioFormatsImage::loadImageInfo(int x, int y) throw(file_error) // PLEASE NOTE: these can differ between resolution levels cerr << "Parsing details" << endl; - cerr << "Optimal: " << tile_width << " " << tile_height << endl; + cerr << "opt wh: " << bfi.get_optimal_tile_width() << " " << bfi.get_optimal_tile_height() << endl; + cerr << "tile wh: " << tile_width << " " << tile_height << endl; cerr << "rgbChannelCount: " << bfi.get_rgb_channel_count() << endl; // Number of colors returned with each openbytes call cerr << "sizeC: " << bfi.get_size_c() << endl; cerr << "effectiveSizeC: " << bfi.get_effective_size_c() << endl; // colors on separate planes. 1 if all on same plane @@ -170,13 +156,6 @@ void BioFormatsImage::loadImageInfo(int x, int y) throw(file_error) } } - if (bfi.get_effective_size_c() != 1) - { - // We need to find an example file to learn how to parse such files - logfile << "Unimplemented: get_effective_size_c is not one but " << bfi.get_effective_size_c() << endl; - throw file_error("Unimplemented: get_effective_size_c is not one but " + std::to_string(bfi.get_effective_size_c())); - } - if (bfi.is_indexed_color() && !bfi.is_false_color()) { // We must read from the table @@ -209,13 +188,11 @@ void BioFormatsImage::loadImageInfo(int x, int y) throw(file_error) while (too_big) { tile_height >>= 1; - if (!too_big) - break; tile_width >>= 1; } #undef too_big - // save the openslide dimensions. + // save the bioformats dimensions. std::vector bioformats_widths, bioformats_heights; bioformats_widths.clear(); bioformats_heights.clear(); @@ -292,7 +269,7 @@ void BioFormatsImage::loadImageInfo(int x, int y) throw(file_error) // what if there are ~~openslide~~ bioformats levels with dim smaller than this? // populate at 1/2 size steps - while ((w > 256) || (h > 256)) + while ((w > tile_width) || (h > tile_height)) { // need a level that has image completely inside 1 tile. // (stop with both w and h less than tile_w/h, previous iteration divided by 2. @@ -555,8 +532,6 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley rt->filename = getImagePath(); rt->timestamp = timestamp; - int allocate_length = rt->dataLength; - if (bfi.set_current_resolution(bestLayer) < 0) { auto s = string("FATAL : bad resolution: " + std::to_string(bestLayer) + " rather than up to " + std::to_string(bfi.get_resolution_count() - 1)); @@ -564,10 +539,12 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley throw file_error(s); } + int allocate_length = rt->dataLength; + // Note: Pixel formats are either the same for every resolution (see: channels_internal) // or can differ between resolutions (see: should_interleave). // Assuming the former saves lots of time. The latter must be called after set_current_resolution. - // 1 JNI call is less than 1ms according to https://stackoverflow.com/a/36141175 + // 1 JNI call takes less than 1ms according to https://stackoverflow.com/a/36141175 char should_reduce_channels_from_4to3 = 0; // uncached: @@ -575,11 +552,6 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley if (channels != 3 && channels != 4) { throw file_error("Channels not 3 or 4: " + std::to_string(channels)); - } - if (channels == 4) - { - should_reduce_channels_from_4to3 = 1; - allocate_length = tw * th * 4 * sizeof(unsigned char); }*/ // cached: @@ -643,12 +615,12 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley if (!rt->data) throw file_error(string("FATAL : BioFormatsImage read_region => allocation memory ERROR")); -#ifdef DEBUG_OSI +#ifdef BENCHMARK auto start = std::chrono::high_resolution_clock::now(); #endif int bytes_received = bfi.open_bytes(tx0, ty0, tw, th); -#ifdef DEBUG_OSI +#ifdef BENCHMARK auto finish = std::chrono::high_resolution_clock::now(); std::chrono::duration elapsed = finish - start; milliseconds += elapsed.count(); @@ -664,8 +636,8 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley if (bytes_received != channels_internal * bytespc_internal * tw * th) { - cerr << "got an unexpected number of bytes\n"; - throw file_error("ERROR: expected len " + std::to_string(channels * bpc / 8 * tw * th) + " but got " + std::to_string(bytes_received)); + cerr << "got an unexpected number of bytes: " << bytes_received << " instead of " << channels * bytespc_internal * tw * th << endl; + throw file_error("ERROR: expected len " + std::to_string(channels * bytespc_internal * tw * th) + " but got " + std::to_string(bytes_received)); } // Note: please don't copy anything more than // bytes_received when it's positive as the rest contains junk from the past @@ -776,7 +748,7 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley for (int i = 0; i < pixels * channels_internal; i++) { // Unnecessary copy rather than considering these offset and coefficient - // variables when we'll already copy, but this allows readability + // variables when we'll already copy in the next steps, but this allows readability // and not making the common 8-bit reading slower buf[i] = buf[coefficient * i + offset]; } @@ -796,9 +768,9 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley if (should_interleave) { - char *red = bfi.communication_buffer(); - char *green = &bfi.communication_buffer()[pixels]; - char *blue = &bfi.communication_buffer()[2 * pixels]; + char *red = buf; + char *green = &buf[pixels]; + char *blue = &buf[2 * pixels]; for (int i = 0; i < pixels; i++) { @@ -819,14 +791,14 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley { for (int i = 0; i < pixels; i++) { - data_out[3 * i] = bfi.communication_buffer()[4 * i]; - data_out[3 * i + 1] = bfi.communication_buffer()[4 * i + 1]; - data_out[3 * i + 2] = bfi.communication_buffer()[4 * i + 2]; + data_out[3 * i] = buf[4 * i]; + data_out[3 * i + 1] = buf[4 * i + 1]; + data_out[3 * i + 2] = buf[4 * i + 2]; } } else { - memcpy(rt->data, bfi.communication_buffer(), bytes_received); + memcpy(data_out, buf, bytes_received); } } diff --git a/iipsrv/src/BioFormatsImage.h b/iipsrv/src/BioFormatsImage.h index b84c209..67763ae 100644 --- a/iipsrv/src/BioFormatsImage.h +++ b/iipsrv/src/BioFormatsImage.h @@ -35,9 +35,9 @@ class BioFormatsImage : public IIPImage int channels_internal; int pick_byte = 0; // 0 for pick first (from big endian serialized), 1 for pick last -#ifdef DEBUG_OSI +#ifdef BENCHMARK int milliseconds = 0; -#endif DEBUG_OSI +#endif // Unimplemented methods in line with OpenslideImage.h: // void read(...); diff --git a/iipsrv/src/BioFormatsInstance.cc b/iipsrv/src/BioFormatsInstance.cc index 613a878..e93980b 100644 --- a/iipsrv/src/BioFormatsInstance.cc +++ b/iipsrv/src/BioFormatsInstance.cc @@ -16,6 +16,6 @@ BioFormatsInstance::BioFormatsInstance() bfi_communication_buffer_len); if (error) { - throw runtime_error("BioFormatsInstance.cc error: " + std::string(error->description)); + throw std::runtime_error("BioFormatsInstance.cc error: " + std::string(error->description)); } } diff --git a/iipsrv/src/BioFormatsThread.cc b/iipsrv/src/BioFormatsThread.cc index 121bab6..b5922f7 100644 --- a/iipsrv/src/BioFormatsThread.cc +++ b/iipsrv/src/BioFormatsThread.cc @@ -3,6 +3,8 @@ */ #include "BioFormatsThread.h" +#include +#include BioFormatsThread::BioFormatsThread() { @@ -12,8 +14,8 @@ BioFormatsThread::BioFormatsThread() char *cpdir = getenv("BFBRIDGE_CLASSPATH"); if (!cpdir || cpdir[0] == '\0') { - cerr << "Please set BFBRIDGE_CLASSPATH to a single directory where jar files can be found.\n"; - throw runtime_error("Please set BFBRIDGE_CLASSPATH to a single directory where jar files can be found.\n") + std::cerr << "Please set BFBRIDGE_CLASSPATH to a single directory where jar files can be found.\n"; + throw std::runtime_error("Please set BFBRIDGE_CLASSPATH to a single directory where jar files can be found.\n"); } // Optional: @@ -25,8 +27,8 @@ BioFormatsThread::BioFormatsThread() bfbridge_error_t *error = bfbridge_make_vm(&bfvm, cpdir, cachedir); if (error) { - fprintf(stderr, "BioFormatsThread.cc bfbridge_make_vm error: %s\n", error->description); - throw "BioFormatsThread.cc bfbridge_make_vm error: \n" + std::string(error->description); + std::cerr << "BioFormatsThread.cc bfbridge_make_vm error: " << error->description << std::endl; + throw std::runtime_error("BioFormatsThread.cc bfbridge_make_vm error: \n" + std::string(error->description)); } // Expensive function being used from a header-only library. @@ -34,7 +36,7 @@ BioFormatsThread::BioFormatsThread() error = bfbridge_make_thread(&bfthread, &bfvm); if (error) { - fprintf(stderr, "BioFormatsThread.cc bfbridge_make_thread error: %s\n", error->description); + std::cerr << "BioFormatsThread.cc bfbridge_make_thread error: " << error->description << std::endl; throw "BioFormatsThread.cc bfbridge_make_thread error: \n" + std::string(error->description); } } diff --git a/iipsrv/src/BioFormatsThread.h b/iipsrv/src/BioFormatsThread.h index 7394cf3..a707a2a 100644 --- a/iipsrv/src/BioFormatsThread.h +++ b/iipsrv/src/BioFormatsThread.h @@ -5,12 +5,18 @@ #ifndef BIOFORMATSTHREAD_H #define BIOFORMATSTHREAD_H +#ifndef BFBRIDGE_INLINE +#define BFBRIDGE_INLINE +#endif + +#ifndef BFBRIDGE_KNOW_BUFFER_LEN +#define BFBRIDGE_KNOW_BUFFER_LEN +#endif + #include #include #include -#define BFBRIDGE_INLINE -#define BFBRIDGE_KNOW_BUFFER_LEN -#include "../BFBridge/bfbridge_basiclib.h" +#include "../../BFBridge/c/bfbridge_basiclib.h" class BioFormatsThread { diff --git a/iipsrv/src/IIPImage.cc b/iipsrv/src/IIPImage.cc index 762bdad..b78eb0a 100644 --- a/iipsrv/src/IIPImage.cc +++ b/iipsrv/src/IIPImage.cc @@ -129,7 +129,7 @@ void IIPImage::testImageType() throw(file_error) if ( vendor != NULL ) { if ( !strcmp(vendor, "generic-tiff") ) { // Have generic TIFF, so use iipsrv reader - format = TIF + format = TIF; return; } // OpenSlide but not generic tiff @@ -149,6 +149,7 @@ void IIPImage::testImageType() throw(file_error) format = BIOFORMATS; return; } + BioFormatsManager::free( std::move(bfi) ); } @@ -206,7 +207,7 @@ void IIPImage::testImageType() throw(file_error) suffix=="bif") format = OPENSLIDE; else if ( - suffix = "v3draw" || + suffix == "v3draw" || suffix == "ano" || suffix == "cfg" || suffix == "csv" || diff --git a/iipsrv/src/Makefile.am b/iipsrv/src/Makefile.am index eb14ee1..ddbb10d 100644 --- a/iipsrv/src/Makefile.am +++ b/iipsrv/src/Makefile.am @@ -10,9 +10,11 @@ LIBS = @LIBS@ @LIBFCGI_LIBS@ @DL_LIBS@ @JPEG_LIBS@ @TIFF_LIBS@ -lm -lopenslide AM_LDFLAGS = @LIBFCGI_LDFLAGS@ -rpath $(JAVA_HOME)/lib/server AM_CPPFLAGS = -I/usr/local/include/openslide -DBFBRIDGE_INLINE +AM_CFLAGS = -DBFBRIDGE_INLINE + # jni-md.h should also be included hence the platform paths, see link in https://stackoverflow.com/a/37029528 # https://github.com/openjdk/jdk/blob/6e3cc131daa9f3b883164333bdaad7aa3a6ca018/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/utilities/PlatformInfo.java#L32 -AM_CPPFLAGS += -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux -I$(JAVA_HOME)/include/darwin -I$(JAVA_HOME)/include/win32 -I$(JAVA_HOME)/include/bsd -DBFBRIDGE_INLINE +AM_CPPFLAGS += -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux -I$(JAVA_HOME)/include/darwin -I$(JAVA_HOME)/include/win32 -I$(JAVA_HOME)/include/bsd iipsrv_fcgi_LDADD = Main.o From 917448f839b707f730dd37964927e1947d03265e Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Sun, 27 Aug 2023 21:35:25 +0100 Subject: [PATCH 10/17] more --- iipsrv/src/BioFormatsImage.cc | 6 +++--- iipsrv/src/IIPImage.cc | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/iipsrv/src/BioFormatsImage.cc b/iipsrv/src/BioFormatsImage.cc index 70de64d..926116a 100644 --- a/iipsrv/src/BioFormatsImage.cc +++ b/iipsrv/src/BioFormatsImage.cc @@ -184,13 +184,13 @@ void BioFormatsImage::loadImageInfo(int x, int y) throw(file_error) throw file_error("Error while getting bits per pixel: " + err); } -#define too_big (tile_width * tile_height * bytespc_internal * bfi.get_rgb_channel_count() > bfi_communication_buffer_len) - while (too_big) + // too big for our preallocated buffer? use a smaller square + while ( + tile_width * tile_height * bytespc_internal * channels_internal > bfi_communication_buffer_len) { tile_height >>= 1; tile_width >>= 1; } -#undef too_big // save the bioformats dimensions. std::vector bioformats_widths, bioformats_heights; diff --git a/iipsrv/src/IIPImage.cc b/iipsrv/src/IIPImage.cc index b78eb0a..68167db 100644 --- a/iipsrv/src/IIPImage.cc +++ b/iipsrv/src/IIPImage.cc @@ -166,8 +166,8 @@ void IIPImage::testImageType() throw(file_error) return; } } + else format = UNSUPPORTED; - format = UNSUPPORTED; } else{ From cbe67a69a7819a14fee770c91d4046c04d65d8d8 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Sun, 27 Aug 2023 21:37:28 +0100 Subject: [PATCH 11/17] Update BioFormatsImage.cc --- iipsrv/src/IIPImage.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iipsrv/src/IIPImage.cc b/iipsrv/src/IIPImage.cc index 68167db..3f0fad3 100644 --- a/iipsrv/src/IIPImage.cc +++ b/iipsrv/src/IIPImage.cc @@ -166,7 +166,7 @@ void IIPImage::testImageType() throw(file_error) return; } } - else format = UNSUPPORTED; + format = UNSUPPORTED; } else{ From b6b29d0d084f86af5252070f928e96828cd22554 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Sun, 27 Aug 2023 21:38:31 +0100 Subject: [PATCH 12/17] Update BioFormatsImage.cc --- iipsrv/src/BioFormatsImage.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iipsrv/src/BioFormatsImage.cc b/iipsrv/src/BioFormatsImage.cc index 926116a..6a39dea 100644 --- a/iipsrv/src/BioFormatsImage.cc +++ b/iipsrv/src/BioFormatsImage.cc @@ -768,9 +768,9 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley if (should_interleave) { - char *red = buf; - char *green = &buf[pixels]; - char *blue = &buf[2 * pixels]; + unsigned char *red = buf; + unsigned char *green = &buf[pixels]; + unsigned char *blue = &buf[2 * pixels]; for (int i = 0; i < pixels; i++) { From 2912c244327ab0b87e431e4e233cd1e61f601f5a Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:44:47 +0100 Subject: [PATCH 13/17] update --- iipsrv/src/BioFormatsImage.cc | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/iipsrv/src/BioFormatsImage.cc b/iipsrv/src/BioFormatsImage.cc index 6a39dea..9250c93 100644 --- a/iipsrv/src/BioFormatsImage.cc +++ b/iipsrv/src/BioFormatsImage.cc @@ -135,9 +135,6 @@ void BioFormatsImage::loadImageInfo(int x, int y) throw(file_error) cerr << "isInterleaved: " << (int)bfi.is_interleaved() << endl; #endif - // iipsrv takes 1 or 3 only. tell iip that we'll give it a 3-channel image - channels = 3; - // Note: this code assumes that the number of channels is the same among resolutions // otherwise should be moved to getnativetile channels_internal = bfi.get_rgb_channel_count(); @@ -156,6 +153,9 @@ void BioFormatsImage::loadImageInfo(int x, int y) throw(file_error) } } + // iipsrv takes 1 or 3 only. we may need to reduce to 3 from 4, but the end result would be 3. + channels = (channels_internal == 1) ? 1 : 3; + if (bfi.is_indexed_color() && !bfi.is_false_color()) { // We must read from the table @@ -166,16 +166,17 @@ void BioFormatsImage::loadImageInfo(int x, int y) throw(file_error) throw file_error("Unimplemented: False color image"); } + /* This is usually true but for single channeled images 'C' is the last if (bfi.get_dimension_order().length() && bfi.get_dimension_order()[2] != 'C') { logfile << "Unimplemented: unfamiliar dimension order " << bfi.get_dimension_order() << endl; throw file_error("Unimplemented: unfamiliar dimension order " + std::string(bfi.get_dimension_order())); - } + }*/ // bfi.get_bytes_per_pixel actually gives bits per channel per pixel, so don't divide by channels int bytespc_internal = bfi.get_bytes_per_pixel(); bpc = 8; - colourspace = sRGB; + colourspace = (channels == 1) ? sRGB : GREYSCALE; if (bytespc_internal <= 0) { @@ -525,10 +526,10 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley }*/ // create the RawTile object - RawTilePtr rt(new RawTile(tiley * ntlx + tilex, iipres, 0, 0, tw, th, 3, bpc)); + RawTilePtr rt(new RawTile(tiley * ntlx + tilex, iipres, 0, 0, tw, th, channels, bpc)); // compute the size, etc - rt->dataLength = tw * th * 3 * sizeof(unsigned char); + rt->dataLength = tw * th * channels * sizeof(unsigned char); rt->filename = getImagePath(); rt->timestamp = timestamp; @@ -558,7 +559,7 @@ RawTilePtr BioFormatsImage::getNativeTile(const size_t tilex, const size_t tiley should_reduce_channels_from_4to3 = channels_internal == 4; // Known to differ among resolutions - int should_interleave = !bfi.is_interleaved(); + int should_interleave = !bfi.is_interleaved() && channels != 1; // Perhaps the next three can be cached // https://github.com/ome/bioformats/blob/metadata54/components/formats-api/src/loci/formats/FormatTools.java#L76 From 08fc35d3ee283d908298b01e299598b39dde6435 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:29:24 +0100 Subject: [PATCH 14/17] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7aa54d6..4d80252 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,6 @@ Makefile.in /ltmain.sh /m4/ /missing + +# BioFormats either copied by Docker of copied manually and locally +BFBridge/ From d06c41d3135f1880675842a7c57413b0a63d86e2 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Wed, 6 Sep 2023 12:17:36 +0100 Subject: [PATCH 15/17] Fix wrong message in logs --- iipsrv/src/BioFormatsImage.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iipsrv/src/BioFormatsImage.cc b/iipsrv/src/BioFormatsImage.cc index 9250c93..404e53c 100644 --- a/iipsrv/src/BioFormatsImage.cc +++ b/iipsrv/src/BioFormatsImage.cc @@ -41,7 +41,7 @@ void BioFormatsImage::openImage() throw(file_error) logfile << "ERROR: encountered error: " << error << " while opening " << filename << " with BioFormats: " << endl << flush; - throw file_error(string("Error opening '" + filename + "' with OpenSlide, error " + error)); + throw file_error(string("Error opening '" + filename + "' with BioFormats, error " + error)); } #ifdef DEBUG_OSI logfile << "BioFormats :: openImage() :: " << timer.getTime() << " microseconds" << endl From 4c9861d5dbd14f08392815c5b6dc922bf85bca36 Mon Sep 17 00:00:00 2001 From: CGDogan <126820728+CGDogan@users.noreply.github.com> Date: Mon, 18 Sep 2023 17:43:48 +0100 Subject: [PATCH 16/17] Loglevel WARN --- apache2-iipsrv-fcgid.conf | 1 + fcgid.conf | 1 + iip_httpd.conf | 1 + 3 files changed, 3 insertions(+) diff --git a/apache2-iipsrv-fcgid.conf b/apache2-iipsrv-fcgid.conf index 7eee54c..8ac0a88 100644 --- a/apache2-iipsrv-fcgid.conf +++ b/apache2-iipsrv-fcgid.conf @@ -29,6 +29,7 @@ FcgidInitialEnv LD_LIBRARY_PATH "/usr/local/lib" FcgidInitialEnv MAX_TILE_CACHE_SIZE "64" FcgidInitialEnv BFBRIDGE_CACHEDIR "/tmp/" FcgidInitialEnv BFBRIDGE_CLASSPATH "/usr/lib/java" +FcgidInitialEnv BFBRIDGE_LOGLEVEL "WARN" # Define the idle timeout as unlimited and the number of # processes we want diff --git a/fcgid.conf b/fcgid.conf index 7f3a068..824f7e1 100644 --- a/fcgid.conf +++ b/fcgid.conf @@ -29,6 +29,7 @@ FcgidInitialEnv MAX_TILE_CACHE_SIZE "64" FcgidInitialEnv CORS "*" FcgidInitialEnv BFBRIDGE_CACHEDIR "/tmp/" FcgidInitialEnv BFBRIDGE_CLASSPATH "/usr/lib/java" +FcgidInitialEnv BFBRIDGE_LOGLEVEL "WARN" # Define the idle timeout as unlimited and the number of # processes we want diff --git a/iip_httpd.conf b/iip_httpd.conf index 8e3709f..415c799 100644 --- a/iip_httpd.conf +++ b/iip_httpd.conf @@ -30,5 +30,6 @@ FastCgiServer /var/www/localhost/fcgi-bin/iipsrv.fcgi \ -initial-env CORS=* \ -initial-env BFBRIDGE_CACHEDIR=/tmp/ \ -initial-env BFBRIDGE_CLASSPATH=/usr/lib/java \ +-initial-env BFBRIDGE_LOGLEVEL=WARN \ -listen-queue-depth 2048 \ -processes 1 From bade175f3e7421be2f8cdece4b77e75b61f0f5c8 Mon Sep 17 00:00:00 2001 From: Birm Date: Thu, 9 Nov 2023 14:21:47 -0500 Subject: [PATCH 17/17] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 4213d86..b60c65b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2019, caMicroscope +Copyright (c) 2018-2023, caMicroscope All rights reserved. Redistribution and use in source and binary forms, with or without