¿Cómo convertir de CMYK a RGB en Java correctamente?
(6)
Mi código Java para convertir un jpeg CMYK a RGB da como resultado que la imagen de salida sea demasiado clara - vea el código a continuación. ¿Alguien puede sugerir la forma correcta de hacer la conversión?
El siguiente código requiere Java Advanced Image IO para leer el archivo jpeg y 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"));
}
}
CMYK hacia adelante / atrás RGB es difícil: está convirtiendo el color entre aditivo y sustractivo. Si desea una coincidencia exacta, debe buscar en los perfiles de espacio de color por dispositivo. Lo que se ve bien en un espacio de color generalmente no se convierte físicamente en otro (es decir, salida CMYK adecuada, no una vista previa ingenua en un monitor).
Desde mi propia experiencia, convertir RGB a CMYK ingenuamente da como resultado una imagen que es demasiado oscura. Dado que usted informa lo contrario en la dirección opuesta, probablemente se encuentre una curva de ajuste de brillo aproximado que hará un buen trabajo (pero tenga cuidado con extrañas no linealidades dentro del espacio de color). Si tiene acceso a Photoshop, entiendo que tiene algún tipo de opción de vista previa CMYK que podría acelerar el proceso de cálculo de dicha aproximación.
Copiaré mi respuesta del otro hilo :
Para que se muestren correctamente, las imágenes CMYK deben contener información sobre el espacio de color como Perfil ICC. Entonces, la mejor manera es usar ese perfil ICC que se puede extraer fácilmente con Sanselan :
ICC_Profile iccProfile = Sanselan.getICCProfile(new File("filename.jpg"));
ColorSpace cs = new ICC_ColorSpace(iccProfile);
En caso de que no haya un Perfil ICC adjunto a la imagen, usaría los perfiles de Adobe como predeterminados.
Ahora el problema es que no se puede simplemente cargar un archivo JPEG con espacio de color personalizado usando ImageIO, ya que fallará arrojando una excepción quejándose de que no admite espacio de color o algo así. Hense deberás trabajar con rásteres:
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);
A continuación, puede usar el result
donde lo necesite y habrá convertido los colores.
En la práctica, sin embargo, me he topado con algunas imágenes (tomadas con la cámara y procesadas con Photoshop) que de alguna manera habían invertido los valores de color, por lo que la imagen resultante siempre se invertía e incluso después de invertirlas eran demasiado brillantes. Aunque todavía no tengo idea de cómo saber exactamente cuándo usarlo (cuando necesito invertir los valores de píxel), tengo un algoritmo que corrige estos valores y convierte el color píxel por píxel:
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);
}
Estoy bastante seguro de que RasterOp o ColorConvertOp podrían usarse para hacer que la conversación sea más eficiente, pero esto fue suficiente para mí.
En serio, no hay necesidad de utilizar estos algoritmos de conversión de CMYK a RGB simplificados ya que puede usar el Perfil ICC que está incorporado en la imagen o está disponible de forma gratuita desde Adobe. La imagen resultante se verá mejor si no perfecta (con perfil incrustado).
Hay una nueva biblioteca de código abierto que admite el procesamiento CMYK. Todo lo que necesita hacer es agregar la dependencia a su proyecto y se agregará un nuevo lector a la lista de lectores (mientras que el conocido JPEGImageReader no puede tratar con CMYK). Probablemente desee iterar sobre estos lectores y leer la imagen con el primer lector que no arroje una excepción. Este paquete es un candidato de lanzamiento, pero lo estoy usando y resolvió un gran problema con el que tuvimos problemas para lidiar.
http://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-jpeg/
EDITAR: como se indica en los comentarios, ahora también puede encontrar una versión estable en lugar de RC.
Puede hacer la iteración de esta manera para obtener la imagen almacenada en búfer, y una vez que la obtuvo, el resto es fácil (puede usar cualquier paquete de conversión de imagen existente para guardarlo como otro formato):
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");
}
Mi solución se basa en una respuesta anterior. Utilicé "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);
En otras palabras:
- Abra la imagen como Imagen Buffered.
- Obtenga su ráster.
- Invierta el canal negro en este ráster.
- Convertir a rgb
Ya hay muchas cosas buenas en las respuestas existentes. Pero ninguno de ellos es una solución completa que maneje los diferentes tipos de imágenes JPEG CMYK.
Para imágenes JPEG CMYK, debe distinguir entre CMYK normal, Adobe CMYK (con valores invertidos, es decir, 255 para sin tinta y 0 para tinta máxima) y Adobe CYYK (también una variante con colores invertidos).
Esta solución requiere Sanselan (o Apache Commons Imaging como se llama ahora) y requiere un perfil de color CMYK razonable (archivo .icc). Puede obtener el último de Adobe o de 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);
}
El código primero intenta leer el archivo usando el método regular, que funciona para archivos RGB. Si falla, lee los detalles del modelo de color (perfil, marcador de Adobe, variante de Adobe). Luego lee los datos brutos de píxel (ráster) y realiza toda la conversión necesaria (YCCK a CMYK, colores invertidos, CMYK a RGB).
Actualizar:
El código original tiene un pequeño problema: el resultado fue demasiado brillante. Las personas del proyecto twelvemonkeys-imageio tuvieron el mismo problema (ver esta post ) y lo arreglaron parcheando el perfil de color de manera que Java utiliza un intento de representación de color perceptual. La solución se ha integrado en el código anterior.
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();
}
}
}