javaimagergbjavax.imageioread-write

Why is the color of my image changed after writing it as a jpg file?


I'm currently making a method that converts a ppm file to a jpg, png, and bmp file. The way I did it is reading the content of a ppm file, creating a BufferedImage, and assigning each pixel from the ppm file to the corresponding pixel in the BufferedImage. My bmp and png files look correct. However, the jpg file looks completely different.

Below is my code:

import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

import javax.imageio.ImageIO;

public class readPPMOutputOthers {

  public static void main(String[] args) throws InterruptedException {
    // read a ppm file

    Scanner sc;
    // if the file is not found, it will throw an exception
    try {
      sc = new Scanner(new FileInputStream("res/test2.ppm"));
    } catch (FileNotFoundException e) {
      throw new IllegalArgumentException("File not found!");
    }

    // the file now is a StringBuilder
    // read line by line to get information
    StringBuilder builder = new StringBuilder();
    while (sc.hasNextLine()) {
      String s = sc.nextLine();
      // ignore comment #
      if (s.charAt(0) != '#') {
        builder.append(s).append(System.lineSeparator());
      }
    }

   
    sc = new Scanner(builder.toString());
    String token;
    token = sc.next();

    // set the fields
    // initial load image
    int width = sc.nextInt();
    int height = sc.nextInt();
    int maxValue = sc.nextInt();

    List<Integer> pixels = new ArrayList<>();
    for (int i = 0; i < height; i++) {
      for (int j = 0; j < width; j++) {
        int r = sc.nextInt();
        int g = sc.nextInt();
        int b = sc.nextInt();

        int rgb = r;
        rgb = (rgb << 8) + g;
        rgb = (rgb << 8) + b;
        pixels.add(rgb);
      }
    }

    // make a BufferedImage from pixels
    BufferedImage outputImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    int[] outputImagePixelData = ((DataBufferInt) outputImg.getRaster().getDataBuffer()).getData();

    for (int i = 0; i < pixels.size(); i++) {
      outputImagePixelData[i] = pixels.get(i);
    }

    try {
        ImageIO.write(outputImg, "png",
            new File("res/test.png"));
      ImageIO.write(outputImg, "jpg",
          new File("res/test2.jpg"));
        ImageIO.write(outputImg, "bmp",
            new File("res/test.bmp"));
    } catch (IOException e) {
      System.out.println("Exception occurred :" + e.getMessage());
    }
    System.out.println("Images were written successfully.");
  }
}

images comparison

The weird thing is it works for a very large image but not for this small image. I need to make it work for such small images because of testing. I've been digging posts about this on google and still didn't find a way to solve this. Any help would be appreciated!


Solution

  • The reason for the strange colors is YUV420 chroma subsumpling used by JPEG encoding.

    In YUV420 every 2x2 pixels have the same chroma information (the 2x2 pixels have the same color).
    The 2x2 pixels have the same color, but each pixel has different luminance (brighness).


    The YUV420 Chroma subsumpling is demonstrated in Wikipedia:
    enter image description here

    And in our case:
    enter image description here becomes enter image description here
    The brown color is a mixture of the original red, cyan magenta and the yellow colors (the brown color is "shared" by the 4 pixels).



    The commonly used JPEG color format is YUV420, but JPEG standard does support YUV444 Chroma subsumpling.
    GIMP manages to save JPEG images with YUV444 Chroma subsumpling.

    Example (2x2 image):
    Too small: enter image description here Enlarged: enter image description here

    I couldn't find an example for saving YUV444 JPEG in JAVA...