Mipmap (gamma corrected)

High constrast regions become ‘darker’ when scaling down. Use this (slow) code to fixy fixy.


vs.


vs.

Have fun.


   public static BufferedImage mipmapGammaCorrected(BufferedImage src, int level)
   {
      if (level < 1)
      {
         throw new IllegalArgumentException();
      }

      for (int i = 0; i < level; i++)
      {
         BufferedImage tmp = mipmapGammaCorrected(src);
         if (i != 0)
            src.flush(); // do not flush argument
         src = tmp;
      }
      return src;
   }

   public static BufferedImage mipmapGammaCorrected(BufferedImage src)
   {
      int wSrc = src.getWidth();
      int hSrc = src.getHeight();

      if (wSrc % 2 != 0 || hSrc % 2 != 0)
      {
         throw new IllegalStateException("dimensions must be multiple of 2");
      }

      int wDst = wSrc / 2;
      int hDst = hSrc / 2;

      int[] argbFull = src.getRGB(0, 0, wSrc, hSrc, null, 0, wSrc);

      int type = BufferedImage.TYPE_INT_RGB;
      if (src.getAlphaRaster() != null)
      {
         type = BufferedImage.TYPE_INT_ARGB;

         // merge alpha into RGB values
         int[] alphaFull = src.getAlphaRaster().getPixels(0, 0, wSrc, hSrc, (int[]) null);
         for (int i = 0; i < alphaFull.length; i++)
         {
            argbFull[i] = (alphaFull[i] << 24) | (argbFull[i] & 0x00FFFFFF);
         }
      }

      BufferedImage half = new BufferedImage(wDst, hDst, type);

      int[] argbHalf = new int[argbFull.length >>> 2];

      for (int y = 0; y < hDst; y++)
      {
         for (int x = 0; x < wDst; x++)
         {
            int p0 = argbFull[((y << 1) | 0) * wSrc + ((x << 1) | 0)];
            int p1 = argbFull[((y << 1) | 1) * wSrc + ((x << 1) | 0)];
            int p2 = argbFull[((y << 1) | 1) * wSrc + ((x << 1) | 1)];
            int p3 = argbFull[((y << 1) | 0) * wSrc + ((x << 1) | 1)];

            int a = gammaCorrectedAverage(p0, p1, p2, p3, 24);
            int r = gammaCorrectedAverage(p0, p1, p2, p3, 16);
            int g = gammaCorrectedAverage(p0, p1, p2, p3, 8);
            int b = gammaCorrectedAverage(p0, p1, p2, p3, 0);

            argbHalf[y * wDst + x] = (a << 24) | (r << 16) | (g << 8) | (b << 0);
         }
      }

      half.setRGB(0, 0, wDst, hDst, argbHalf, 0, wDst);
      if (type == BufferedImage.TYPE_INT_ARGB)
      {
         // extract alpha from ARGB values
         int[] alpha = new int[argbHalf.length];
         for (int i = 0; i < alpha.length; i++)
            alpha[i] = (argbHalf[i] >> 24) & 0xFF;
         half.getAlphaRaster().setPixels(0, 0, wDst, hDst, alpha);
      }

      return half;
   }

   static int gammaCorrectedAverage(int a, int b, int c, int d, int shift)
   {
      float x = ((a >> shift) & 0xFF) / 255.0f;
      float y = ((b >> shift) & 0xFF) / 255.0f;
      float z = ((c >> shift) & 0xFF) / 255.0f;
      float w = ((d >> shift) & 0xFF) / 255.0f;

      float e = x * x + y * y + z * z + w * w;
      e = (float) Math.sqrt(e * 0.25f);
      return (int) (e * 255.0f);
   }

Could you possibly attach two larger pics so we can see what the differences actually look like?

Uh… do you see the text is much brighter against the blue background?

You don’t see that effect too clearly in the larger images, so there wouldn’t be any point in showing them.

It’s all about the quality in the smaller/smallest mipmap levels.

I implemented a shader version of this a few years ago, it improves texture quality by a huge margin, especially in a gamma-correct rendering environment (most HDR renderers). You don’t even have to use mipmaps to notice it, just compare a shaded surface with a texture in your engine side-by-side with the same texture in your favorite image editor, it won’t be what the artist created under non-gamma-correct rendering. It’s the same thing with mipmaps, any kind of action on the non-linear values of the original image will distort the colors.

My implementation did an initial conversion to linear space on a floating-point texture, then did the downscale in fp precision with a bicubic filter, then converted back to gamma-space. It was nearly instant too, even with large textures, GPU acceleration owns. :wink:
Edit: I also used x^2.2 instead of x^2, since 2.2 is the value used by most monitors and image editing software. x^2 is a very decent approximation though, I use it in the fast path of my renderer, since you can then use inversesqrt instead of pow for the gamma correction.

Anyway, I’d post the code but it’s kinda buried deep in my tool code, so if anyone wants code-snippets or help with implementing something similar, feel free to contact me. But Riven’s implementation is great for most use-cases, as always. :wink:

Anything for better quality thumbnails. As long as it is fast. Is the effect noticeable on larger thumbs?

Answer:

Ah, also, the CPU version (my version) is extremely slow.

Ah, well. Ok.