2009-08-25

Image :: mipmap with gamma

Most people don't realize that when you scale down an image, the gamma changes. For those who care, here is the code to fixy fixy:

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

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

   public static BufferedImage mipmapGammaCorrected(BufferedImage src, double gamma)
   {
      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, gamma);
            int r = gammaCorrectedAverage(p0, p1, p2, p3, 16, gamma);
            int g = gammaCorrectedAverage(p0, p1, p2, p3,  8, gamma);
            int b = gammaCorrectedAverage(p0, p1, p2, p3,  0, gamma);

            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, double gamma)
   {
      double x = Math.pow(((a >> shift) & 0xFF) / 255.0f, gamma);
      double y = Math.pow(((b >> shift) & 0xFF) / 255.0f, gamma);
      double z = Math.pow(((c >> shift) & 0xFF) / 255.0f, gamma);
      double w = Math.pow(((d >> shift) & 0xFF) / 255.0f, gamma);

      return (int) Math.round(Math.pow((x+y+z+w) * 0.25f, 1.0 / gamma) * 255.0);
   }
}

No comments:

Post a Comment