Skip to content

Commit

Permalink
(PPM) Use custom TIFF reader for loading cell map (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
csparker247 authored Nov 14, 2023
1 parent 9f84190 commit bda28ec
Show file tree
Hide file tree
Showing 7 changed files with 739 additions and 26 deletions.
1 change: 1 addition & 0 deletions core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ set(test_srcs
test/LoggingTest.cpp
test/SignalsTest.cpp
test/IterationTest.cpp
test/TIFFIOTest.cpp
)

# Add a test executable for each src
Expand Down
11 changes: 4 additions & 7 deletions core/include/vc/core/io/PointSetIO.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -450,14 +450,11 @@ class PointSetIO

// Read data
T t;
auto nbytes = header.dim * typeBytes;
std::size_t nbytes = header.width * header.dim * typeBytes;
std::vector<T> points(header.width, 0);
points.reserve(header.width);
for (size_t h = 0; h < header.height; ++h) {
std::vector<T> points;
points.reserve(header.width);
for (size_t w = 0; w < header.width; ++w) {
infile.read(reinterpret_cast<char*>(t.val), nbytes);
points.push_back(t);
}
infile.read(reinterpret_cast<char*>(points.data()), nbytes);
ps.pushRow(points);
}

Expand Down
30 changes: 28 additions & 2 deletions core/include/vc/core/io/TIFFIO.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,37 @@ enum class Compression {
JP2000 = 34712
};

/**
* @brief Read a TIFF file
*
* Reads Gray, Gray+Alpha, RGB, and RGBA TIFF images. Supports 8, 16, and
* 32-bit integer types as well as 32-bit float types. 3 and 4 channel images
* will be returned with a BGR channel order, except for 8-bit and 16-bit
* signed integer types which will be returned with an RGB channel order.
*
* Only supports single image TIFF files with scanline encoding and a
* contiguous planar configuration (this matches the format written by
* WriteTIFF). Unless you need to read some obscure image type (e.g. 32-bit
* float or signed integer images), it's generally preferable to use cv::imread.
*
* If the raw size of the image (width x height x channels x bytes-per-sample)
* is >= 4GB, the TIFF will be written using the BigTIFF extension to the TIFF
* format.
*
* @param path Path to TIFF file
* @throws std::runtime_error Unrecoverable read errors
*/
auto ReadTIFF(const volcart::filesystem::path& path) -> cv::Mat;

/**
* @brief Write a TIFF image to file
*
* Supports writing floating point and signed integer TIFFs, in addition to
* unsigned 8 & 16 bit integer types. Also supports 1-4 channel images.
* Writes Gray, Gray+Alpha, RGB, and RGBA TIFF images. Supports 8, 16, and
* 32-bit integer types as well as 32-bit float types. 3 and 4 channel images
* are assumed to have a BGR channel order, except for 8-bit and 16-bit signed
* integer types which are not supported.
*
* @throws std::runtime_error All writing errors
*/
void WriteTIFF(
const volcart::filesystem::path& path,
Expand Down
10 changes: 8 additions & 2 deletions core/src/PerPixelMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,18 @@ auto PerPixelMap::ReadPPM(const fs::path& path) -> PerPixelMap
ppm.height_ = ppm.map_.height();
ppm.width_ = ppm.map_.width();

ppm.mask_ = cv::imread(MaskPath(path).string(), cv::IMREAD_GRAYSCALE);
auto maskPath = MaskPath(path);
if (fs::exists(maskPath)) {
ppm.mask_ = cv::imread(maskPath.string(), cv::IMREAD_GRAYSCALE);
}
if (ppm.mask_.empty()) {
Logger()->warn("Failed to read mask: {}", MaskPath(path).string());
}

ppm.cellMap_ = cv::imread(CellMapPath(path).string(), cv::IMREAD_UNCHANGED);
auto cellMapPath = CellMapPath(path);
if (fs::exists(cellMapPath)) {
ppm.cellMap_ = tiffio::ReadTIFF(cellMapPath);
}
if (ppm.cellMap_.empty()) {
Logger()->warn(
"Failed to read cell map: {}", CellMapPath(path).string());
Expand Down
154 changes: 143 additions & 11 deletions core/src/TIFFIO.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,132 @@

#include "vc/core/Version.hpp"
#include "vc/core/io/FileExtensionFilter.hpp"
#include "vc/core/util/Logging.hpp"

// Wrapping in a namespace to avoid define collisions
namespace lt
{
#include <tiffio.h>
}

namespace vc = volcart;
namespace tio = volcart::tiffio;
namespace fs = volcart::filesystem;

namespace
{
// Return a CV Mat type using TIF type (signed, unsigned, float),
// bit-depth, and number of channels
auto GetCVMatType(
const uint16_t tifType, const uint16_t depth, const uint16_t channels)
-> int
{
switch (depth) {
case 8:
if (tifType == SAMPLEFORMAT_INT) {
return CV_MAKETYPE(CV_8S, channels);
} else {
return CV_MAKETYPE(CV_8U, channels);
}
case 16:
if (tifType == SAMPLEFORMAT_INT) {
return CV_MAKETYPE(CV_16S, channels);
} else {
return CV_MAKETYPE(CV_16U, channels);
}
case 32:
if (tifType == SAMPLEFORMAT_INT) {
return CV_MAKETYPE(CV_32S, channels);
} else {
return CV_MAKETYPE(CV_32F, channels);
}
default:
return CV_8UC3;
}
}

constexpr std::size_t MAX_TIFF_BYTES{4'294'967'296};
constexpr std::size_t BITS_PER_BYTE{8};

inline auto NeedBigTIFF(
std::size_t w, std::size_t h, std::size_t cns, std::size_t bps) -> bool
{
const std::size_t bytes = w * h * cns * bps / BITS_PER_BYTE;
return bytes >= MAX_TIFF_BYTES;
}

} // namespace

auto tio::ReadTIFF(const volcart::filesystem::path& path) -> cv::Mat
{
// Make sure input file exists
if (!fs::exists(path)) {
throw std::runtime_error("File does not exist");
}

// Open the file read-only
lt::TIFF* tif = lt::TIFFOpen(path.c_str(), "r");
if (tif == nullptr) {
throw std::runtime_error("Failed to open tif");
}

// Get metadata
uint32_t width = 0;
uint32_t height = 0;
uint16_t type = 1;
uint16_t depth = 1;
uint16_t channels = 1;
uint16_t config = 0;
TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &height);
TIFFGetField(tif, TIFFTAG_SAMPLEFORMAT, &type);
TIFFGetField(tif, TIFFTAG_BITSPERSAMPLE, &depth);
TIFFGetField(tif, TIFFTAG_SAMPLESPERPIXEL, &channels);
TIFFGetField(tif, TIFFTAG_PLANARCONFIG, &config);
auto cvType = ::GetCVMatType(type, depth, channels);

// Construct the mat
auto h = static_cast<int>(height);
auto w = static_cast<int>(width);
cv::Mat img = cv::Mat::zeros(h, w, cvType);

// Read the rows
auto bufferSize = static_cast<size_t>(lt::TIFFScanlineSize(tif));
std::vector<char> buffer(bufferSize + 4);
if (config == PLANARCONFIG_CONTIG) {
for (auto row = 0; row < height; row++) {
lt::TIFFReadScanline(tif, &buffer[0], row);
std::memcpy(img.ptr(row), &buffer[0], bufferSize);
}
} else if (config == PLANARCONFIG_SEPARATE) {
std::runtime_error(
"Unsupported TIFF planar configuration: PLANARCONFIG_SEPARATE");
}

// Do channel conversion
auto cvtNeeded = img.channels() == 3 or img.channels() == 4;
auto cvtSupported = img.depth() != CV_8S and img.depth() != CV_16S and
img.depth() != CV_32S;
if (cvtNeeded) {
if (cvtSupported) {
if (img.channels() == 3) {
cv::cvtColor(img, img, cv::COLOR_RGB2BGR);
} else if (img.channels() == 4) {
cv::cvtColor(img, img, cv::COLOR_RGBA2BGRA);
}
} else {
vc::Logger()->warn(
"[TIFFIO] RGB->BGR conversion for signed 8-bit and 16-bit "
"images is not supported. Image will be loaded with RGB "
"element order.");
}
}

lt::TIFFClose(tif);

return img;
}

// Write a TIFF to a file. This implementation heavily borrows from how OpenCV's
// TIFFEncoder writes to the TIFF
void tio::WriteTIFF(
Expand Down Expand Up @@ -88,8 +204,34 @@ void tio::WriteTIFF(
throw std::runtime_error("Unsupported number of channels");
}

// Get working copy with converted channels if an RGB-type image
auto cvtNeeded = img.channels() == 3 or img.channels() == 4;
auto cvtSupported = img.depth() != CV_8S and img.depth() != CV_16S and
img.depth() != CV_32S;
cv::Mat imgCopy;
if (cvtNeeded and cvtSupported) {
if (img.channels() == 3) {
cv::cvtColor(img, imgCopy, cv::COLOR_BGR2RGB);
} else if (img.channels() == 4) {
cv::cvtColor(img, imgCopy, cv::COLOR_BGRA2RGBA);
}
} else if (cvtNeeded) {
throw std::runtime_error(
"BGR->RGB conversion for signed 8-bit and 16-bit images is not "
"supported.");
} else {
imgCopy = img;
}

// Estimated file size in bytes
auto useBigTIFF = ::NeedBigTIFF(width, height, channels, bitsPerSample);
if (useBigTIFF) {
Logger()->warn("File estimate >= 4GB. Writing as BigTIFF.");
}

// Open the file
auto out = lt::TIFFOpen(path.c_str(), "w");
const std::string mode = (useBigTIFF) ? "w8" : "w";
auto* out = lt::TIFFOpen(path.c_str(), mode.c_str());
if (out == nullptr) {
throw std::runtime_error("Failed to open file for writing");
}
Expand Down Expand Up @@ -122,16 +264,6 @@ void tio::WriteTIFF(
auto bufferSize = static_cast<size_t>(lt::TIFFScanlineSize(out));
std::vector<char> buffer(bufferSize + 32);

// Get working copy with converted channels if an RGB-type image
cv::Mat imgCopy;
if (img.channels() == 3) {
cv::cvtColor(img, imgCopy, cv::COLOR_BGR2RGB);
} else if (img.channels() == 4) {
cv::cvtColor(img, imgCopy, cv::COLOR_BGRA2RGBA);
} else {
imgCopy = img;
}

// For each row
for (unsigned row = 0; row < height; row++) {
std::memcpy(&buffer[0], imgCopy.ptr(row), bufferSize);
Expand Down
26 changes: 22 additions & 4 deletions core/test/PerPixelMapTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,24 @@ using namespace volcart;
TEST(PerPixelMap, WriteRead)
{
// Build a PPM
PerPixelMap ppm(10, 10);
PerPixelMap ppm(100, 100);
cv::Mat mask = cv::Mat::zeros(100, 100, CV_8UC1);
cv::Mat cellMap = cv::Mat(100, 100, CV_32SC1);
cellMap = cv::Scalar::all(-1);
for (auto y = 0; y < 10; ++y) {
for (auto x = 0; x < 10; ++x) {
auto outY = 46 + y;
auto outX = 46 + x;
auto dx = static_cast<double>(x);
auto dy = static_cast<double>(y);
ppm(y, x) = {dx, dy, (dx + dy) / 2.0, dx, dy, (dx + dy) / 2.0};
ppm(outY, outX) = {dx, dy, (dx + dy) / 2.0,
dx, dy, (dx + dy) / 2.0};
mask.at<std::uint8_t>(outY, outX) = 255U;
cellMap.at<std::int32_t>(outY, outX) = y + 10;
}
}
ppm.setMask(mask);
ppm.setCellMap(cellMap);

// Write the PPM
std::string path{"vc_core_PerPixelMap_WriteRead.ppm"};
Expand All @@ -25,9 +35,17 @@ TEST(PerPixelMap, WriteRead)
EXPECT_NO_THROW(result = PerPixelMap::ReadPPM(path));

// Test the values
for (auto y = 0; y < 10; ++y) {
for (auto x = 0; x < 10; ++x) {
for (auto y = 0; y < 100; ++y) {
for (auto x = 0; x < 100; ++x) {
EXPECT_EQ(result(y, x), ppm(y, x));
}
}

// Test the mask
cv::Mat diff = ppm.mask() != result.mask();
EXPECT_EQ(cv::countNonZero(diff), 0);

// Test the cell map
diff = ppm.cellMap() != result.cellMap();
EXPECT_EQ(cv::countNonZero(diff), 0);
}
Loading

0 comments on commit bda28ec

Please sign in to comment.