Qualitätsunterschiede bei JPEG

blz

Bekanntes Mitglied
Hi
ich hab mir ein Programm geschrieben, mit dem ich JPEG Bilder auf 400x400 reduziere.
die DPI Zahl des original bildes wird dabei beibehalten.

jetzt habe ich mal das gleiche bild in photoshop auf die gleiche größe und dpi zahl verkleinert und habe beim speichern die beste qualitätsstufe (12) gewählt.

nun ist das von meinem programm erstellte bild 37KB groß und das von Photoshop 162KB.
Letzteres sieht wesentlich besser aus.
(bittiefe ist jeweils 24)

Nun die Frage: was kann ich da bei der komprimierung ausser auflösung, dpi und bittiefe noch verändern, damit meins genauso gut aussieht? die 4fache größe würde ich gerne in kauf nehmen...
 

Ark

Top Contributor
Ich weiß zwar nicht, wie man das mit den Standard-APIs hinbekommt, aber generell gibt es noch mindestens einen Wert in JPEG (irgendeine Arraygröße oder -auflösung oder so was), der von vielen Bildbearbeitungsprogrammen benutzt wird, um die "Qualität" des Bildes einzustellen (etwa beim Exportieren nach JPEG).

Was die Bittiefe angeht: Kommt darauf an, welches Farbmodell du benutzt. ;) Soweit ich weiß, schreibt da JPEG nichts vor, aber typischerweise verwendet man für JPEG das YCbCr-Farbmodell (und eben nicht RGB). Dabei wird meist noch meist ein Chroma-Subsampling vorgenommen, sprich: Cb und Cr werden meist schlechter aufgelöst als Y, da das menschliche Auge für Helligkeitsunterschiede empfindlicher ist als für Farbunterschiede. Vielleicht kann man da noch was rausholen. Da kommt es halt auch ganz darauf an, wer das Ergebnis als nächstes "verarbeiten" soll (Mensch oder Maschine :D).

Da du die Auflösung verringerst, solltest du das Bild vor(!) dem Verkleinern durch einen geeigneten Tiefpass (z.B. Gauß) jagen, sonst gibt's hässliche Aliasing-Effekte. Und zum Verkleinern gibt es ja grundsätzlich verschiedene Verfahren. Das als zumeist "beste" Verfahren präsentierte dürfte https://en.wikipedia.org/wiki/Lanczos_resampling sein.

Mal davon abgesehen, meine ich, kann JPEG auch verlustfrei speichern (keine Ahnung, ob das stimmt :D). Spricht eigentlich was gegen PNG?

Ark
 

Marco13

Top Contributor
PNG ist verlustfrei, d.h. wenn man das Bild als PNG speichert und dann wieder lädt kommt GENAU das gleiche raus, wie vorher. Bei JPG geht (abhängig vom Kompressionsgrad) Information verloren, und die Bildqualität wird ggf. schlechter.

Da die Frage aber immer wieder mal kommt, habe ich gerade mal das Snippet zusammengefrickelt:
Java:
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;

/**
 * Utility class for writing JPG files with a specific image quality.
 */
public class ImageWritingUtility
{
    /**
     * A simple test
     * 
     * @param args Not used
     * @throws IOException If an IO error occurs
     */
    public static void main(String[] args) throws IOException
    {
        BufferedImage image = ImageIO.read(new File("ImageA.jpg"));
        writeJPG(image, new File("ImageA_0.0.jpg"), 0.0f);
        writeJPG(image, new File("ImageA_0.1.jpg"), 0.1f);
        writeJPG(image, new File("ImageA_0.5.jpg"), 0.5f);
        writeJPG(image, new File("ImageA_0.9.jpg"), 0.9f);
        writeJPG(image, new File("ImageA_1.0.jpg"), 1.0f);
    }
    
    /**
     * Write the given RenderedImage as a JPEG to the given file,
     * using the given quality. The quality must be a value between
     * 0 (lowest quality, maximum compression) and 1 (highest 
     * quality, minimum compression)
     *  
     * @param renderedImage The image to write
     * @param file The file to write to
     * @param quality The quality, between 0 and 1
     * @throws IOException If an IO error occurs
     */
    public static void writeJPG(RenderedImage renderedImage, 
        File file, float quality) throws IOException
    {
        OutputStream outputStream = null;
        try
        {
            outputStream = new FileOutputStream(file);
            writeJPG(renderedImage, outputStream, quality);
        }
        finally
        {
            if (outputStream != null)
            {
                outputStream.close();
            }
        }
    }
    
    /**
     * Write the given RenderedImage as a JPEG to the given outputStream,
     * using the given quality. The quality must be a value between
     * 0 (lowest quality, maximum compression) and 1 (highest 
     * quality, minimum compression). The caller is responsible for
     * closing the given stream. 
     *  
     * @param renderedImage The image to write
     * @param outputStream The stream to write to
     * @param quality The quality, between 0 and 1
     * @throws IOException If an IO error occurs
     */
    public static void writeJPG(RenderedImage renderedImage, 
        OutputStream outputStream, float quality) throws IOException
    {
        Iterator<ImageWriter> imageWriters = 
            ImageIO.getImageWritersByFormatName("jpeg");
        ImageWriter imageWriter = imageWriters.next();
        ImageOutputStream imageOutputStream = 
            ImageIO.createImageOutputStream(outputStream);
        imageWriter.setOutput(imageOutputStream);
        ImageWriteParam param = imageWriter.getDefaultWriteParam();
        param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        param.setCompressionQuality(quality);
        IIOImage iioImage = new IIOImage(renderedImage, null, null);
        imageWriter.write(null, iioImage, param);
    }
}
 

Ark

Top Contributor
Was heißt "besser"? Das kommt auf den Anwendungsfall an. Es ist garantiert verlustfrei (also nix mit "Qualitäts"-Gefummel wie bei JPEG). Es ist so gut wie immer größer (vom Speicherverbrauch). Verluste durch Umrechnungen in ein anderes Farbmodell (RGB → YCbCr) können nicht entstehen … es ist halt einfach PNG und nicht JPEG. ^^

Verkleinern musst du dennoch selbst, das wird dir PNG nicht abnehmen. ;) Insofern sichert PNG nur zu, dass du keinen Verlust durch irgendwelche schlecht gewählten Parameter bei der JPEG-Kompression hast. Oder anders ausgedrückt: Wenn du PNG wählst, ist das Bild garantiert nicht aufgrund von JPEG-Kompressionsartefakten etc. schlechter als das Referenzbild. (Ob das den notwendigen/hinreichenden Unterschied macht, musst du selbst wissen.)

Dem gegenüber steht aber ein wahrscheinlich sehr viel größerer Speicherverbrauch bei PNG an, zumindest bei Fotos etc. Wenn es dir da um die Größe geht, kannst du eventuell noch ein bisschen auf Kosten von Zeit rausholen, indem du bei der PNG-Komprimierung was einstellst. (Keine Ahnung, wie man das mit den Standard-APIs macht, und bei Fotos wird sich das wahrscheinlich nicht lohnen, für das letzte bisschen kann man z.B. zu OptiPNG greifen, aber wie gesagt: viel bringen wird's wahrscheinlich nicht.)

Was das Verkleinern angeht: ich weiß nicht, ob die Standard-APIs dafür was anbieten, und ich weiß auch nicht, ob Lanczos einen geeigneten Tiefpass impliziert (wenn es so wäre, brauchst du keinen geeigneten Tiefpass wählen, also z.B. Gauß, Butterworth, Sinc-Filter-Annäherung, etc., kommt halt auf den Anwendungsfall an).

@Marco13: Vielen Dank für das Beispiel. So was könnte ja dann zu den FAQs. :) Dort kannst du dann auch gleich erklären, wie man ein PNG-Bild (mit ImageIO) einliest, dass der Alpha-Kanal erhalten bleibt. Das habe ich nämlich bis heute nicht rausgekriegt.

Ark
 

xehpuk

Top Contributor
Dort kannst du dann auch gleich erklären, wie man ein PNG-Bild (mit ImageIO) einliest, dass der Alpha-Kanal erhalten bleibt. Das habe ich nämlich bis heute nicht rausgekriegt.
KSKB or it didn't happen! ;)
Code:
ImageIO.read()
hat mich da bisher nicht im Stich gelassen.
 

Bile Demon

Bekanntes Mitglied
Der Fehler muss wohl schon ziemlich lange behoben sein. Ich arbeite seit über 2 Jahren via ImageIO mit transparenten PNGs, hat soweit immer funktioniert.

Hat mich selbst erstaunt wie einfach das alles ist, und dass man sich nichtmal um den Alpha-Kanal selbst kümmern muss ^^
 

blz

Bekanntes Mitglied
ok erstmal danke fuer all die Beitraege!
Ich bin jetzt etwas unsicher, wie ich vorgehen soll.

Was ich machen will:

Ein Programm schreiben, dass fuer alle CD-Front-Cover meiner mp3 Sammlung (die in verschiedensten Qualitäten und Auflösungen vorliegen) eine neue Datei erstellt, die sagen wir mal nicht größer als 200 kB ist.
(Denn ich möchte die Cover dann direkt in den ID3Tag speichern und will die Datei nicht unnötig groß machen, da meine Versuche ergeben haben, dass man bei dieser Größe auf jeden Fall Covers hat, die qualitativ ausreichend für den IPod oder den CoverFlow in iTunes sind)

Jetzt habe ich festgestellt gibt es ja unterschiedliche Vorgehen:

1. Einerseits wie in dem obigen Java Code, wo die Kompressions(?)Qualität verändert wird. Die Auflösung
bleibt aber ja die gleiche. (Was doch theoretisch sinnos ist, oder? Weil für ein Bild mit der obigen
Qualität 0.0 wuerde ja auch eine kleinere Auflösung reichen, oder?)

2. Andererseits kann man ja auch die Auflösung runterschrauben. Meine Versuche haben ergeben, dass zB 400x400 ausreichend ist, solange die Qualität gut ist.

Ich habe hier mal eine (vermutlich dilletantische) Klasse geschrieben, die die Auflösung auf 400x400 runterschraubt:

[Java]
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class ScaleImage {

public static void main(String[] args) throws IOException {

BufferedImage originalImage = ImageIO.read(new File("c:/front.png"));

double height = originalImage.getHeight();
double width = originalImage.getWidth();

double newHeight = 400;
double newWidth = 400;

//Pruefen, ob mindestens entweder Hoehe oder Breite des Covers groesser als 400 ist.
//Falls ja, wird die groessere Seite auf 400, die kleinere auf den dem Seitenverhaeltnis
//des Originals entsprechenden kleineren Wert hinuntergerechnet.
if (width > 400 || height > 400) {
if (width > height) {
newHeight = height / (width / 400);
} else if (height > width) {
newWidth = width / (height / 400);
}
}

int type = originalImage.getType();

BufferedImage scaledImage = new BufferedImage((int)newWidth, (int)newHeight, type);

Graphics2D g2d = (Graphics2D) scaledImage.getGraphics();

g2d.drawImage(originalImage, 0, 0, (int)newWidth, (int)newHeight, null);

ImageIO.write(scaledImage, "png", new File("/bild-small.png"));
}
}

[/Java]


Danach ist aber auf jeden Fall die Qualität dahin...




Was waere wohl das beste Vorgehen? Erst die Auflösung auf 400x400 und falls die Datei dann noch zu groß ist, die Quali runterschrauben - oder andersrum?
Oder ist das nicht so trivial :)?
 

Marco13

Top Contributor
Die Auflösung und die Qualität sind eigentlich unabhängig voneinander - aber beide beeinflussen die Dateigröße.

Das gepostete ist grundsätzlich richtig. Ich weiß gerade nicht, was der defaultwert ist, aber ggf. bleibt die Qualität durch sowas wie
Java:
graphics2D.setRenderingHint(
    RenderingHints.KEY_INTERPOLATION,
    RenderingHints.VALUE_INTERPOLATION_BILINEAR);
etwas besser. Ausführlicher (nicht mehr ganz taufrisch, aber immernoch in weiten Teilen interessant und relevant dafür) steht's auf The Perils of Image.getScaledInstance() | Java.net

Wenn das Bild auf die richtige Größe runterskaliert ist, kommt der zweite Schritt: Das Speichern. Entweder als PNG (verlustfrei) oder eben als JPG. Da könnte man dann

Sooo, hier hatte ich beim Schreiben eine pause gemacht, und mal was zusammegefrickelt: Ein paar Klassen rund um einen "ImageLimiter" herum (die verwenden auch das oben gepostete ImageWritingUtility), der im Prinzip diese Funktionen anbietet:
- Man kann ihm ein Bild geben
- Man kann eine maximale Auflösung (Größe) vorgeben
- Man kann eine Qualität vorgeben
- Man kann eine maximale Dateigröße vorgeben.
- Man kann sich das Ergebnisbild holen um es anzuzeigen
Qualität und Dateigröße sind natürlich "gegenläufig" und das eine wird automatisch neu berechnet und aktualisiert wenn sich das andere ändert. Bei der Abhängigkeit von "maximale Dateigröße" zu "Qualität" ist das ein bißchen frickelig, da hab' ich so eine absurd-aufwändige Form einer ""binären Suche"" eingebaut, die so lange an der Qualität rumpfuscht bis die Dateigröße eingehalten wird. Insgesamt ist das dadurch und durch die "on-the-fly-updates" beim Bewegen der Slider recht rechenintensiv, aber vielleicht ganz praktisch.
Java:
package javaforum.imagelimiter;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class ImageLimiterTest 
{
    public static void main(String[] args) throws IOException
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                try
                {
                    createAndShowGUI();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        });
    }
    
    private static void createAndShowGUI() throws IOException
    {
        JFrame f = new JFrame("ImageLimiter");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ImageLimiterPanel imageLimiterPanel = new ImageLimiterPanel(new ImageLimiter());
        BufferedImage inputImage = ImageIO.read(new File("iPod image.jpg"));
        imageLimiterPanel.setInputImage(inputImage);
        f.getContentPane().add(imageLimiterPanel);
        f.setSize(800,600);
        f.setVisible(true);
    }
}

class ImageLimiter
{
    private BufferedImage inputImage;
    private BufferedImage scaledImage;
    private BufferedImage outputImage;
    
    private int maxResolution;
    private float quality;

    private int fileSizeBytes;
    
    public void setInputImage(BufferedImage inputImage)
    {
        this.inputImage = inputImage;
        this.maxResolution = Math.max(inputImage.getWidth(), inputImage.getHeight());
        this.quality = 1.0f;
        this.scaledImage = computeScaledImage(inputImage, maxResolution);
        updateOutputImage();
    }
    
    public BufferedImage getOutputImage()
    {
        return outputImage;
    }

    public int getFileSizeBytes()
    {
        return fileSizeBytes;
    }
    
    public void setMaxResolution(int maxResolution)
    {
        this.maxResolution = maxResolution;
        this.scaledImage = computeScaledImage(inputImage, maxResolution);
        updateOutputImage();
    }
    
    public void setQuality(float quality)
    {
        this.quality = quality;
        updateOutputImage();
    }
    
    public float getQuality()
    {
        return quality;
    }
    
    public void setMaxFileSize(int maxFileSizeBytes)
    {
        this.quality = computeQuality(scaledImage, maxFileSizeBytes);
        updateOutputImage();
    }
    
    private void updateOutputImage()
    {
        try
        {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageWritingUtility.writeJPG(scaledImage, baos, quality);
            baos.close();
            byte data[] = baos.toByteArray();
            fileSizeBytes = data.length;
            ByteArrayInputStream bais = new ByteArrayInputStream(data);
            outputImage = ImageIO.read(bais);
            bais.close();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
    

    static float computeQuality(BufferedImage image, int sizeLimitBytes)
    {
        int minSizeBytes = computeSizeBytes(image, 0.0f);
        if (sizeLimitBytes < minSizeBytes)
        {
            return 0.0f;
        }
        int maxSizeBytes = computeSizeBytes(image, 1.0f);
        if (sizeLimitBytes > maxSizeBytes)
        {
            return 1.0f;
        }
        float intervalSize = 0.5f;
        float quality = 0.5f;
        float lastSmaller = 0;
        while (true)
        {
            int sizeBytes = computeSizeBytes(image, quality);
            if (sizeBytes >= sizeLimitBytes)
            {
                //System.out.println("For "+quality+" have size "+sizeBytes+", decrease quality by "+intervalSize);
                quality -= intervalSize;
                intervalSize /= 2;
            }
            else if (sizeBytes < sizeLimitBytes)
            {
                //System.out.println("For "+quality+" have size "+sizeBytes+", increase quality by "+intervalSize);
                lastSmaller = quality;
                quality += intervalSize;
                intervalSize /= 2;
            }
            if (intervalSize < 0.01f)
            {
                break;
            }
        }
        return lastSmaller;
    }
    
    private static int computeSizeBytes(BufferedImage image, float quality)
    {
        quality = Math.min(1, Math.max(0, quality));
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try
        {
            ImageWritingUtility.writeJPG(image, baos, quality);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            try
            {
                baos.close();
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
        }
        byte data[] = baos.toByteArray();
        return data.length;
    }
    
    private static BufferedImage computeScaledImage(BufferedImage input, int limit)
    {
        int w = input.getWidth();
        int h  = input.getHeight();
        float aspect = (float)w / h;
        if (aspect > 1)
        {
            w = limit;
            h = (int)(w / aspect);
        }
        else
        {
            h = limit;
            w = (int)(h * aspect);
        }
        BufferedImage output = new BufferedImage(
            w, h, BufferedImage.TYPE_INT_ARGB);     
        
        Graphics2D g = output.createGraphics();
        g.setRenderingHint(
            RenderingHints.KEY_INTERPOLATION,
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(input, 0, 0, w, h, null);
        g.dispose();
        
        return output;
    }
    
    
}


class ImageLimiterPanel extends JPanel
{
    private ImageLimiter imageLimiter;

    private ImageIcon inputImageIcon;
    private ImageIcon outputImageIcon;
    
    private JScrollPane inputScrollPane;
    private JScrollPane outputScrollPane;

    private JSlider qualitySlider;
    private JLabel qualityLabel;
    
    private JSlider resolutionLimitSlider;
    private JLabel resolutionLimitLabel;
    
    private JSpinner sizeLimitSpinner;
    private JLabel sizeLimitLabel;
    
    private boolean updating = false;
    
    public ImageLimiterPanel(ImageLimiter imageLimiter)
    {
        this.imageLimiter = imageLimiter;
        
        setLayout(new BorderLayout());
        
        final JSplitPane splitPane = new JSplitPane();
        
        inputImageIcon = new ImageIcon();
        JLabel inputImageLabel = new JLabel(inputImageIcon);
        inputScrollPane = new JScrollPane(inputImageLabel);
        inputScrollPane.setBorder(BorderFactory.createTitledBorder("Input"));
        splitPane.setLeftComponent(inputScrollPane);
        
        outputImageIcon = new ImageIcon();
        JLabel outputImageLabel = new JLabel(outputImageIcon);
        outputScrollPane = new JScrollPane(outputImageLabel);
        outputScrollPane.setBorder(BorderFactory.createTitledBorder("Output"));
        splitPane.setRightComponent(outputScrollPane);
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                splitPane.setDividerLocation(0.5);
            }
        });
        
        add(splitPane, BorderLayout.CENTER);

        
        JPanel controlPanel = new JPanel(new GridLayout(0,1));

        JPanel resolutionLimitPanel = createResolutionLimitPanel();
        controlPanel.add(resolutionLimitPanel);

        JPanel qualityPanel = createQualityPanel();
        controlPanel.add(qualityPanel);
        
        JPanel sizePanel = createSizeLimitPanel();
        controlPanel.add(sizePanel);
        
        add(controlPanel, BorderLayout.NORTH);
        
    }
    
    public void setInputImage(BufferedImage inputImage)
    {
        imageLimiter.setInputImage(inputImage);
        inputImageIcon.setImage(inputImage);
        int max = Math.max(inputImage.getWidth(), inputImage.getHeight());
        resolutionLimitSlider.setMaximum(max);        
        resolutionLimitSlider.setValue(max);        
    }

    private JPanel createResolutionLimitPanel()
    {
        JPanel resolutionLimitPanel = new JPanel(new BorderLayout());
        resolutionLimitLabel = new JLabel("Resolution: ");
        resolutionLimitLabel.setPreferredSize(new Dimension(300, 10));
        resolutionLimitPanel.add(resolutionLimitLabel, BorderLayout.WEST);
        resolutionLimitSlider = new JSlider(0,100,80);
        resolutionLimitSlider.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent arg0)
            {
                if (!updating)
                {
                    updating = true;

                    int maxResolution = resolutionLimitSlider.getValue();
                    imageLimiter.setMaxResolution(maxResolution);
                    updateOutputImage(imageLimiter.getOutputImage());
                    
                    sizeLimitLabel.setText("Size: "+imageLimiter.getFileSizeBytes());
                    sizeLimitSpinner.setValue(imageLimiter.getFileSizeBytes());
                    
                    updating = false;
                }
            }
        });
        resolutionLimitPanel.add(resolutionLimitSlider, BorderLayout.CENTER);
        return resolutionLimitPanel;
    }
    
    private JPanel createQualityPanel()
    {
        JPanel qualityPanel = new JPanel(new BorderLayout());
        qualityLabel = new JLabel("Quality: ");
        qualityLabel.setPreferredSize(new Dimension(300, 10));
        qualityPanel.add(qualityLabel, BorderLayout.WEST);
        qualitySlider = new JSlider(0,100,80);
        qualitySlider.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent arg0)
            {
                if (!updating)
                {
                    updating = true;
                    
                    float quality = qualitySlider.getValue()/100.0f;
                    imageLimiter.setQuality(quality);
                    updateOutputImage(imageLimiter.getOutputImage());
                    
                    qualityLabel.setText("Quality: "+String.format("%.2f", quality));
                    sizeLimitLabel.setText("Size: "+imageLimiter.getFileSizeBytes());
                    sizeLimitSpinner.setValue(imageLimiter.getFileSizeBytes());
                    
                    updating = false;
                }
            }
        });
        qualityPanel.add(qualitySlider, BorderLayout.CENTER);
        return qualityPanel;
    }
    
    private JPanel createSizeLimitPanel()
    {
        JPanel sizeLimitPanel = new JPanel(new BorderLayout());
        sizeLimitLabel = new JLabel("Size: ");
        sizeLimitLabel.setPreferredSize(new Dimension(300, 10));
        sizeLimitPanel.add(sizeLimitLabel, BorderLayout.WEST);
        sizeLimitSpinner = new JSpinner(new SpinnerNumberModel(10000, 0, 1000000000, 1000));
        sizeLimitSpinner.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent arg0)
            {
                if (!updating)
                {
                    updating = true;
                    
                    int sizeLimit = (Integer)sizeLimitSpinner.getValue();
                    imageLimiter.setMaxFileSize(sizeLimit);
                    updateOutputImage(imageLimiter.getOutputImage());
                    
                    qualityLabel.setText("Quality: "+String.format("%.2f", imageLimiter.getQuality()));
                    qualitySlider.setValue((int)(imageLimiter.getQuality()*100));
                    
                    sizeLimitLabel.setText("Size: "+imageLimiter.getFileSizeBytes());
                    sizeLimitSpinner.setValue(imageLimiter.getFileSizeBytes());

                    updating = false;
                }

            }
        });
        sizeLimitPanel.add(sizeLimitSpinner, BorderLayout.CENTER);
        return sizeLimitPanel;
    }
    
    private void updateOutputImage(BufferedImage outputImage)
    {
        outputImageIcon.setImage(outputImage);
        outputScrollPane.invalidate();
        revalidate();
        outputScrollPane.repaint();
    }
    
}


Genau das richtige für einen Freitag Abend :D

Eigentlich wollte ich den "ImageLimiter" auch noch aufbohren, so dass man ihm eine Auflösung und Qualität vorgeben kann, und er dann über alle Dateien in einem Verzeichnis läuft und die Bilder anpasst, aber das wäre dann ... sogar für einen Freitag abend zu langweilig (zumal ich das alles ja gar nicht brauche :bahnhof: )
 

blz

Bekanntes Mitglied
Wow!
Erstmal vielen Dank, ich hab das jetzt mal durchgelesen und werd erstmal etwas brauchen um das zu verarbeiten und zu verstehen, aber wollt schon mal danke sagen.
Was für eine Hammer Antwort auf ne Forumsfrage!
Mit Nachfragen ist fruehestens ab morgen zu rechnen :)
 

Marco13

Top Contributor
Nicht überbewerten. Das ist nur recht schnell hingeschrieben (und wie man sieht: Nicht eine einzige Kommentarzeile :oops: ), aber vielleicht sind ein paar nützliche Zeilen dabei.
 

blz

Bekanntes Mitglied
So, hab jetzt mal einige Zeit mit deinem Tool rumgespielt, ganz interessant.. :)

Insgesamt erscheint es auf jeden Fall definitiv so, dass man erst mal die Qualität runterschrauben sollte und die Auflösung beibehalten sollte.
Die Datei wird deutlich kleiner und bis zu ner Quali von 0.5 ist kaum was zu sehen.

Allerdings ist es ja jetzt bei dem Tool zB so, dass wenn ich ein 4MB Bild einles und dann vorgebe, dass ichs gern maximal 200KB gross haben will, dass dann einfach die Qualität soweit heruntergeschraubt wird, dass es passt.

Interessant waere zu wissen, ob es nicht zB am besten waer, die Quali zu einem grossen Teil runterzuschrauben aber auch ein bisschen die Auflösung zu verkleinern, um einen minimalen Qualitätsverlust zu bekommen. Die Frage ist nur, wie man das rausfindet...
 

Marco13

Top Contributor
Es gäbe theoretisch ja drei Optionen:

Auflösung fest: Qualität <-> Dateigröße
Qualität fest: Auflösung <-> Dateigröße
Dateigröße fest: Auflösung <-> Qualität

Im Moment wird nur das erste von dem Programm unterstützt. Eigentlich wollte ich es auch noch dahin gehend erweitern, dass man sich auswählen kann (mit einem RadioButton) welche der Größen fixiert sein soll, und welche beiden Slider man dann noch (gegenläufig) bewegen kann, aber ich bin nach dem, was du gesagt hast, davon ausgegangen, dass die Auflösung am Anfang fest vorgegeben wird. Vielleicht erweitere ich es irgendwann nochmal...
 

blz

Bekanntes Mitglied
Ja ok, wobei ich die 3. Option nicht brauch, weil es geht mir ja genau darum, die Dateigroesse unter ne bestimmte Schranke zu bringen.

Von daher bleibt eben die Frage, ob es sinnvoll ist, entweder
1. nur Quali runterzuschrauben
2. nur Auflösung runterzuschrauben
3. Beides runterzuschrauben (und was wieviel..)

Habe jetzt schon einiges über jpg gelesen, aber darauf hab ich noch keine antwort gefunden.
ausserdem kann ich ja bei aufloesung einerseits die pixelmaße verändern (also von 1600x1600 auf 400x400) und andererseits die dpi zahl.
was mach ich bloß :)
 

Marco13

Top Contributor
Ja ok, wobei ich die 3. Option nicht brauch, weil es geht mir ja genau darum, die Dateigroesse unter ne bestimmte Schranke zu bringen.

Von daher bleibt eben die Frage, ob es sinnvoll ist, entweder
1. nur Quali runterzuschrauben
2. nur Auflösung runterzuschrauben
3. Beides runterzuschrauben (und was wieviel..)

Das, was du beschrieben hast, WÄRE genau die 3. Option: Man stellt die maximal zulässige Dateigröße ein (z.B. 200KB) und kann sich dann aussuchen, ob man das durch geringere Qualität oder durch geringere Auflösung erreicht. (Natürlich könnte man theoretisch auch beides runterschrauben, aber dann hätte man ja das Potential verschwendet, was auf Basis der zulässigen Dateigröße noch gegeben gewesen wäre).
 

blz

Bekanntes Mitglied
hm, ja ok.
und wie finde ich raus, was man zu welchem teil macht.
ist die "qualität des anblicks" was objektives, kann man also einen algorithmus entwerfen, der die bildqualität und die auflösung in einem solchen verhältnis runterschraubt, dass das bild beim anschaun noch die maximal mögliche "anschauqualität hat" (begriff im gegensatz zur jpg-qualität gewählt :))

oder ist das was subjektives, wo ich halt bisschen rumprobier und dann halt ungefaehr das nehm was ich denk...?
 

Ark

Top Contributor
Vielleicht ein Tipp: Bei JPEG wird, wenn mich nicht alles täuscht, immer das gesamte Bild in 8x8-Pixel-Blöcke zerlegt (ob du willst oder nicht :D), und diese Blöcke werden dann unabhängig voneinander einer DCT unterzogen usw. Soll heißen:
  • Versuche, Breite und Höhe des Bildes auf ein ganzzahliges Vielfaches von 8 zu bringen (z.B. durch Zuschneiden etc., Seitenverhältnis sollte wohl aber schon beibehalten werden, denke ich).
  • Wähle im Zweifelsfall eine höhere Auflösung mit schlechterer "JPEG-Qualität" als anders herum.
Das nur mal so zur Orientierung. Es kommt natürlich immer auch auf den konkreten Anwendungsfall an.

Ark
 

blz

Bekanntes Mitglied
Ok, falls mal wer vorbeistolpert, dens auch interessiert:

Ich bin fuer mich zu dem Ergebnis gekommen, dass es sich nicht lohnt in die Tiefen der JPG-Komprimierung einzusteigen. Allerdings nur deshalb nicht, weil man schon, wenn man die Auflösung beibehaelt und nur an der Qualität schraubt, sehr gute Ergebnisse erzielt.

Daher habe ich diese Methode geschrieben:

[Java]
/*
* Berechnet die Qualität, mit der ein JPG als BufferedImage unter die Obergrenze maxSize
* komprimiert werden kann, mittels binärer Suche.
*/
public static float computeJPGsizeBinary(BufferedImage img, int maxSize) {

float finalQuality = 1;
float quality = 1;
float quality1 = 1;
float quality2 = 0;

while (true) {
ByteArrayOutputStream out = new ByteArrayOutputStream();

try {
writeImage(img, out, quality);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

if ((out.size() / 1024) > (maxSize + 5)) {
System.out.println("zu groß: " + out.size() / 1024);
quality1 = quality;
quality = quality - (Math.abs(quality2 - quality) / 2);

//Auf Endlosschleife ueberpruefen
if (quality == quality1) {
finalQuality = quality;
System.out.println("Final quality: " + quality);
System.out.println("Final size: " + out.size() / 1024);
break;
}

quality2 = quality1;
System.out.println("--> Neue Quali: " + quality);
} else if ((maxSize - 5) <= (out.size() / 1024) && (out.size() / 1024) <= (maxSize + 5)) {
System.out.println("Final quality: " + quality);
System.out.println("Final size: " + out.size() / 1024);
finalQuality = quality;
break;
} else {
System.out.println("zu klein: " + out.size() / 1024);
quality2 = quality;
quality = Math.abs((quality1 - quality)) / 2 + quality;

//Auf Endlosschleife ueberpruefen
if (quality == quality2) {
finalQuality = quality;
System.out.println("Final quality: " + quality);
System.out.println("Final size: " + out.size() / 1024);
break;
}

quality1 = quality2;
System.out.println("--> new quality: "+ quality);
}
}

return finalQuality;
}
[/Java]

Um das Bild dann tatsaechlich zu schreiben, benutze ich die (glaub ich oben schon mal gepostete) Methode:

[Java]

public static void compressJPG(File input, File output, float quality) throws IOException {

BufferedImage bImg = ImageIO.read(input);


OutputStream os = null;
try
{
os = new FileOutputStream(output);
Iterator<ImageWriter> imageWriters = ImageIO.getImageWritersByFormatName("jpg");
ImageWriter iWriter = imageWriters.next();

ImageOutputStream ioStream = ImageIO.createImageOutputStream(os);
iWriter.setOutput(ioStream);

ImageWriteParam param = iWriter.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(quality);

IIOImage iioImage = new IIOImage(bImg, null, null);

iWriter.write(null, iioImage, param);
}
finally
{
if (os != null)
{
os.close();
}
}
}
[/Java]

Hf!
 

Marco13

Top Contributor
Kannst du in wenigen Worten erklären, wo da der konzeptuelle Unterschied zu der (etwas gewagten und Rechenaufwändigen) binären Suche ist, die ich oben schon gepostet hatte?
 

Ähnliche Java Themen


Oben