如何在Java中正确地将CMYK转换为RGB?
我的Java代码将CMYK jpeg转换为RGB会导致输出图像太亮 – 请参阅下面的代码。 任何人都可以build议正确的方式来做转换?
以下代码要求Java Advanced Image 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")); } }
已有的答案中有很多好东西。 但他们都不是一个完整的解决scheme,处理不同种类的CMYK JPEG图像。
对于CMYK JPEG图像,您需要区分常规CMYK,Adobe CMYK(具有反转值,即255表示无墨,0表示最大墨)和Adobe CYYK(某些反转色的变体)。
这里的解决scheme需要Sanselan(或现在称为Apache Commons Imaging),它需要一个合理的CMYK颜色configuration文件(.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文件。 如果失败,它会读取颜色模型(configuration文件,Adobe标记,Adobe变体)的详细信息。 然后它读取原始像素数据(光栅),并进行所有必要的转换(YCCK到CMYK,反转颜色,CMYK到RGB)。
更新:
原始代码有一个小问题:结果太亮。 来自12monkeys-imageio项目的人有同样的问题(见这篇文章 ),并通过修补颜色configuration文件来修复它,以便Java使用感知颜色渲染意图。 该修复已被集成到上面的代码中。
我将从另一个线索复制我的答案:
为了正确显示,CMYK图像应该包含ICCconfiguration文件的颜色空间信息 。 所以最好的方法是使用可以用Sanselan轻松提取的ICCconfiguration文件:
ICC_Profile iccProfile = Sanselan.getICCProfile(new File("filename.jpg")); ColorSpace cs = new ICC_ColorSpace(iccProfile);
如果没有ICCconfiguration文件附加到图像,我会使用Adobeconfiguration文件作为默认。
现在的问题是,你不能只加载使用ImageIO的自定义色彩空间的JPEG文件,因为它会失败,抛出一个exception,抱怨它不支持一些颜色空间或像这样的Sthing。 Hense你将不得不与栅格工作:
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处理),这些图像颠倒了颜色值,所以产生的图像总是被倒转,甚至在反转之后,它们又变得太亮。 虽然我仍然不知道如何确切地使用它(当我需要反转像素值时),但我有一个algorithm来校正这些值并逐个像素地转换颜色:
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转换algorithm,因为您可以使用embedded到图像中或从Adobe免费获得的ICCconfiguration文件。 如果不是完美的(使用embedded的configuration文件),所生成的图像看起来会更好。
有一个支持CMYK处理的新的开源库。 所有你需要做的就是添加依赖项到你的项目中,一个新的阅读器将被添加到阅读器列表(而已知的JPEGImageReader不能处理CMYK)。 您可能会想要迭代这些阅读器,并阅读图像使用第一个不会抛出exception的阅读器。 这个软件包是一个候选版本,但我正在使用它,它解决了我们很难处理的一个巨大的问题。
http://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-jpeg/
编辑:如评论中所述,你现在也可以find一个稳定的版本,而不是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"); }
我的溶剂是基于以前的答案。 我用“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);
换一种说法:
- 以BufferedImage打开图像。
- 得到它的光栅。
- 在这个光栅中反转黑色通道。
- 转换为rgb
CMYK到/来回RGB是困难的 – 你在加法和减法颜色之间转换。 如果您想要完全匹配,则需要查看每个设备的色彩空间configuration文件。 在一个色彩空间中看起来不错的情况通常不会在物理上转换为另一个色彩空间(即适当的CMYK输出 – 而不是显示器上的天真预览)。
根据我自己的经验,将RGB转换为CMYK往往会导致图像太暗。 假设你在相反的方向上报告相反的情况,可能会find一个近似的亮度调整曲线,这将会做一个公平的工作(但要注意颜色空间内奇怪的非线性)。 如果你有权访问Photoshop,我明白它有一些CMYK预览选项,可能会加快找出这样一个近似的过程。
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(); } } }