Skip to content

Commit ac4eb59

Browse files
committed
feat(tiff): better YCbCr handling; impl. of ReferenceBlackWhite tag
1 parent 1e619b5 commit ac4eb59

File tree

2 files changed

+84
-18
lines changed

2 files changed

+84
-18
lines changed

include/tev/imageio/ImageLoader.h

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@
3535
namespace tev {
3636

3737
template <bool SRGB_TO_LINEAR = false>
38-
Task<void> yCbCrToRgb(float* data, const nanogui::Vector2i& size, size_t numSamplesPerPixel, int priority) {
38+
Task<void> yCbCrToRgb(
39+
float* data,
40+
const nanogui::Vector2i& size,
41+
size_t numSamplesPerPixel,
42+
int priority,
43+
const nanogui::Vector4f& coeffs = {1.402f, -0.344136f, -0.714136f, 1.772f}
44+
) {
3945
if (numSamplesPerPixel < 3) {
4046
tlog::warning() << "Cannot convert from YCbCr to RGB: not enough channels.";
4147
co_return;
@@ -46,15 +52,15 @@ Task<void> yCbCrToRgb(float* data, const nanogui::Vector2i& size, size_t numSamp
4652
0,
4753
numPixels,
4854
numPixels * 3,
49-
[numSamplesPerPixel, data](size_t i) {
55+
[&coeffs, numSamplesPerPixel, data](size_t i) {
5056
const float Y = data[i * numSamplesPerPixel + 0];
5157
const float Cb = data[i * numSamplesPerPixel + 1] - 0.5f;
5258
const float Cr = data[i * numSamplesPerPixel + 2] - 0.5f;
5359

5460
// BT.601 conversion
55-
float r = Y + 1.402f * Cr;
56-
float g = Y - 0.344136f * Cb - 0.714136f * Cr;
57-
float b = Y + 1.772f * Cb;
61+
float r = Y + coeffs[0] * Cr;
62+
float g = Y + coeffs[1] * Cb + coeffs[2] * Cr;
63+
float b = Y + coeffs[3] * Cb;
5864

5965
if constexpr (SRGB_TO_LINEAR) {
6066
r = toLinear(r);

src/imageio/TiffImageLoader.cpp

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,56 @@ Task<void> postprocessRgb(
10761076
const int priority
10771077
) {
10781078
const Vector2i size = resultData.size();
1079+
const size_t numPixels = (size_t)size.x() * size.y();
1080+
1081+
const size_t bps = photometric == PHOTOMETRIC_PALETTE ? 16 : dataBitsPerSample;
1082+
1083+
if (float* referenceBw; TIFFGetField(tif, TIFFTAG_REFERENCEBLACKWHITE, &referenceBw) && referenceBw) {
1084+
const size_t maxVal = (1ull << bps) - 1;
1085+
1086+
const bool isYCbCr = photometric == PHOTOMETRIC_YCBCR;
1087+
const Vector3f codingRange = {(float)maxVal, isYCbCr ? 127.0f : (float)maxVal, isYCbCr ? 127.0f : (float)maxVal};
1088+
1089+
const Vector3f refBlack = Vector3f{referenceBw[0], referenceBw[2], referenceBw[4]};
1090+
const Vector3f refWhite = Vector3f{referenceBw[1], referenceBw[3], referenceBw[5]};
1091+
const Vector3f invRange = 1.0f / (refWhite - refBlack);
1092+
1093+
const Vector3f offset = isYCbCr ? Vector3f{0.0f, 0.5f, 0.5f} : Vector3f{0.0f};
1094+
1095+
const Vector3f totalScale = codingRange * invRange / maxVal;
1096+
1097+
tlog::debug() << fmt::format("Found reference black/white: black={} white={}", refBlack, refWhite);
1098+
1099+
co_await ThreadPool::global().parallelForAsync<size_t>(
1100+
0,
1101+
numPixels,
1102+
numPixels * numColorChannels,
1103+
[&](size_t i) {
1104+
for (int c = 0; c < numColorChannels; ++c) {
1105+
const size_t idx = i * numRgbaChannels + c;
1106+
floatRgbaData[idx] = (floatRgbaData[idx] * maxVal - refBlack[c]) * totalScale[c] + offset[c];
1107+
}
1108+
},
1109+
priority
1110+
);
1111+
}
1112+
1113+
if (photometric == PHOTOMETRIC_YCBCR && numRgbaChannels >= 3) {
1114+
Vector4f coeffs = {1.402f, -0.344136f, -0.714136f, 1.772f};
1115+
if (float* yCbCrCoeffs; TIFFGetField(tif, TIFFTAG_YCBCRCOEFFICIENTS, &yCbCrCoeffs) && yCbCrCoeffs) {
1116+
const Vector3f K = {yCbCrCoeffs[0], yCbCrCoeffs[1], yCbCrCoeffs[2]};
1117+
coeffs = {
1118+
2.0f * (1.0f - K.x()),
1119+
-2.0f * K.z() * (1.0f - K.z()) / K.y(),
1120+
-2.0f * K.x() * (1.0f - K.x()) / K.y(),
1121+
2.0f * (1.0f - K.z()),
1122+
};
1123+
1124+
tlog::debug() << fmt::format("Found YCbCr coefficients: {} -> {}", K, coeffs);
1125+
}
1126+
1127+
co_await yCbCrToRgb(floatRgbaData.data(), size, numRgbaChannels, priority, coeffs);
1128+
}
10791129

10801130
chroma_t chroma = rec709Chroma();
10811131
if (float* primaries; TIFFGetField(tif, TIFFTAG_PRIMARYCHROMATICITIES, &primaries) && primaries) {
@@ -1114,7 +1164,6 @@ Task<void> postprocessRgb(
11141164
}
11151165
}
11161166

1117-
const size_t bps = photometric == PHOTOMETRIC_PALETTE ? 16 : dataBitsPerSample;
11181167
const size_t maxIdx = (1ull << bps) - 1;
11191168

11201169
Vector3i transferRangeBlack = {0};
@@ -1132,7 +1181,6 @@ Task<void> postprocessRgb(
11321181

11331182
const Vector3f scale = Vector3f(1.0f) / Vector3f(transferRangeWhite - transferRangeBlack);
11341183

1135-
const size_t numPixels = (size_t)size.x() * size.y();
11361184
co_await ThreadPool::global().parallelForAsync<size_t>(
11371185
0,
11381186
numPixels,
@@ -1222,7 +1270,7 @@ Task<ImageData> decodeJpeg(
12221270
) {
12231271
vector<uint8_t> stream;
12241272
if (jpegTables.size() > 4) {
1225-
tlog::debug() << "JPEG tables found; prepending to compressed data...";
1273+
// tlog::debug() << "JPEG tables found; prepending to compressed data...";
12261274

12271275
const uint32_t tablesPayloadLen = jpegTables.size() - 4;
12281276
const uint8_t* tablesPayload = jpegTables.data() + 2;
@@ -1336,10 +1384,6 @@ Task<ImageData> decodeJpeg(
13361384
decompressGuard.disarm();
13371385
jpeg_finish_decompress(&cinfo);
13381386

1339-
if (photometric == PHOTOMETRIC_YCBCR && result.channels.size() >= 3) {
1340-
co_await yCbCrToRgb(result.channels.front().floatData(), result.channels.front().size(), result.channels.size(), priority);
1341-
}
1342-
13431387
*nestedBitsPerSample = (size_t)precision;
13441388
co_return result;
13451389
}
@@ -1429,10 +1473,6 @@ Task<ImageData> readTiffImage(
14291473
if (decodeRaw) {
14301474
bitsPerSample = 32;
14311475
sampleFormat = SAMPLEFORMAT_IEEEFP;
1432-
1433-
if (photometric == PHOTOMETRIC_YCBCR) {
1434-
photometric = PHOTOMETRIC_RGB;
1435-
}
14361476
}
14371477

14381478
span<const uint8_t> jpegTables = {};
@@ -1455,6 +1495,7 @@ Task<ImageData> readTiffImage(
14551495
PHOTOMETRIC_RGB,
14561496
PHOTOMETRIC_PALETTE,
14571497
PHOTOMETRIC_MASK,
1498+
PHOTOMETRIC_YCBCR,
14581499
PHOTOMETRIC_LOGLUV,
14591500
PHOTOMETRIC_LOGL,
14601501
PHOTOMETRIC_CFA, // Color Filter Array; displayed as grayscale for now
@@ -1469,7 +1510,26 @@ Task<ImageData> readTiffImage(
14691510
}
14701511

14711512
if (photometric == PHOTOMETRIC_YCBCR) {
1472-
throw ImageLoadError{"YCbCr images are unsupported."};
1513+
if (compression == COMPRESSION_JPEG || compression == COMPRESSION_LOSSY_JPEG || compression == COMPRESSION_JP2000) {
1514+
// Our JPEG decoder upsamples YCbCr data for us
1515+
TIFFUnsetField(tif, TIFFTAG_YCBCRSUBSAMPLING);
1516+
1517+
if (compression == COMPRESSION_JP2000) {
1518+
// Our JPEG2000 encoder furthermore outputs RGB directly
1519+
photometric = PHOTOMETRIC_RGB;
1520+
TIFFUnsetField(tif, TIFFTAG_REFERENCEBLACKWHITE);
1521+
}
1522+
}
1523+
1524+
if (uint16_t subsampling[2]; TIFFGetField(tif, TIFFTAG_YCBCRSUBSAMPLING, &subsampling[0], &subsampling[1])) {
1525+
tlog::debug() << fmt::format("Found YCbCr subsampling: {}x{}", subsampling[0], subsampling[1]);
1526+
1527+
const bool hasSubsampling = subsampling[0] != 1 || subsampling[1] != 1;
1528+
if (hasSubsampling) {
1529+
// TODO: actually handle subsampling
1530+
throw ImageLoadError{"Subsampled YCbCr images are only supported for JPEG-compressed TIFFs."};
1531+
}
1532+
}
14731533
}
14741534

14751535
// TODO: handle CIELAB, ICCLAB, ITULAB (shouldn't be too tough)
@@ -2260,7 +2320,7 @@ Task<ImageData> readTiffImage(
22602320
} else if (photometric == PHOTOMETRIC_LOGLUV || photometric == PHOTOMETRIC_LOGL) {
22612321
// If we're a LogLUV image, we've already configured the encoder to give us linear XYZ data, so we can just convert that to Rec.709.
22622322
resultData.toRec709 = xyzToChromaMatrix(rec709Chroma());
2263-
} else if (photometric <= PHOTOMETRIC_PALETTE) {
2323+
} else if (photometric <= PHOTOMETRIC_PALETTE || photometric == PHOTOMETRIC_YCBCR) {
22642324
co_await postprocessRgb(tif, photometric, dataBitsPerSample, numColorChannels, numRgbaChannels, floatRgbaData, resultData, priority);
22652325
} else {
22662326
// Other photometric interpretations do not need a transfer

0 commit comments

Comments
 (0)