Skip to content

Commit 101b8bb

Browse files
committed
fix(png): Improve png write with alpha is low (AcademySoftwareFoundation#3985)
We found that PNG output (which is required to be unassociated alpha) had precision problems with low alpha values because it quantized to the final bit depth before doing the alpha deassociation. Explanation: PNG requires associated alpha to be stored in the file. So if you have (unassociated) pixel value [0.00235, 0.00106, 0.00117, 0.0025], that means that we need to divide the colors by alpha before storing in the file, so that's [0.94, 0.424, 0.468, 0.0025] if you have full precision. But you don't, because PNG only supports uint8 and uint16. For uint8, this should end up as [240, 108, 119, 1]. But it turns out that our png writer does the integer conversion before the un-premultiplication, so actually we first quantize from [0.00235, 0.00106, 0.00117, 0.0025] to [1 0 0 1], and THEN we un-premultiply, giving us the [255, 0, 0, 1] that made you think "Why is that red value clipped? And where did green and blue go?" The solution, then, is to deassociate as floats first, then quantize to the final integer data type. Also add a test for this, as well as a test that verifies correctly writing of 16 bit files that I had some problems with in the last png PR. Signed-off-by: Larry Gritz <[email protected]>
1 parent e2ed0db commit 101b8bb

File tree

5 files changed

+96
-44
lines changed

5 files changed

+96
-44
lines changed

src/png.imageio/pngoutput.cpp

Lines changed: 77 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ class PNGOutput final : public ImageOutput {
8989
}
9090

9191
template<class T>
92-
void deassociateAlpha(T* data, int size, int channels, int alpha_channel,
93-
float gamma);
92+
void deassociateAlpha(T* data, size_t npixels, int channels,
93+
int alpha_channel, float gamma);
9494
};
9595

9696

@@ -269,30 +269,33 @@ PNGOutput::close()
269269

270270
template<class T>
271271
void
272-
PNGOutput::deassociateAlpha(T* data, int size, int channels, int alpha_channel,
273-
float gamma)
272+
PNGOutput::deassociateAlpha(T* data, size_t npixels, int channels,
273+
int alpha_channel, float gamma)
274274
{
275-
unsigned int max = std::numeric_limits<T>::max();
276275
if (gamma == 1) {
277-
for (int x = 0; x < size; ++x, data += channels)
278-
if (data[alpha_channel])
279-
for (int c = 0; c < channels; c++)
280-
if (c != alpha_channel) {
281-
unsigned int f = data[c];
282-
f = (f * max) / data[alpha_channel];
283-
data[c] = (T)std::min(max, f);
284-
}
276+
for (size_t x = 0; x < npixels; ++x, data += channels) {
277+
DataArrayProxy<T, float> val(data);
278+
float alpha = val[alpha_channel];
279+
if (alpha != 0.0f && alpha != 1.0f) {
280+
for (int c = 0; c < channels; c++) {
281+
if (c != alpha_channel)
282+
val[c] = data[c] / alpha;
283+
}
284+
}
285+
}
285286
} else {
286-
for (int x = 0; x < size; ++x, data += channels)
287-
if (data[alpha_channel]) {
287+
for (size_t x = 0; x < npixels; ++x, data += channels) {
288+
DataArrayProxy<T, float> val(data);
289+
float alpha = val[alpha_channel];
290+
if (alpha != 0.0f && alpha != 1.0f) {
288291
// See associateAlpha() for an explanation.
289-
float alpha_deassociate = pow((float)max / data[alpha_channel],
290-
gamma);
291-
for (int c = 0; c < channels; c++)
292+
float alpha_deassociate = pow(1.0f / val[alpha_channel], gamma);
293+
for (int c = 0; c < channels; c++) {
292294
if (c != alpha_channel)
293-
data[c] = static_cast<T>(std::min(
294-
max, (unsigned int)(data[c] * alpha_deassociate)));
295+
val[c] = val[c] * alpha_deassociate;
296+
}
295297
}
298+
}
296299
}
297300
}
298301

@@ -302,26 +305,42 @@ bool
302305
PNGOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
303306
stride_t xstride)
304307
{
305-
y -= m_spec.y;
306308
m_spec.auto_stride(xstride, format, spec().nchannels);
307309
const void* origdata = data;
310+
if (format == TypeUnknown)
311+
format = m_spec.format;
312+
313+
// PNG specifically dictates unassociated (un-"premultiplied") alpha.
314+
// If we need to unassociate alpha, do it in float.
315+
std::unique_ptr<float[]> unassoc_scratch;
316+
if (m_convert_alpha) {
317+
size_t nvals = size_t(m_spec.width) * size_t(m_spec.nchannels);
318+
float* floatvals = nullptr;
319+
if (nvals * sizeof(float) <= (1 << 16)) {
320+
floatvals = OIIO_ALLOCA(float, nvals); // small enough for stack
321+
} else {
322+
unassoc_scratch.reset(new float[nvals]);
323+
floatvals = unassoc_scratch.get();
324+
}
325+
// Contiguize and convert to float
326+
OIIO::convert_image(m_spec.nchannels, m_spec.width, 1, 1, data, format,
327+
xstride, AutoStride, AutoStride, floatvals,
328+
TypeFloat, AutoStride, AutoStride, AutoStride);
329+
// Deassociate alpha
330+
deassociateAlpha(floatvals, size_t(m_spec.width), m_spec.nchannels,
331+
m_spec.alpha_channel, m_gamma);
332+
data = floatvals;
333+
format = TypeFloat;
334+
xstride = size_t(m_spec.nchannels) * sizeof(float);
335+
}
336+
308337
data = to_native_scanline(format, data, xstride, m_scratch, m_dither, y, z);
309338
if (data == origdata && (m_convert_alpha || m_need_swap)) {
310339
m_scratch.assign((unsigned char*)data,
311340
(unsigned char*)data + m_spec.scanline_bytes());
312341
data = &m_scratch[0];
313342
}
314343

315-
// PNG specifically dictates unassociated (un-"premultiplied") alpha
316-
if (m_convert_alpha) {
317-
if (m_spec.format == TypeDesc::UINT16)
318-
deassociateAlpha((unsigned short*)data, m_spec.width,
319-
m_spec.nchannels, m_spec.alpha_channel, m_gamma);
320-
else
321-
deassociateAlpha((unsigned char*)data, m_spec.width,
322-
m_spec.nchannels, m_spec.alpha_channel, m_gamma);
323-
}
324-
325344
// PNG is always big endian
326345
if (m_need_swap)
327346
swap_endian((unsigned short*)data, m_spec.width * m_spec.nchannels);
@@ -354,27 +373,41 @@ PNGOutput::write_scanlines(int ybegin, int yend, int z, TypeDesc format,
354373
m_spec.auto_stride(xstride, ystride, zstride, format, m_spec.nchannels,
355374
m_spec.width, m_spec.height);
356375
const void* origdata = data;
376+
if (format == TypeUnknown)
377+
format = m_spec.format;
378+
379+
// PNG specifically dictates unassociated (un-"premultiplied") alpha.
380+
// If we need to unassociate alpha, do it in float.
381+
std::unique_ptr<float[]> unassoc_scratch;
382+
size_t npixels = size_t(m_spec.width) * size_t(yend - ybegin);
383+
size_t nvals = npixels * size_t(m_spec.nchannels);
384+
if (m_convert_alpha) {
385+
unassoc_scratch.reset(new float[nvals]);
386+
float* floatvals = unassoc_scratch.get();
387+
// Contiguize and convert to float
388+
OIIO::convert_image(m_spec.nchannels, m_spec.width, m_spec.height, 1,
389+
data, format, xstride, ystride, AutoStride,
390+
floatvals, TypeFloat, AutoStride, AutoStride,
391+
AutoStride);
392+
// Deassociate alpha
393+
deassociateAlpha(floatvals, npixels, m_spec.nchannels,
394+
m_spec.alpha_channel, m_gamma);
395+
data = floatvals;
396+
format = TypeFloat;
397+
xstride = size_t(m_spec.nchannels) * sizeof(float);
398+
ystride = xstride * size_t(m_spec.width);
399+
zstride = ystride * size_t(m_spec.height);
400+
}
401+
357402
data = to_native_rectangle(m_spec.x, m_spec.x + m_spec.width, ybegin, yend,
358403
z, z + 1, format, data, xstride, ystride,
359-
zstride, m_scratch);
360-
size_t npixels = m_spec.width * (yend - ybegin);
361-
size_t nvals = npixels * m_spec.nchannels;
404+
zstride, m_scratch, m_dither, 0, ybegin, z);
362405
if (data == origdata && (m_convert_alpha || m_need_swap)) {
363406
m_scratch.assign((unsigned char*)data,
364407
(unsigned char*)data + nvals * m_spec.format.size());
365408
data = m_scratch.data();
366409
}
367410

368-
// PNG specifically dictates unassociated (un-"premultiplied") alpha
369-
if (m_convert_alpha) {
370-
if (m_spec.format == TypeDesc::UINT16)
371-
deassociateAlpha((unsigned short*)data, npixels, m_spec.nchannels,
372-
m_spec.alpha_channel, m_gamma);
373-
else
374-
deassociateAlpha((unsigned char*)data, npixels, m_spec.nchannels,
375-
m_spec.alpha_channel, m_gamma);
376-
}
377-
378411
// PNG is always big endian
379412
if (m_need_swap)
380413
swap_endian((unsigned short*)data, nvals);

testsuite/png/ref/out-libpng15.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ exif.png : 64 x 64, 3 channel, uint8 png
2727
SHA-1: 7CB41FEA50720B48BE0C145E1473982B23E9AB77
2828
channel list: R, G, B
2929
oiio:ColorSpace: "sRGB"
30+
smallalpha.png : 1 x 1, 4 channel, uint8 png
31+
Pixel (0, 0): 240 108 119 1 (0.94117653 0.42352945 0.4666667 0.003921569)
32+
Comparing "test16.png" and "ref/test16.png"
33+
PASS

testsuite/png/ref/out.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@ exif.png : 64 x 64, 3 channel, uint8 png
3131
Exif:FocalLength: 45.7 (45.7 mm)
3232
Exif:WhiteBalance: 0 (auto)
3333
oiio:ColorSpace: "sRGB"
34+
smallalpha.png : 1 x 1, 4 channel, uint8 png
35+
Pixel (0, 0): 240 108 119 1 (0.94117653 0.42352945 0.4666667 0.003921569)
36+
Comparing "test16.png" and "ref/test16.png"
37+
PASS

testsuite/png/ref/test16.png

1.29 KB
Loading

testsuite/png/run.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,14 @@
1717
"--attrib Exif:FocalLength 45.7 " +
1818
"-o exif.png")
1919
command += info_command ("exif.png", safematch=True)
20+
21+
# regression test for 16 bit output bug
22+
command += oiiotool ("--pattern fill:topleft=1,0,0,1:topright=0,1,0,1:bottomleft=0,0,1,1:bottomright=1,1,1,1 16x16 4 -d uint16 -o test16.png")
23+
24+
# Test high quality alpha deassociation using alpha value close to zero.
25+
# This example is inspired by Yafes on the Slack.
26+
command += oiiotool ("--pattern fill:color=0.00235,0.00106,0.00117,0.0025 1x1 4 -d uint8 -o smallalpha.png")
27+
command += oiiotool ("--no-autopremult --dumpdata smallalpha.png")
28+
29+
outputs = [ "test16.png", "out.txt" ]
30+

0 commit comments

Comments
 (0)