Как правильно конвертировать из CMYK в RGB на Java?
Мой Java-код для преобразования CMYK jpeg в RGB приводит к тому, что выходное изображение слишком светлое - см. Код ниже. Кто-нибудь может предложить правильный способ сделать преобразование?
Следующий код требует 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"));
}
}
6 ответов
В существующих ответах уже есть много хорошего. Но ни одно из них не является полным решением, которое обрабатывает различные виды изображений 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 была такая же проблема (см. Этот пост), и они исправили ее, исправив цветовой профиль так, что в Java используется перцептивная цветопередача. Исправление было интегрировано в приведенный выше код.
Я скопирую свой ответ из другой ветки:
Для правильного отображения изображения CMYK должны содержать информацию о цветовом пространстве в виде профиля ICC. Поэтому лучше всего использовать этот профиль ICC, который можно легко извлечь с помощью Sanselan:
ICC_Profile iccProfile = Sanselan.getICCProfile(new File("filename.jpg"));
ColorSpace cs = new ICC_ColorSpace(iccProfile);
Если к изображению не прикреплен профиль ICC, я бы по умолчанию использовал профили Adobe.
Теперь проблема в том, что вы не можете просто загрузить файл JPEG с пользовательским цветовым пространством, используя ImageIO, так как он не сможет выдать исключение, жалуясь на то, что он не поддерживает некоторое цветовое пространство или подобный стиль. Скорее всего, вам придется работать с растрами:
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, так как вы можете использовать профиль ICC, который встроен в изображение или доступен бесплатно от Adobe. Результирующее изображение будет выглядеть лучше, если не идеально (со встроенным профилем).
Существует новая библиотека с открытым исходным кодом, которая поддерживает обработку 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");
}
Мое решение основано на предыдущем ответе. Я использовал "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 to/fro RGB сложен - вы конвертируете между аддитивным и вычитающим цветом. Если вы хотите точное соответствие, вам нужно изучить профили цветового пространства для каждого устройства. То, что выглядит нормально в одном цветовом пространстве, обычно не получается, когда физически преобразуется в другое (т.е. правильный вывод CMYK - не наивный предварительный просмотр на мониторе).
Исходя из моего собственного опыта, преобразование RGB в CMYK наивно приводит к получению слишком темного изображения. Учитывая, что вы сообщаете об обратном в обратном направлении, вероятно, существует приблизительная кривая регулировки яркости, которая будет работать неплохо (но не упускайте странные нелинейности в цветовом пространстве). Если у вас есть доступ к 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();
}
}
}