Pixel art downscaling: why most tools make it too dark

By Richard Bailey

Shrink a picture to pixel art, or to a mosaic, and it comes out darker than the picture. Not everywhere: the flat areas are fine. The edges darken, wherever light meets dark, and on a busy photo that’s most of it. The cause is one shortcut, and I measured how much it costs while building this site’s pixel art converter and mosaic maker: about 5% of the light from a typical photograph, and up to three quarters of it from a single square.

A pixel value is not an amount of light

The numbers in an image file aren’t proportional to light. A pixel stored as 128, halfway between black and white on the file’s scale, gives off about 21% of the light a white pixel does. The scale is bent on purpose, because eyes are more sensitive to steps in the dark than in the light, and bending it spends the file’s 256 levels where they can be seen. It’s a bit like a volume knob: halfway round isn’t half as loud.

Averaging is where the bend bites. The average colour of a block of pixels is the average of their light, not of their file numbers. Take the simplest block there is, one black pixel and one white one. Averaging the numbers gives 128. Averaging the light gives half of white’s light, which the file stores as 188. Those are very different greys.

The checkerboard test

So the test is a checkerboard of single black and white pixels, shrunk by eight. The right answer is 188. I gave it to five downscalers people actually use, at their default settings.

Downscaler Result (188 is right)
Chromium 155, canvas drawImage at low, medium and high quality 127, too dark
Chromium 155, createImageBitmap with a resize 127, too dark
Pillow 10.2, resize with BOX, BILINEAR or LANCZOS 128, too dark
ImageMagick 6, convert -resize 127, too dark
ImageMagick 6, with -colorspace RGB before and -colorspace sRGB after 187, right
ffmpeg 6, scale with the default filter or area 128, too dark
sharp 0.35 (libvips 8.18), resize with lanczos3 or mitchell 128, too dark
050100150200Grey produced188 is rightChromium|canvas: 127Chromiumcanvas127Pillow: 128Pillow128ImageMagick: 127ImageMagick127ImageMagick|with flags: 187ImageMagickwith flags187ffmpeg: 128ffmpeg128sharp|(libvips): 128sharp(libvips)128
What each downscaler made of the checkerboard at its defaults. The right answer is 188; every default gives 127 or 128, and only ImageMagick, asked with two extra flags, gets there.

Every one of them, at its defaults, averages the file’s numbers. Only ImageMagick offers the correct answer, and only if you ask for it in two extra flags. The browser’s canvas, which is what most online converters use, is in the first row.

Two grey panels: on the left, the downscaled checkerboard is a darker grey than the flat grey beside it; on the right, the two match

That’s what the mistake looks like. Each panel is the shrunken checkerboard on its left half and, on its right half, a flat grey of 188, which is the brightness the checkerboard really has. On the left, the quick way: 128 beside 188, plainly darker. On the right, averaged as light: 187 beside 188.

On photographs

A checkerboard is the worst case, so I measured photographs too: the 24 Kodak test images, the standard set in image compression research, shrunk to 64 squares across both ways. For each picture I added up the light its squares give off when averaged the quick way, and again when averaged the right way, and compared the two totals. The difference is the light the quick way loses.

0%5%10%15%20%25%32486496128Squares acrossLight lostAverage of the 24 photos: 6.50% at 32Average of the 24 photos: 5.40% at 48Average of the 24 photos: 4.70% at 64Average of the 24 photos: 3.20% at 128Average of the 24 photosMost affected photo: 20.60% at 32Most affected photo: 17.40% at 48Most affected photo: 14.70% at 64Most affected photo: 9.80% at 128Most affected photo
  • Average of the 24 photos
  • Most affected photo
Light lost by averaging the quick way, by size, for the average photo and for the most affected one. Fewer squares means more edges inside each square, so the loss grows as the picture shrinks.
Squares across Less light, average of 24 photos From the least to the most affected Worst single square
32 6.5% 2.3 to 20.6% 76%
48 5.4% 1.6 to 17.4% 71%
64 4.7% 1.5 to 14.7% 75%
128 3.2% 1.0 to 9.8% 79%

Whole pictures lose a few percent of their light, and busy ones lose more: the worst of the 24 at 64 across, a line of motocross riders, loses 14.7%. The single worst square in every photo loses well over half its light, and that’s the part you see: dark fringes along every edge, so the picture reads as muddy rather than as wrong.

The motocross riders at 64 squares across, averaged the quick way on the left and as light on the right; the left is darker along every edge

Left, the quick way; right, averaged as light. The wheels and the white jerseys are where to look.

What it does to a mosaic

A mosaic adds a second step: each square is matched to the nearest brick colour, so a square that comes out too dark gets a darker brick. With the mosaic maker’s default palette of 40 brick colours, averaging the quick way changed 11% of the bricks on an average photo at 64 across, and 29.5% on the motocross photo, each changed brick sitting a mean 7.4 sRGB steps from the one it should have been. At 32 across it’s 14% of bricks on average and 38% at worst.

0%10%20%30%40%32486496128Studs acrossBricks changedAverage of the 24 photos: 14.30% at 32Average of the 24 photos: 12.40% at 48Average of the 24 photos: 11% at 64Average of the 24 photos: 8.20% at 128Average of the 24 photosMost affected photo: 37.90% at 32Most affected photo: 33.30% at 48Most affected photo: 29.50% at 64Most affected photo: 21.60% at 128Most affected photo
  • Average of the 24 photos
  • Most affected photo
Bricks that come out different when the squares are averaged the quick way, with the mosaic maker's 40-colour palette, for the average photo and the most affected one.

The motocross riders as a brick mosaic 64 across, matched from the quick average on the left and from light on the right

Those are bricks you’d buy and place. A mosaic that comes out darker than its photo is usually this and nothing else.

The fix

Convert each value to light, average, convert back. Three lines, and it’s what the tools on this site do.

const toLinear = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
const toSrgb = (v) => (v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055);

// The average of a block of pixel values, each 0 to 255.
function average(values) {
  const light = values.reduce((sum, v) => sum + toLinear(v / 255), 0) / values.length;
  return Math.round(255 * toSrgb(light));
}

average([0, 255]); // 188, not 128

The two conversions are the standard curve every screen and every image file uses to turn stored numbers into light (its name is the sRGB transfer function), so there’s nothing to tune. The cost is a lookup per pixel, which is nothing.

When it matters

Whenever a downscale has to represent fine detail with few pixels: pixel art, mosaics, bead patterns, cross-stitch charts, thumbnails of text or line art. A photo shrunk by a quarter for a web page loses a fraction of a percent and nobody can tell. A photo shrunk to 48 bricks across loses an eighth of its light along every edge, and everyone can.

If you use ImageMagick, add the two colourspace flags. If you use anything else at its defaults, including a browser, assume the quick way and expect the dark edges. If you want it right without thinking about it, the pixel art converter, the mosaic maker and the image resizer here all average light.

LEGO® is a trademark of the LEGO Group, which does not sponsor, authorise or endorse this site.