如何在Java中正确地将CMYK转换为RGB?

22

我的Java代码将CMYK Jpeg转换为RGB后,输出的图像颜色太浅了-请参见下面的代码。 有谁可以建议正确的转换方法吗?

以下代码需要使用Java高级图像IO来读取Jpeg和example-cmyk.jpg

import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.io.File;

import javax.imageio.ImageIO;

public class TestCmykToRgb {

    public static void main(String[] args) throws Exception {
        BufferedImage cmykImage = ImageIO.read(new File(
                "j:\\temp\\example-cmyk.jpg"));


        BufferedImage rgbImage = new BufferedImage(cmykImage.getWidth(),
                cmykImage.getHeight(), BufferedImage.TYPE_INT_RGB);

        ColorConvertOp op = new ColorConvertOp(null);
        op.filter(cmykImage, rgbImage);

        ImageIO.write(rgbImage, "JPEG", new File("j:\\temp\\example-rgb.jpg"));

    }
}
6个回答

29
已有很多好的答案了,但是没有一个是完整的解决方案,可以处理不同类型的CMYK JPEG图像。
对于CMYK JPEG图像,您需要区分常规CMYK、Adobe CMYK(具有反转值,即255表示无墨水,0表示最大墨水)和Adobe CYYK(某些变体也具有反转颜色)。
这个解决方案需要使用Sanselan(或现在称为Apache Commons Imaging),并且需要合理的CMYK颜色配置文件(.icc文件)。您可以从Adobe或eci.org获取后者。
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"));

        if (cmykProfile.getProfileClass() != ICC_Profile.CLASS_DISPLAY) {
            byte[] profileData = cmykProfile.getData();

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

                cmykProfile = ICC_Profile.getInstance(profileData);
            }
        }

        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;
    }
}


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);
}

该代码首先尝试使用常规方法读取文件,适用于RGB文件。如果失败,则读取颜色模型的详细信息(配置文件、Adobe标记、Adobe变体)。然后,读取原始像素数据(光栅)并进行所有必要的转换(YCCK到CMYK,反转颜色,CMYK到RGB)。
更新:
原始代码存在轻微问题:结果过亮。twelvemonkeys-imageio项目的人们也遇到了同样的问题(请参见此post),他们通过修补颜色配置文件以便Java使用感知色彩呈现意图来解决了这个问题。修复已集成到上述代码中。

1
+1。感谢您发布的内容。这非常有帮助。这是我找到的第一个(不包括线程中的其他回复)在开箱即用方面做得相当不错的解决方案。 - Leigh
谢谢,我使用了上面的代码,并从CYMKDemo.jar中使用了一个较小的ICC文件 [LINK](http://www.randelshofer.ch/blog/2011/08/reading-cmyk-jpeg-images-with-java-imageio/),因为我不需要2MB的ECI文件。 - Omertron
太棒了。有关如何保存带有CMYK头的JPEG(以便它可以在加载到您的库中时受益于格式)的建议吗? - jedierikb
@Codo,你的解决方案非常好,但我发现它实际上大大减小了图像大小,我的图像原本是5 MB,转换后的图像只有200 KB。你能帮忙吗?- Virednra - viren
我猜你说的是JPEG图像在磁盘上的文件大小(而不是内存中的图像大小)。我的代码中没有保存为JPEG图像的内容。因此,差异可能是由于JPEG设置造成的:源文件采用CMYK和低压缩率,结果采用RGB和高压缩率。 - Codo
显示剩余4条评论

7
我会从其他线程复制我的答案:
为了正确显示CMYK图像,应该包含颜色空间信息作为ICC配置文件。因此,最好的方法是使用可以轻松提取的ICC配置文件,如Sanselan
ICC_Profile iccProfile = Sanselan.getICCProfile(new File("filename.jpg"));
ColorSpace cs = new ICC_ColorSpace(iccProfile);    

如果图像没有附加ICC配置文件,我将使用Adobe profiles作为默认值。
现在的问题是,您不能仅使用ImageIO加载具有自定义颜色空间的JPEG文件,因为它会失败并引发异常,指出它不支持某些颜色空间或其他类似的东西。因此,您必须使用光栅来解决问题:
JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(new ByteArrayInputStream(data));
Raster srcRaster = decoder.decodeAsRaster();

BufferedImage result = new BufferedImage(srcRaster.getWidth(), srcRaster.getHeight(), BufferedImage.TYPE_INT_RGB);
WritableRaster resultRaster = result.getRaster();

ColorConvertOp cmykToRgb = new ColorConvertOp(cs, result.getColorModel().getColorSpace(), null);
cmykToRgb.filter(srcRaster, resultRaster);

您可以在需要的任何地方使用result,它将转换颜色。
然而,在实践中,我遇到了一些图像(用相机拍摄并用Photoshop处理)其颜色值已经反转,因此生成的图像总是反转的,即使再次反转后它们仍然太亮。虽然我仍然不知道何时准确使用它(何时需要反转像素值),但我有一个算法可以纠正这些值,并逐像素转换颜色:
JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(new ByteArrayInputStream(data));
Raster srcRaster =  decoder.decodeAsRaster();

BufferedImage ret = new BufferedImage(srcRaster.getWidth(), srcRaster.getHeight(), BufferedImage.TYPE_INT_RGB);
WritableRaster resultRaster = ret.getRaster();

for (int x = srcRaster.getMinX(); x < srcRaster.getWidth(); ++x)
    for (int y = srcRaster.getMinY(); y < srcRaster.getHeight(); ++y) {

        float[] p = srcRaster.getPixel(x, y, (float[])null);

        for (int i = 0; i < p.length; ++i)
            p[i] = 1 - p[i] / 255f;

        p = cs.toRGB(p);

        for (int i = 0; i < p.length; ++i)
            p[i] = p[i] * 255f;

        resultRaster.setPixel(x, y, p);
    }

我很确定RasterOp或ColorConvertOp可以用来使对话更有效率,但这对我来说已经足够了。
说真的,没有必要使用这些简化的CMYK到RGB转换算法,因为你可以使用嵌入在图像中或从Adobe免费获取的ICC配置文件。如果有嵌入的配置文件,生成的图像将会看起来更好,甚至完美。

Adobe配置文件的问题在于许可证:“如果该软件是为Adobe发布的应用软件产品(“主机应用程序”)使用而设计的,则Adobe仅向您授予与主机应用程序一起使用此类软件的非独占许可,前提是您拥有来自Adobe的有效主机应用程序许可证。除非如下所述,否则此类软件的许可将受到Adobe最终用户许可协议的条款和条件的约束,该协议管理您对主机应用程序的使用。”我不确定是否允许以这种方式使用? - Joergi
1
eci.org上有免费的ICC颜色配置文件。我可以推荐ISO Coated v2 300%作为一个很好的通用配置文件。 - Codo
这个解决方案在Java 7及以上版本中无法工作,因为com.sun....JPEGImageDecoder类的包已不再可用。显然,直接使用com.sun...包是不好的做法。 - Alkanshel

4

有一个新的开源库支持CMYK处理。您只需将依赖项添加到项目中,就会在读取器列表中添加一个新的读取器(而已知的JPEGImageReader无法处理CMYK)。您可能希望遍历这些读取器,并使用第一个不会抛出异常的读取器读取图像。该软件包是一个发行候选版,但我正在使用它,它解决了我们难以处理的一个巨大问题。

http://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-jpeg/

编辑:如评论中所述,现在您也可以找到稳定版本而不是RC版。

您可以通过以下方式进行迭代以获取BufferedImage,获取后,其余部分就很容易了(您可以使用任何现有的图像转换包将其保存为另一种格式):

try (ImageInputStream input = ImageIO.createImageInputStream(source)) {

        // Find potential readers
        Iterator<ImageReader> readers = ImageIO.getImageReaders(input);

        // For each reader: try to read
        while (readers != null && readers.hasNext()) {
            ImageReader reader = readers.next();
            try {
                reader.setInput(input);
                BufferedImage image = reader.read(0);
                return image;
            } catch (IIOException e) {
                // Try next reader, ignore.
            } catch (Exception e) {
                // Unexpected exception. do not continue
                throw e;
            } finally {
                // Close reader resources
                reader.dispose();
            }
        }

        // Couldn't resize with any of the readers
        throw new IIOException("Unable to resize image");
    }

3

我的解决方案基于之前的答案。我使用了“USWebCoatedSWOP.icc”:

        //load source image
        JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(srcImageInputStream);
        BufferedImage src = decoder.decodeAsBufferedImage();
        WritableRaster srcRaster = src.getRaster();
        //prepare result image
        BufferedImage result = new BufferedImage(srcRaster.getWidth(), srcRaster.getHeight(), BufferedImage.TYPE_INT_RGB);
        WritableRaster resultRaster = result.getRaster();
        //prepare icc profiles
        ICC_Profile iccProfileCYMK = ICC_Profile.getInstance(new FileInputStream("path_to_cmyk_icc_profile"));
        ColorSpace sRGBColorSpace = ColorSpace.getInstance(ColorSpace.CS_sRGB);

        //invert k channel
        for (int x = srcRaster.getMinX(); x < srcRaster.getWidth(); x++) {
            for (int y = srcRaster.getMinY(); y < srcRaster.getHeight(); y++) {
                float[] pixel = srcRaster.getPixel(x, y, (float[])null);
                pixel[3] = 255f-pixel[3];
                srcRaster.setPixel(x, y, pixel);
            }
        }

        //convert
        ColorConvertOp cmykToRgb = new ColorConvertOp(new ICC_ColorSpace(iccProfileCYMK), sRGBColorSpace, null);
        cmykToRgb.filter(srcRaster, resultRaster);

换句话说:

  1. 打开图像作为BufferedImage。
  2. 获取其光栅。
  3. 反转此光栅中的黑色通道。
  4. 转换为rgb。

JPEGImageDecoder使用了一个com.sun包,在Java 7中已被删除。 - Alkanshel

1
    import java.awt.color.ColorSpace;
    import java.awt.color.ICC_ColorSpace;
    import java.awt.color.ICC_Profile;
    import java.io.IOException;
    import java.util.Arrays;


    public class ColorConv {
final static String pathToCMYKProfile = "C:\\UncoatedFOGRA29.icc";

public static float[] rgbToCmyk(float... rgb) throws IOException {
    if (rgb.length != 3) {
        throw new IllegalArgumentException();
    }
    ColorSpace instance = new ICC_ColorSpace(ICC_Profile.getInstance(pathToCMYKProfile));
    float[] fromRGB = instance.fromRGB(rgb);
    return fromRGB;
}
public static float[] cmykToRgb(float... cmyk) throws IOException {
    if (cmyk.length != 4) {
        throw new IllegalArgumentException();
    }
    ColorSpace instance = new ICC_ColorSpace(ICC_Profile.getInstance(pathToCMYKProfile));
    float[] fromRGB = instance.toRGB(cmyk);
    return fromRGB;
}

public static void main(String... args) {
    try {
        float[] rgbToCmyk = rgbToCmyk(1.0f, 1.0f, 1.0f);
        System.out.println(Arrays.toString(rgbToCmyk));
        System.out.println(Arrays.toString(cmykToRgb(rgbToCmyk[0], rgbToCmyk[1], rgbToCmyk[2], rgbToCmyk[3])));
    } catch (IOException e) {
        e.printStackTrace();
    }
}
}

1
CMYK 转换为 RGB 或者从 RGB 转换为 CMYK 都是困难的 - 这涉及到加法和减法颜色的转换。如果你想要一个精确匹配,你需要研究每个设备的颜色空间配置文件。在一个颜色空间中看起来不错的东西,当它被转换到另一个颜色空间时通常就不再好看了(例如真正的 CMYK 输出 - 而不是在显示器上的简单预览)。
根据我的经验,简单地将 RGB 转换为 CMYK 往往会导致图像变得太暗。考虑到你报告相反的情况,可能可以找到一种适当的亮度调整曲线,它可以做出一个不错的效果(但要注意颜色空间内的奇怪非线性)。如果你有使用 Photoshop 的机会,我知道它有一些 CMYK 预览选项,可能可以加快找出这样一个近似值的过程。

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