Summary

A framing device and brief overview of technical details about a digital photo gallery that compresses its own images.

Contents

Lossy Photo Gallery

Welcome!

Hi there! I got a good deal on a webpage to show off some of my pictures. Or, well, I thought it was a good deal when I signed the lease. Turns out the landlord wasn't entirely forthcoming about some of the issues this place has. See, as we all know, the information superhighway is a series of tubes. All of our data flows through those tubes, but sometimes they develop memory leaks. And guess what? There's a huge tube above this webpage, and of course it's leaky, and of course the landlord didn't tell me.

I've tried my best to keep up with garbage collection so this place doesn't get too bitrotted, but despite my best efforts, I just can't seem to keep gunk from building up on the pictures. If you don't mind, can you please wipe them off when they get dirty and start to look JPEG'd? I've left some Windex and rags by each of them. Thanks.

Click here to visit the gallery!

Background and Explanation

Lossy Compression Losses

Many years ago, I wrote a Java tool that took two bitmaps— as in specifically .bmp files—and computed the difference in their pixel values. It would take these differences and use them to construct a new bitmap that visualizes the difference between two images. The goal was to better understand what JPEG compression really looks like by using an original image and a compressed version as inputs.

Fast forward to now and my goodness modern web browsers with HTML5 and Javascript are infinitely better at handling images than those Java image libraries that I couldn't even get to work so I had to resort to byte-level hacking of .bmp files.

Gamma

Calculating the difference in brightness between two pixels requires taking gamma correction into consideration. Gamma correction is a clever trick that lets computers and other electronics map pixel brightness data in such a way that doubling the brightness value of a pixel stored in an image file results in a pixel that's perceived as twice as bright when displayed on a screen. Because human perception of most sensory inputs including brightness of light sources is nonlinear and follows a power curve, computer monitors have their own power curve that mimics this. The actual amount of light emitted by the brighter pixel on the screen is about four times greater.

The exponent—the gamma value— of monitor's power curve varies depending the application, but 2.2 is by far the most common exponent and also used by: NTSC television, the sRGB color space, and most digital cameras. CRT screens don't need much gamma correction on the output side because, coincidentally, the relationship between the voltage applied to an electron gun and the resulting brightness of the targeted phosphor dot follows a power curve with an exponent of about 2.2. This relationship is separate from human perception or gamma correction, to be clear, but it was convenient for engineers during the 20th century.

I'm struggling to explain using words in part because I'm not sure how much background knowledge the average reader has. Let's try explaining it using graphics instead. Consider a series of four grey boxes with additional black and white boxes:

RGB value Color Perceived
brightness
Actual
brightness
0, 0, 0 0% 0%
51, 51, 51 20% 2.9%
102, 102, 102 40% 13.3%
153, 153, 153 60% 32.5%
204, 204, 204 80% 61.2%
255, 255, 255 100% 100%

The third box should appear twice as bright as the second, and the fifth box should appear twice as bright as the third. Each box going down the table should appear to be the same amount brighter than the one before. But the actual, physical brightness—the amount of light emitted by your screen— increases faster as the greys get lighter.

Consider another series of grey boxes, except these are scaled in by the actual light emitted by your screen assuming it's calibrated decently.

RGB value Color Perceived
brightness
Actual
brightness
0, 0, 0 0% 0%
123, 123, 123 48% 20%
168, 168, 168 66% 40%
202, 202, 202 79% 60%
230, 230, 230 90% 80%
255, 255, 255 100% 100%

A pixel at 20% brightness on a monitor is already about halfway to the maximum RGB value. This is part of why dark images frequently look bad when compressed: the darker half of RGB values only have a quarter the brightness resolution to work with compared to the lighter half. The aggressive video compression used by streaming services, particularly Netflix, exemplifies this, frequently crushing details out of dark scenes.

And if it still doesn't make sense, imagine taking a flashlight with you on a walk at night. If you shine the flashlight on the sidewalk in front of you, it will look much brighter than before and you will have an easier time seeing any obstacles or trip hazards. However, if you take that same walk at noon on a sunny day, the flashlight—despite emitting exactly as much light as it does at night—will make no perceptible difference whatsoever because the sun overpowers it.

Code

Tl;dr HTML canvases are extremely cool and each picture in the gallery is secretly a stack of four of them in a <div>. The code works as follows:

  1. An image is loaded into hidden <img> tags.
  2. The image data of each <img> is copied into a hidden <canvas>.
  3. The contents of the hidden <canvas> are copied into two other <canvas> tags that are actually displayed.
  4. The contents of one <canvas> are copied and the browser's built-in JPEG algorithm is applied.
  5. The JPEG'd image data is output to the other <canvas>.
  6. The z-height property of the two <canvas> tags are modified so the output canvas is on top.
  7. Repeat the previous three steps until the image is clicked.
  8. The contents of the <canvas> containing the original image and the <canvas> most recently used to store an output are copied.
  9. The value of each color channel in each pixel in both <canvas> tags' image data is raised to the gamma power.
  10. The absolute value of the difference of each respective color channel in each respective pixel in the two images is stored.
  11. These values are raised to the power of 1/gamma, i.e., the root of the gamma value.
  12. These roots are assembled into data used to construct a differential image.
  13. The differential image is output to a fourth <canvas> and its z-index is changed so it's on top.
  14. When the differential image is clicked, return to step 3.

You may be wondering if using a non-integer exponent makes the code slower than simply approximating gamma correction using gamma = 2. And it is! It takes about 150 milliseconds to generate a 0.5 megapixel image on my desktop with gamma = 2.2 whereas it only takes 30 ms with gamma = 2. My Macbook Pro featuring a 17-year-old Core 2 Duo is also proportionally faster, decreasing from 350 ms to 70 ms to generate the same image.

That said, the code still executes fast enough for my purposes here using gamma = 2.2, and I'm pleased that it works as well as it does on such an old system. Clearly it's limited by single-threaded performance given that that's the only aspect of my desktop that's less than an order of magnitude faster than the Macbook.

Parameters

I set up a few parameters that the program uses as constants so I could tune it while it was a work in progress, and then I changed them to variables so they can be changed at runtime. Hit F12 or cmd+opt+K on Mac or whatever the heck the shortcut is on Linux to bring up the console. I got sick of debugging event listeners so there's no GUI.