Skip to content

Commit 0a6d5a7

Browse files
committed
fix(gainmap): only apply to top-level RGB or luminance layer
1 parent cb21a32 commit 0a6d5a7

File tree

1 file changed

+66
-62
lines changed

1 file changed

+66
-62
lines changed

src/imageio/GainMap.cpp

Lines changed: 66 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,34 @@ string GainmapHeadroom::toString() const {
4646
}
4747
}
4848

49+
static vector<Channel*> getRgbOrLuminanceChannels(ImageData& image) {
50+
image.updateLayers();
51+
52+
Channel* r = nullptr;
53+
Channel* g = nullptr;
54+
Channel* b = nullptr;
55+
56+
for (auto& layer : image.layers) {
57+
if (((r = image.mutableChannel(layer + "R")) && (g = image.mutableChannel(layer + "G")) && (b = image.mutableChannel(layer + "B"))) ||
58+
((r = image.mutableChannel(layer + "r")) && (g = image.mutableChannel(layer + "g")) && (b = image.mutableChannel(layer + "b")))) {
59+
return {r, g, b};
60+
} else if ((r = image.mutableChannel(layer + "L")) || (r = image.mutableChannel(layer + "l")) ||
61+
(r = image.mutableChannel(layer + "Y")) || (r = image.mutableChannel(layer + "y"))) {
62+
return {r};
63+
}
64+
}
65+
66+
return {};
67+
}
68+
4969
Task<void> preprocessAndApplyAppleGainMap(
5070
ImageData& image, ImageData& gainMap, const optional<Ifd>& amn, const GainmapHeadroom& targetHeadroom, int priority
5171
) {
52-
if (image.channels.empty() || gainMap.channels.empty()) {
53-
tlog::warning() << "ISO gain map: image or gain map has no channels. Skipping gain map application.";
72+
const auto imageChannels = getRgbOrLuminanceChannels(image);
73+
auto gainMapChannels = getRgbOrLuminanceChannels(gainMap);
74+
75+
if (imageChannels.empty() || gainMapChannels.empty()) {
76+
tlog::warning() << "Apple gain map: image or gain map has no channels. Skipping gain map application.";
5477
co_return;
5578
}
5679

@@ -59,28 +82,33 @@ Task<void> preprocessAndApplyAppleGainMap(
5982
tlog::debug() << "Apple gain map: linearizing and resizing";
6083

6184
// First: linearize per the spec, then resize to image size
62-
const auto gainmapSize = gainMap.channels[0].size();
85+
const auto gainmapSize = gainMapChannels.front()->size();
6386
const size_t gainmapNumPixels = (size_t)gainmapSize.x() * gainmapSize.y();
6487
co_await ThreadPool::global().parallelForAsync<size_t>(
6588
0,
6689
gainmapNumPixels,
67-
gainmapNumPixels * gainMap.channels.size(),
90+
gainmapNumPixels * gainMapChannels.size(),
6891
[&](int i) {
69-
for (int c = 0; c < (int)gainMap.channels.size(); ++c) {
92+
for (int c = 0; c < (int)gainMapChannels.size(); ++c) {
7093
// NOTE: The docs (above link) say to use the Rec.709 transfer function here, but comparisons with ISO gain maps indicate
7194
// that the gain maps are actually encoded with the sRGB transfer function.
72-
// const float gain = ituth273::invTransferComponent(ituth273::ETransfer::BT709, gainMap.channels[gainmapChannel].at(i));
73-
gainMap.channels[c].setAt(i, toLinear(gainMap.channels[c].at(i)));
95+
// const float gain = ituth273::invTransferComponent(ituth273::ETransfer::BT709, gainMapChannels[gainmapChannel].at(i));
96+
gainMapChannels[c]->setAt(i, toLinear(gainMapChannels[c]->at(i)));
7497
}
7598
},
7699
priority
77100
);
78101

79-
const auto size = image.channels[0].size();
102+
const auto size = imageChannels.front()->size();
80103

81104
co_await ImageLoader::resizeImageData(gainMap, size, priority);
82105

83-
TEV_ASSERT(size == gainMap.channels[0].size(), "Image and gain map must have the same size.");
106+
// Re-fetch channels after resize
107+
gainMapChannels = getRgbOrLuminanceChannels(gainMap);
108+
TEV_ASSERT(!gainMapChannels.empty(), "Gain map must have at least one channel after resize.");
109+
TEV_ASSERT(
110+
size == gainMapChannels.front()->size(), "Image and gain map must have the same size. ({}!={})", size, gainMapChannels.front()->size()
111+
);
84112

85113
// 0.0 and 8.0 result in the weakest effect. They are a sane default; see https://developer.apple.com/forums/thread/709331
86114
float maker33 = 0.0f;
@@ -121,45 +149,23 @@ Task<void> preprocessAndApplyAppleGainMap(
121149
"Apple gain map: derived weight {} from headroom {} and maker note #33={} #48={}", headroom, targetHeadroom.toString(), maker33, maker48
122150
);
123151

124-
const int numImageChannels = (int)image.channels.size();
125-
const int numGainMapChannels = (int)gainMap.channels.size();
126-
127-
if (numGainMapChannels > 1) {
152+
if (gainMapChannels.size() > 1) {
128153
tlog::warning() << "Apple gain map: should only have one channel. Attempting to apply multi-channel gain map.";
129154
}
130155

131-
int alphaChannelIndex = -1;
132-
for (int c = 0; c < numImageChannels; ++c) {
133-
bool isAlpha = Channel::isAlpha(image.channels[c].name());
134-
if (isAlpha) {
135-
if (alphaChannelIndex != -1) {
136-
tlog::warning() << fmt::format(
137-
"Apple gain map: image has multiple alpha channels, using the first one: {}", image.channels[alphaChannelIndex].name()
138-
);
139-
continue;
140-
}
141-
142-
alphaChannelIndex = c;
143-
}
144-
}
145-
146156
const size_t numPixels = (size_t)size.x() * size.y();
147157
co_await ThreadPool::global().parallelForAsync<size_t>(
148158
0,
149159
numPixels,
150-
numPixels * numImageChannels,
160+
numPixels * imageChannels.size(),
151161
[&](size_t i) {
152-
for (int c = 0; c < numImageChannels; ++c) {
153-
if (c == alphaChannelIndex) {
154-
continue;
155-
}
162+
for (size_t c = 0; c < imageChannels.size(); ++c) {
163+
const size_t gainmapChannel = std::min(c, gainMapChannels.size() - 1);
156164

157-
const int gainmapChannel = std::min(c, numGainMapChannels - 1);
165+
const float sdr = imageChannels[c]->at(i);
166+
const float gain = gainMapChannels[gainmapChannel]->at(i);
158167

159-
const float sdr = image.channels[c].at(i);
160-
const float gain = gainMap.channels[gainmapChannel].at(i);
161-
162-
image.channels[c].setAt(i, sdr * (1.0f + (headroom - 1.0f) * gain));
168+
imageChannels[c]->setAt(i, sdr * (1.0f + (headroom - 1.0f) * gain));
163169
}
164170
},
165171
priority
@@ -177,7 +183,10 @@ Task<void> preprocessAndApplyIsoGainMap(
177183
const GainmapHeadroom& targetHeadroom,
178184
int priority
179185
) {
180-
if (image.channels.empty() || gainMap.channels.empty()) {
186+
const auto imageChannels = getRgbOrLuminanceChannels(image);
187+
auto gainMapChannels = getRgbOrLuminanceChannels(gainMap);
188+
189+
if (imageChannels.empty() || gainMapChannels.empty()) {
181190
tlog::warning() << "ISO gain map: image or gain map has no channels. Skipping gain map application.";
182191
co_return;
183192
}
@@ -196,40 +205,35 @@ Task<void> preprocessAndApplyIsoGainMap(
196205
);
197206

198207
// Per the spec, unnormalize and then resize (in log space) to image size
199-
const auto gainmapSize = gainMap.channels[0].size();
208+
const auto gainmapSize = gainMapChannels.front()->size();
200209
const size_t gainmapNumPixels = (size_t)gainmapSize.x() * gainmapSize.y();
201210
co_await ThreadPool::global().parallelForAsync<size_t>(
202211
0,
203212
gainmapNumPixels,
204-
gainmapNumPixels * gainMap.channels.size(),
213+
gainmapNumPixels * gainMapChannels.size(),
205214
[&](int i) {
206-
for (int c = 0; c < (int)gainMap.channels.size(); ++c) {
207-
const float val = gainMap.channels[c].at(i);
215+
for (int c = 0; c < (int)gainMapChannels.size(); ++c) {
216+
const float val = gainMapChannels[c]->at(i);
208217

209218
const float logRecovery = copysign(std::pow(abs(val), 1.0f / metadata.gainMapGamma()[c]), val);
210219
const float logBoost = metadata.gainMapMin()[c] * (1.0f - logRecovery) + metadata.gainMapMax()[c] * logRecovery;
211220

212-
gainMap.channels[c].setAt(i, logBoost);
221+
gainMapChannels[c]->setAt(i, logBoost);
213222
}
214223
},
215224
priority
216225
);
217226

218-
const auto size = image.channels[0].size();
227+
const auto size = imageChannels.front()->size();
219228

220229
co_await ImageLoader::resizeImageData(gainMap, size, priority);
221230

222-
TEV_ASSERT(size == gainMap.channels[0].size(), "Image and gain map must have the same size.");
223-
224-
const int numImageChannels = (int)image.channels.size();
225-
const int numGainMapChannels = (int)gainMap.channels.size();
226-
227-
int alphaChannelIndex = -1;
228-
if (Channel::isAlpha(image.channels.back().name())) {
229-
alphaChannelIndex = image.channels.size() - 1;
230-
}
231-
232-
const int numColorChannels = std::min(alphaChannelIndex == -1 ? numImageChannels : (numImageChannels - 1), 3);
231+
// Re-fetch channels after resize
232+
gainMapChannels = getRgbOrLuminanceChannels(gainMap);
233+
TEV_ASSERT(!gainMapChannels.empty(), "Gain map must have at least one channel after resize.");
234+
TEV_ASSERT(
235+
size == gainMapChannels.front()->size(), "Image and gain map must have the same size. ({}!={})", size, gainMapChannels.front()->size()
236+
);
233237

234238
// Before applying the gainmap, convert the image to the appropriate color space. Fall back to base chroma if alt chroma requested but
235239
// not given (image should have been left in base chroma in that case).
@@ -265,17 +269,17 @@ Task<void> preprocessAndApplyIsoGainMap(
265269
co_await ThreadPool::global().parallelForAsync<size_t>(
266270
0,
267271
numPixels,
268-
numPixels * numColorChannels,
272+
numPixels * imageChannels.size(),
269273
[&](size_t i) {
270-
for (int c = 0; c < numColorChannels; ++c) {
271-
const int gainmapChannel = std::min(c, numGainMapChannels - 1);
274+
for (size_t c = 0; c < imageChannels.size(); ++c) {
275+
const int gainmapChannel = std::min(c, gainMapChannels.size() - 1);
272276

273-
const float logBoost = gainMap.channels[gainmapChannel].at(i);
277+
const float logBoost = gainMapChannels[gainmapChannel]->at(i);
274278

275-
const float sdr = image.channels[c].at(i);
279+
const float sdr = imageChannels[c]->at(i);
276280
const float hdr = (sdr + metadata.baseOffset()[c]) * exp2f(logBoost * weight) - metadata.alternateOffset()[c];
277281

278-
image.channels[c].setAt(i, hdr);
282+
imageChannels[c]->setAt(i, hdr);
279283
}
280284
},
281285
priority

0 commit comments

Comments
 (0)