Come convertire correttamente da CMYK a RGB in Java?

Il mio codice Java per convertire un CMYK jpeg in RGB comporta che l’immagine in uscita sia troppo leggera – vedi il codice qui sotto. Qualcuno può suggerire il modo corretto di fare la conversione?

Il codice seguente richiede Java Advanced Image IO per leggere jpeg ed 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")); } } 

Ci sono già molte cose buone nelle risposte esistenti. Ma nessuno di loro è una soluzione completa che gestisce i diversi tipi di immagini CMYK JPEG.

Per le immagini JPEG CMYK, è necessario distinguere tra normale CMYK, Adobe CMYK (con valori invertiti, ovvero 255 per nessun inchiostro e 0 per il massimo dell’inchiostro) e Adobe CYYK (anche alcune varianti con colors invertiti).

Questa soluzione richiede Sanselan (o Apache Commons Imaging come viene chiamato ora) e richiede un profilo colore CMYK ragionevole (file .icc). È ansible ottenere il successivo da Adobe o da 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 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); } 

Il codice prova prima a leggere il file usando il metodo normale, che funziona per i file RGB. Se fallisce, legge i dettagli del modello di colore (profilo, marcatore Adobe, variante Adobe). Quindi legge i dati grezzi dei pixel (raster) e fa tutta la conversione necessaria (da YCCK a CMYK, colors invertiti, da CMYK a RGB).

Aggiornare:

Il codice originale ha un piccolo problema: il risultato era troppo luminoso. Le persone del progetto twelvemonkeys-imageio hanno avuto lo stesso problema (vedi questo post ) e l’hanno risolto applicando patch al profilo colore in modo che Java utilizzasse un intento di rendering del colore percettivo. La correzione è stata integrata nel codice precedente.

Copierò la mia risposta dall’altro thread :

Per essere visualizzati correttamente, le immagini CMYK devono contenere informazioni sullo spazio colore come Profilo ICC. Quindi il modo migliore è usare quel profilo ICC che può essere facilmente estratto con Sanselan :

 ICC_Profile iccProfile = Sanselan.getICCProfile(new File("filename.jpg")); ColorSpace cs = new ICC_ColorSpace(iccProfile); 

Nel caso in cui non ci sia un profilo ICC collegato all’immagine, utilizzerei i profili Adobe come predefinito.

Ora il problema è che non è ansible caricare il file JPEG con lo spazio colore personalizzato utilizzando ImageIO in quanto non riuscirà a lanciare un’eccezione lamentandosi del fatto che non supporta lo spazio colore o lo sthing del genere. Hense dovrai lavorare con i raster:

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

È quindi ansible utilizzare il result ovunque sia necessario e avrà i colors convertiti.

In pratica, tuttavia, mi sono imbattuto in alcune immagini (scattate con la fotocamera e elaborate con Photoshop) che avevano in qualche modo invertito i valori dei colors, quindi l’immagine risultante era sempre invertita e anche dopo averli invertiti ancora una volta erano troppo luminosi. Anche se non ho ancora idea di come scoprire esattamente quando usarlo (quando ho bisogno di invertire i valori dei pixel), ho un algoritmo che corregge questi valori e converte i colors pixel per pixel:

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

Sono abbastanza sicuro che RasterOp o ColorConvertOp possano essere usati per rendere la conversazione più efficiente, ma questo è stato abbastanza per me.

Seriamente, non è necessario utilizzare questi algoritmi di conversione CMYK / RGB semplificati, poiché è ansible utilizzare il profilo ICC incorporato nell'immagine o disponibile gratuitamente da Adobe. L'immagine risultante sembrerà migliore se non perfetta (con profilo incorporato).

C’è una nuova libreria open source che supporta l’elaborazione CMYK. Tutto quello che devi fare è aggiungere la dipendenza al tuo progetto e un nuovo lettore verrà aggiunto alla lista dei lettori (mentre il noto JPEGImageReader non può trattare con CMYK). Probabilmente vorrai iterare su questi lettori e leggere l’immagine usando il primo lettore che non genera eccezioni. Questo pacchetto è un candidato alla versione, ma lo sto usando e ha risolto un enorme problema che abbiamo avuto difficoltà a gestire.

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

EDIT: come indicato nei commenti, ora puoi trovare anche una versione stabile piuttosto che RC.

Puoi eseguire l’iterazione in questo modo per ottenere BufferedImage, e dopo averlo ottenuto, il resto è semplice (puoi usare qualsiasi pacchetto di conversione di immagini esistente per salvarlo come un altro formato):

 try (ImageInputStream input = ImageIO.createImageInputStream(source)) { // Find potential readers Iterator 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"); } 

La mia sollution è basata su una risposta precedente. Ho usato “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); 

In altre parole:

  1. Apri l'immagine come BufferedImage.
  2. Prendi il suo raster.
  3. Invertire il canale nero in questo raster.
  4. Converti in rgb

CMYK a / fro RGB è difficile – stai convertendo tra colors additivi e sottrattivi. Se si desidera una corrispondenza esatta, è necessario esaminare i profili dello spazio colore per dispositivo. Quello che sembra OK in uno spazio colore di solito non lo fa quando viene fisicamente convertito in un altro (cioè un’uscita CMYK corretta – non un’anteprima ingenua su un monitor).

Dalla mia esperienza personale, la conversione RGB in CMYK tende in modo ingenuo a produrre un’immagine troppo scura. Dato che si segnala il contrario nella direzione opposta, è probabile che sia presente una curva di regolazione della luminosità approssimativa che svolgerà un lavoro equo (ma attenzione alle strane non-linearità all’interno dello spazio colore). Se hai accesso a Photoshop, capisco che ha una sorta di opzione di anteprima CMYK che potrebbe velocizzare il processo di calcolo di tale approssimazione.

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