Here’s a cool trick: try downloading this picture and looking at it in your file browser:
If your computer is like mine, the output picture should look pretty different! Here’s what I see:
So what happened?
Gamma and Images
Most programs that handle pictures don’t support a little known feature called Gamma Correction. This feature has been part of analog and digital imaging since the early days of Television, and was meant to work around limitations for darker colors. For digital imaging, when a pixel is displayed to the screen, the brightness is scaled by an exponent. This allows pictures to have a higher dynamic range for colors, at the cost of losing some accuracy around the brighter colors.
But what exponent to choose? The most common exponent, called gamma, is 2.2, which is commonly associated with sRGB. In fact, with the success of the Internet, almost all programs today assume this is correct value. This has resulted in surprising behavior, because most programs don’t handle the case where gamma is different. The number of programs that get this wrong is surprisingly high, including some big names like Adobe Photoshop and Facebook. Rather than shame them, let’s exploit this behavior to hide one picture inside of another. Browsers actually do handle gamma correctly, so we can use this difference in behavior to make a picture that looks different based on what program you view it with.
Here’s the source code to get started:
git clone https://github.com/carl-mastrangelo/gammux go run gammux/gammux.go --help
Running this will take two images, a thumbnail and a full image, and produce a final image with both present. Compliant programs will show the full image, while non-compliant programs show the thumbnail.
To show how this program works, I have dumped the internal state of the two pictures as they process through. Additionally, I have included a Histogram of the pixel brightness, so you can see what’s going on. The histogram shows black pixels as a column on the left, get progressively brighter pixels as you go right, and end at the right with full brightness. Because there is typically one column much taller than all the rest, I set this column red, and instead scale the image up to show the clarity.
We will be “hiding” the full image in the high brightness pixels, and setting a large gamma to pull them back down. We will then merge this with the thumbnail at “normal” gamma of 2.2, which should be pulled down to black. Compliant programs will show the full picture with a grid of black lines, and non-compliant programs will show the thumbnail picture with a grid of white dots.
Here is the the full image (which you hopefully saw above):
Here is the “Thumbnail” picture and histogram:
As we can see, the two images use up almost the whole range of brightness.
Darken the Thumbnail
In order to make room for the full image, we need to darken the thumbnail. You can see the histogram of the thumbnail get squished towards the left:
Resample the Full Image
We need to make room for the thumbnail by leaving 3 pixels blank for every 1 pixel of the full image. We will do this in a grid of 2x2 squares. Here’s what it looks like:
The histogram looks more vertically squashed, but the shape is roughly the same. Up close, this looks like:
Undo Old Gamma, Apply New Gamma:
Since most programs assume a gamma of 2.2, we will too. Let’s first convert the full image to a linear space:
It looks a lot darker, and the histogram has shifted over to the left more. This is what I mean by giving more resolution at lower brightnesses. Normally linear color space “spends” too much of it’s pixels at brighter colors when it humans are more sensitive to darker ones. Let’s bring the gamma back up from the linear space to 50.0:
Woah that looks bad! But we can see what’s going on though. All the pixels have moved the right of the histogram. This is why the final image looks like it has a grid of white dots; it’s the very bright, full image overlayed on top.
Merge the Thumbnail and the Full Image:
Let’s put the two pictures together. We have two images that should fit perfectly into each other. The pixels look something like:
Thumbnail: Full: Merged: X X O O O X O X X X X X X X X X X X O O O X O X X X X X X X X X
We get the final image by doing this merge:
We can see the histogram is the combination of the gamma adjusted full and the thumbnail. Here they again, side by side:
One thing is wrong though. The final image still looks like the non-compliant version. This is because we have’t set the gamma on the output file. We set the Gamma, a small part of the PNG file, and we get the final image:
We can make tricky images inside of each other by taking advantage of programs treating pictures differently. The effect is particularly noticeable if you attempt to make a thumbnail image of user provided input, because rescaling is done in a linear space, while the pixels are in an exponential space!
You can create your own Gamma multiplexed (muxed) images using the code here:
Available in both Python and Go.