Java使用带有配置文件的CMYK转RGB。输出过暗。

6
许多人已经提出了类似的问题。但是我仍然不明白为什么在使用ICC_Profile转换图片后,输出会变得过于暗淡。我尝试了许多配置文件:从Adobe网站和图片本身获取。 原始图片 Before Image 转换后的图片 After Image 代码
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("jpeg");
ImageReader reader = null;
while (readers.hasNext()){
      reader = readers.next();
      if (reader.canReadRaster()){
          break;
      }
}
// read
ImageInputStream ios = ImageIO.createImageInputStream(new FileInputStream(new File(myPic.jpg)));
reader.setInput(ios);
Raster r = reader.readRaster(0, null);

BufferedImage result = new BufferedImage(r.getWidth(), r.getHeight(), bufferedImage.TYPE_INT_RGB);
WritableRaster resultRaster = result.getRaster();
ICC_Profile iccProfile = ICC_Profile.getInstance(new File("profile_name.icc");
ColorSpace cs = new ICC_ColorSpace(iccProfile);
ColorConvertOp cmykToRgb = new ColorConvertOp(cs, result.getColorModel().getColorSpace(), null);
cmykToRgb.filter(r, resultRaster);

// write
ImageIo.write(resul, "jpg", new File("myPic.jpg"));

在我转换图片之后,还需要做些什么吗?


不好意思,但是你的问题不完整。你从哪里获取图像,将其放在哪里,profile_name.icc文件长什么样子... - Gangnus
嗯,这张图片是由一位设计师制作的。它使用的是CMYK颜色模式,该模式已经内置在图片中了。我尝试了两种方式:1. 从Adobe网站下载了icc_profiles列表并使用了上面的代码;2. 使用Sanselan提取了图片的配置文件并使用了上面的代码。这两种方式都产生了相同的结果,您可以在“之前”和“之后”中看到。希望这能清楚地解决问题。 - nixspirit
抱歉问题描述不清。我的意思不是你个人从哪里获取了图像 :-),而是你的代码从哪里获取它。它是r吗?它是如何读取的?它的定义在哪里?输出也是同样的情况。 - Gangnus
@nixspirit:虽然你的问题已经几个月了,但我发布了一个新答案,解释了大部分潜在问题,并包含了一个可行的解决方案。深色主要是由于旧版Photoshop的一个bug(CMYK值被反转)导致的,现在已成为事实上的标准,大多数JPEG软件都能处理(除了Java)。 - Codo
4个回答

24

这个问题并不是新的。但由于我花了很多时间解决了这个问题并得出了一个可行的解决方案,所以我想在这里发布它。这个解决方案需要使用Sanselan(或现在称为Apache Commons Imaging),并且需要合理的CMYK颜色配置文件(.icc文件)。您可以从Adobe或eci.org获取后者。

基本问题是Java只能默认读取RGB格式的JPEG文件。如果您有一个CMYK文件,则需要区分常规CMYK、Adobe CMYK(具有反转值,即255表示没有油墨,0表示最大油墨)和Adobe CYYK(一些变体也具有反转颜色)。

public class JpegReader {

    public static final int COLOR_TYPE_RGB = 1;
    public static final int COLOR_TYPE_CMYK = 2;
    public static final int COLOR_TYPE_YCCK = 3;

    private int colorType = COLOR_TYPE_RGB;
    private boolean hasAdobeMarker = false;

    public BufferedImage readImage(File file) throws IOException, ImageReadException {
        colorType = COLOR_TYPE_RGB;
        hasAdobeMarker = false;

        ImageInputStream stream = ImageIO.createImageInputStream(file);
        Iterator<ImageReader> iter = ImageIO.getImageReaders(stream);
        while (iter.hasNext()) {
            ImageReader reader = iter.next();
            reader.setInput(stream);

            BufferedImage image;
            ICC_Profile profile = null;
            try {
                image = reader.read(0);
            } catch (IIOException e) {
                colorType = COLOR_TYPE_CMYK;
                checkAdobeMarker(file);
                profile = Sanselan.getICCProfile(file);
                WritableRaster raster = (WritableRaster) reader.readRaster(0, null);
                if (colorType == COLOR_TYPE_YCCK)
                    convertYcckToCmyk(raster);
                if (hasAdobeMarker)
                    convertInvertedColors(raster);
                image = convertCmykToRgb(raster, profile);
            }

            return image;
        }

        return null;
    }

    public void checkAdobeMarker(File file) throws IOException, ImageReadException {
        JpegImageParser parser = new JpegImageParser();
        ByteSource byteSource = new ByteSourceFile(file);
        @SuppressWarnings("rawtypes")
        ArrayList segments = parser.readSegments(byteSource, new int[] { 0xffee }, true);
        if (segments != null && segments.size() >= 1) {
            UnknownSegment app14Segment = (UnknownSegment) segments.get(0);
            byte[] data = app14Segment.bytes;
            if (data.length >= 12 && data[0] == 'A' && data[1] == 'd' && data[2] == 'o' && data[3] == 'b' && data[4] == 'e')
            {
                hasAdobeMarker = true;
                int transform = app14Segment.bytes[11] & 0xff;
                if (transform == 2)
                    colorType = COLOR_TYPE_YCCK;
            }
        }
    }

    public static void convertYcckToCmyk(WritableRaster raster) {
        int height = raster.getHeight();
        int width = raster.getWidth();
        int stride = width * 4;
        int[] pixelRow = new int[stride];
        for (int h = 0; h < height; h++) {
            raster.getPixels(0, h, width, 1, pixelRow);

            for (int x = 0; x < stride; x += 4) {
                int y = pixelRow[x];
                int cb = pixelRow[x + 1];
                int cr = pixelRow[x + 2];

                int c = (int) (y + 1.402 * cr - 178.956);
                int m = (int) (y - 0.34414 * cb - 0.71414 * cr + 135.95984);
                y = (int) (y + 1.772 * cb - 226.316);

                if (c < 0) c = 0; else if (c > 255) c = 255;
                if (m < 0) m = 0; else if (m > 255) m = 255;
                if (y < 0) y = 0; else if (y > 255) y = 255;

                pixelRow[x] = 255 - c;
                pixelRow[x + 1] = 255 - m;
                pixelRow[x + 2] = 255 - y;
            }

            raster.setPixels(0, h, width, 1, pixelRow);
        }
    }

    public static void convertInvertedColors(WritableRaster raster) {
        int height = raster.getHeight();
        int width = raster.getWidth();
        int stride = width * 4;
        int[] pixelRow = new int[stride];
        for (int h = 0; h < height; h++) {
            raster.getPixels(0, h, width, 1, pixelRow);
            for (int x = 0; x < stride; x++)
                pixelRow[x] = 255 - pixelRow[x];
            raster.setPixels(0, h, width, 1, pixelRow);
        }
    }

    public static BufferedImage convertCmykToRgb(Raster cmykRaster, ICC_Profile cmykProfile) throws IOException {
        if (cmykProfile == null)
            cmykProfile = ICC_Profile.getInstance(JpegReader.class.getResourceAsStream("/ISOcoated_v2_300_eci.icc"));
        ICC_ColorSpace cmykCS = new ICC_ColorSpace(cmykProfile);
        BufferedImage rgbImage = new BufferedImage(cmykRaster.getWidth(), cmykRaster.getHeight(), BufferedImage.TYPE_INT_RGB);
        WritableRaster rgbRaster = rgbImage.getRaster();
        ColorSpace rgbCS = rgbImage.getColorModel().getColorSpace();
        ColorConvertOp cmykToRgb = new ColorConvertOp(cmykCS, rgbCS, null);
        cmykToRgb.filter(cmykRaster, rgbRaster);
        return rgbImage;
    }
}

该代码首先尝试使用常规方法读取文件,对于RGB文件有效。如果失败,则读取颜色模型的详细信息(配置文件、Adobe标记、Adobe变体)。然后读取原始像素数据(光栅)并执行所有必要的转换(YCCK到CMYK,反转颜色,CMYK到RGB)。

我对自己的解决方案不太满意。虽然颜色大多数情况下很好,但是暗区域稍微亮了一些,特别是黑色并不完全黑。如果有人知道我可以改进什么,我会很高兴听到的。

更新:

我已经找到了如何解决亮度问题。或者说:twelvemonkeys-imageio项目的人们已经找到了(请参见此文章)。这与颜色呈现意图有关。

他们的修复方法是添加以下行,这对我非常有效。基本上,修改了颜色配置文件,因为似乎没有其他方法告诉ColorConvertOp类使用感性颜色渲染意图。

    if (cmykProfile.getProfileClass() != ICC_Profile.CLASS_DISPLAY) {
        byte[] profileData = cmykProfile.getData(); // Need to clone entire profile, due to a JDK 7 bug

        if (profileData[ICC_Profile.icHdrRenderingIntent] == ICC_Profile.icPerceptual) {
            intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first

            cmykProfile = ICC_Profile.getInstance(profileData);
        }
    }

...

static void intToBigEndian(int value, byte[] array, int index) {
    array[index]   = (byte) (value >> 24);
    array[index+1] = (byte) (value >> 16);
    array[index+2] = (byte) (value >>  8);
    array[index+3] = (byte) (value);
}

2
我已经尝试了您的代码,并使用了这张图片:http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg。我使用了最新的Sanselan,没有提供任何颜色配置文件(我应该如何提供它?)。代码可以运行,但是结果颜色比原始颜色更亮。对于我来说,这还不足以投入生产。无论如何,谢谢。 - Pino
1
@Pino:我在网上发现了一个解决亮度问题的方法。请看我的更新。 - Codo
有趣,但是你错过了 intToBigEndian() 方法。 - Pino
我已经添加了缺失的方法。 - Codo
最近的JpegImageParser现在返回App14Segment而不是UnknownSegment。 - Alex K.
显示剩余3条评论

2

就像我说的那样,我的想法是将CMYK图片转换为RGB,并在我的应用程序中使用。

但由于某种原因,ConvertOp没有进行任何CMYK到RGB的转换。它只将numBand数字减少到3。因此我决定尝试使用CMYKtoRGB算法。

即获取图像,识别其颜色空间并读取或转换它。

另一个问题是Photoshop。我在互联网上找到了这句话:

在Adobe的情况下,它将CMYK配置文件包含在元数据中,但然后将原始图像数据保存为反转的YCbCrK颜色。

最终,我通过以下算法实现了我的目标。 我目前不使用icc_profiles,输出看起来有点暗...但我得到了合适的RGB图像,看起来很好。

伪代码

BufferedImage result = null;
Raster r = reader.readRaster()
if (r.getNumBands != 4){
    result = reader.read(0);
} else {

   if (isPhotoshopYCCK(reader)){
       result = YCCKtoCMYKtoRGB(r);
   }else{
      result = CMYKtoRGB(r);
   }
}

private boolean isPhotoshopYCCK(reader){
    // read IIOMetadata from reader and according to
    // http://download.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html decide which ColorSpace is used
    // or maybe there is another way to do it
    int transform = ... // 2 or 0 or something else
    return transform;
}    

展示YCCK到CMYK到RGB或CMYK到RGB算法毫无意义。在互联网上很容易找到这些。


如果这是您的解决方案,您能否发布完整的代码? - Alkanshel

0
如果您在两种完全不同的方式上使用相同的配置文件时遇到了相同的问题,我认为该文件(profile_name.icc)可能存在问题。

我从Adobe下载了大约20个配置文件。尝试了每一个,但没有任何改变。 - nixspirit
我还使用Sanselan从图片本身中提取了一个配置文件,并创建了如下代码:ICC_Profile iccProfile = Sanselan.getICCProfile(byteArrayStreamFromFile); 结果是相同的。 - nixspirit
看起来这段代码 ColorConvertOp cmykToRgb = new ColorConvertOp(cs, result.getColorModel().getColorSpace(), null); cmykToRgb.filter(r, resultRaster); 没有将该光栅从CMYK转换为RGB颜色空间。 - nixspirit

0
  1. 你正在将以CMYK格式给出的结果写成Jpeg格式,就好像它是以RGB格式编写的一样。 因此,我认为错误在于你正在查找的代码片段之外 :-)

  2. 将最后一行中的correct resul更正为result。


抱歉,我无法编辑我的帖子,否则我必须删除图片。我是这里的新用户,一些版主为我添加了这些图片。 - nixspirit
但是光栅图像应该通过 ColorConvertOp cmykToRgb = new ColorConvertOp(cs, result.getColorModel().getColorSpace(), null); cmykToRgb.filter(r, resultRaster) 进行从CMYK到RGB的转换,结果图像保存了该结果光栅。 - nixspirit
我认为你已经成功地进行了转换,但是现在,在转换之后,它以不同的格式——CMYK,4种颜色坐标而不是3种——被写入。你不能像通常那样显示它,也不能像RPG中的图像一样处理它。因此,你的转换是正确的,但之后的部分属于完全不同的任务。 - Gangnus
我在你的问题上点了+1,这样你就会有更多的积分,希望很快你就可以编辑了。顺便说一句,这个问题确实很有趣 - 它之前出现过,答案就像你在代码中写的那样,但你发现了进一步的问题。 - Gangnus
“但它后面的部分属于另一个任务。”同意。等我完成这个问题后,我会放上完整的答案和一些解释。 - nixspirit
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接