reducir - Java: cambiar el tamaño de la imagen sin perder calidad
como reducir una imagen en java (7)
Tengo 10.000 fotos que deben cambiar de tamaño, así que tengo un programa Java para hacerlo. Lamentablemente, la calidad de la imagen se pierde poco y no tengo acceso a las imágenes sin comprimir.
import java.awt.Graphics;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
/**
* This class will resize all the images in a given folder
* @author
*
*/
public class JavaImageResizer {
public static void main(String[] args) throws IOException {
File folder = new File("/Users/me/Desktop/images/");
File[] listOfFiles = folder.listFiles();
System.out.println("Total No of Files:"+listOfFiles.length);
BufferedImage img = null;
BufferedImage tempPNG = null;
BufferedImage tempJPG = null;
File newFilePNG = null;
File newFileJPG = null;
for (int i = 0; i < listOfFiles.length; i++) {
if (listOfFiles[i].isFile()) {
System.out.println("File " + listOfFiles[i].getName());
img = ImageIO.read(new File("/Users/me/Desktop/images/"+listOfFiles[i].getName()));
tempJPG = resizeImage(img, img.getWidth(), img.getHeight());
newFileJPG = new File("/Users/me/Desktop/images/"+listOfFiles[i].getName()+"_New");
ImageIO.write(tempJPG, "jpg", newFileJPG);
}
}
System.out.println("DONE");
}
/**
* This function resize the image file and returns the BufferedImage object that can be saved to file system.
*/
public static BufferedImage resizeImage(final Image image, int width, int height) {
int targetw = 0;
int targeth = 75;
if (width > height)targetw = 112;
else targetw = 50;
do {
if (width > targetw) {
width /= 2;
if (width < targetw) width = targetw;
}
if (height > targeth) {
height /= 2;
if (height < targeth) height = targeth;
}
} while (width != targetw || height != targeth);
final BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final Graphics2D graphics2D = bufferedImage.createGraphics();
graphics2D.setComposite(AlphaComposite.Src);
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.drawImage(image, 0, 0, width, height, null);
graphics2D.dispose();
return bufferedImage;
}
Una imagen con la que estoy trabajando es esta:
Este es el cambio de tamaño manual que hice en Microsoft Paint:
y este es el resultado de mi programa [bilineal]:
ACTUALIZACIÓN: Sin diferencia significativa usando BICUBIC
y este es el resultado de mi programa [bicúbico]:
¿Hay alguna forma de aumentar la calidad de la salida del programa para que no tenga que cambiar manualmente el tamaño de todas las fotos?
¡Gracias de antemano!
Dada su imagen de entrada, el método de la respuesta en el primer enlace de los comentarios (felicitaciones a Chris Campbell) produce una de las siguientes miniaturas:
(El otro es la miniatura que creó con MS Paint. Es difícil llamar a uno de ellos "mejor" que el otro ...)
EDITAR: Solo para señalar esto también: el problema principal con su código original era que realmente no escalaba la imagen en varios pasos. Usted acaba de usar un bucle extraño para "calcular" el tamaño del objetivo. El punto clave es que realmente realizas la escala en múltiples pasos.
Para completar, el MVCE
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
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;
import javax.imageio.stream.MemoryCacheImageOutputStream;
public class ResizeQuality
{
public static void main(String[] args) throws IOException
{
BufferedImage image = ImageIO.read(new File("X0aPT.jpg"));
BufferedImage scaled = getScaledInstance(
image, 51, 75, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
writeJPG(scaled, new FileOutputStream("X0aPT_tn.jpg"), 0.85f);
}
public static BufferedImage getScaledInstance(
BufferedImage img, int targetWidth,
int targetHeight, Object hint,
boolean higherQuality)
{
int type =
(img.getTransparency() == Transparency.OPAQUE)
? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
BufferedImage ret = (BufferedImage) img;
int w, h;
if (higherQuality)
{
// Use multi-step technique: start with original size, then
// scale down in multiple passes with drawImage()
// until the target size is reached
w = img.getWidth();
h = img.getHeight();
}
else
{
// Use one-step technique: scale directly from original
// size to target size with a single drawImage() call
w = targetWidth;
h = targetHeight;
}
do
{
if (higherQuality && w > targetWidth)
{
w /= 2;
if (w < targetWidth)
{
w = targetWidth;
}
}
if (higherQuality && h > targetHeight)
{
h /= 2;
if (h < targetHeight)
{
h = targetHeight;
}
}
BufferedImage tmp = new BufferedImage(w, h, type);
Graphics2D g2 = tmp.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
g2.drawImage(ret, 0, 0, w, h, null);
g2.dispose();
ret = tmp;
} while (w != targetWidth || h != targetHeight);
return ret;
}
public static void writeJPG(
BufferedImage bufferedImage,
OutputStream outputStream,
float quality) throws IOException
{
Iterator<ImageWriter> iterator =
ImageIO.getImageWritersByFormatName("jpg");
ImageWriter imageWriter = iterator.next();
ImageWriteParam imageWriteParam = imageWriter.getDefaultWriteParam();
imageWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
imageWriteParam.setCompressionQuality(quality);
ImageOutputStream imageOutputStream =
new MemoryCacheImageOutputStream(outputStream);
imageWriter.setOutput(imageOutputStream);
IIOImage iioimage = new IIOImage(bufferedImage, null, null);
imageWriter.write(null, iioimage, imageWriteParam);
imageOutputStream.flush();
}
}
Debajo están mi propia implementación de Progressive Scaling, sin usar ninguna biblioteca externa. Espero que esto ayude.
private static BufferedImage progressiveScaling(BufferedImage before, Integer longestSideLength) {
if (before != null) {
Integer w = before.getWidth();
Integer h = before.getHeight();
Double ratio = h > w ? longestSideLength.doubleValue() / h : longestSideLength.doubleValue() / w;
//Multi Step Rescale operation
//This technique is describen in Chris Campbell’s blog The Perils of Image.getScaledInstance(). As Chris mentions, when downscaling to something less than factor 0.5, you get the best result by doing multiple downscaling with a minimum factor of 0.5 (in other words: each scaling operation should scale to maximum half the size).
while (ratio < 0.5) {
BufferedImage tmp = scale(before, 0.5);
before = tmp;
w = before.getWidth();
h = before.getHeight();
ratio = h > w ? longestSideLength.doubleValue() / h : longestSideLength.doubleValue() / w;
}
BufferedImage after = scale(before, ratio);
return after;
}
return null;
}
private static BufferedImage scale(BufferedImage imageToScale, Double ratio) {
Integer dWidth = ((Double) (imageToScale.getWidth() * ratio)).intValue();
Integer dHeight = ((Double) (imageToScale.getHeight() * ratio)).intValue();
BufferedImage scaledImage = new BufferedImage(dWidth, dHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2D = scaledImage.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.drawImage(imageToScale, 0, 0, dWidth, dHeight, null);
graphics2D.dispose();
return scaledImage;
}
Desafortunadamente, no se recomienda una escala de fábrica lista para usar que proporcione resultados visualmente buenos. Entre otros, estos son los métodos que recomiendo para escalar:
- Lanczos3 Remuestreo (generalmente visualmente mejor, pero más lento)
- Escala descendente progresiva (por lo general visualmente fina, puede ser bastante rápida)
- Escalado en un paso para escalar (con
Graphics2d
bicúbico rápido y buenos resultados, generalmente no tan bueno como Lanczos3)
Ejemplos de cada método se pueden encontrar en esta respuesta.
Comparación visual
Aquí está su imagen escalada a 96x140
con diferentes métodos / libs. Haga clic en la imagen para obtener el tamaño completo:
- Lanczos3 de Morten Nobel
- Thumbnailator Bilinear Progressive Scaling
- Imgscalr ULTRA_QUALTY (1/7 step Bicubic Progressive Scaling)
- Imgscalr QUALTY (Escalamiento progresivo bicúbico de 1/2 paso)
- Escala progresiva bilineal de Morten Nobel
-
Graphics2d
Interpolación bicúbica - Interpolación del vecino más cercano
Graphics2d
- Photoshop CS5 bicúbico como referencia
Lamentablemente, una sola imagen no es suficiente para juzgar un algoritmo de escala, debe probar los iconos con bordes afilados, fotos con texto, etc.
Muestreo de Lanczos
Se dice que es bueno para subir, y especialmente para reducir la escala. Lamentablemente, no existe una implementación nativa en el JDK actual, por lo que puede implementarlo usted mismo y usar una lib como la lib de Morten Nobel . Un ejemplo simple usando dicha lib:
ResampleOp resizeOp = new ResampleOp(dWidth, dHeight);
resizeOp.setFilter(ResampleFilters.getLanczos3Filter());
BufferedImage scaledImage = resizeOp.filter(imageToScale, null);
La lib está publicada en maven-central, que desafortunadamente no se menciona. La desventaja es que generalmente es muy lento sin ninguna implementación altamente optimizada o acelerada por hardware que yo conozca. La implementación de Nobel es aproximadamente 8 veces más lenta que un algoritmo de escalamiento progresivo de 1/2 paso con Graphics2d
. Lea más sobre esta lib en su blog .
Escalado progresivo
Mencionado en el blog de Chris Campbell sobre escalar en Java, el escalado progresivo es, básicamente, escalar una imagen en pasos más pequeños hasta que se alcanzan las dimensiones finales. Campbell lo describe como la mitad del ancho / alto hasta llegar al objetivo. Esto produce buenos resultados y se puede usar con Graphics2D
que puede ser acelerado por hardware, por lo tanto, generalmente tiene muy buen rendimiento con resultados aceptables en la mayoría de los casos. La principal desventaja de esto es que si se reduce a menos de la mitad utilizando Graphics2D
obtienen los mismos resultados mediocres ya que solo se escala una vez.
Aquí hay un ejemplo simple de cómo funciona:
Las siguientes bibliotecas incorporan formas de escalado progresivo basadas en Graphics2d
:
Thumbnailator v0.4.8
Utiliza el algoritmo bilineal progresivo si el objetivo es al menos la mitad de cada dimensión; de lo contrario, usa una escala bilineal sencilla de Graphics2d
y una escala bicúbica para la ampliación.
Resizer resizer = DefaultResizerFactory.getInstance().getResizer(
new Dimension(imageToScale.getWidth(), imageToScale.getHeight()),
new Dimension(dWidth, dHeight))
BufferedImage scaledImage = new FixedSizeThumbnailMaker(
dWidth, dHeight, false, true).resizer(resizer).make(imageToScale);
Es tan rápido o ligeramente más rápido que la escala de un solo paso con Graphics2d
anotando un promedio de 6.9 segundos en mi benchmark .
Imgscalr v4.2
Utiliza escalado bicúbico progresivo. En la configuración QUALITY
usa el algoritmo de estilo Campbell para reducir a la mitad las dimensiones en cada paso, mientras que ULTRA_QUALITY
tiene pasos más finos, reduciendo el tamaño cada incremento en 1/7, lo que genera imágenes generalmente más suaves pero minimiza las instancias donde solo se usa 1 iteración.
BufferedImage scaledImage = Scalr.resize(imageToScale, Scalr.Method.ULTRA_QUALITY, Scalr.Mode.FIT_EXACT, dWidth, dHeight, bufferedImageOpArray);
La desventaja principal es el rendimiento. ULTRA_QUALITY
es considerablemente más lento que las otras librerías. Incluso la QUALITY
un poco más lenta que la implementación de Thumbnailator. Mi benchmark simple resultó en 26,2 segundos y 11,1 segundos de promedio, respectivamente.
Morten Nobel''s lib v0.8.6
También tiene implementaciones para escalamiento progresivo para todos los Graphics2d
básicos (bilineal, bicúbico y vecino más cercano)
BufferedImage scaledImage = new MultiStepRescaleOp(dWidth, dHeight, RenderingHints.VALUE_INTERPOLATION_BILINEAR).filter(imageToScale, null);
Una palabra sobre JDK Scaling Methods
La forma actual de jdk de escalar una imagen sería algo como esto
scaledImage = new BufferedImage(dWidth, dHeight, imageType);
Graphics2D graphics2D = scaledImage.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.drawImage(imageToScale, 0, 0, dWidth, dHeight, null);
graphics2D.dispose();
pero la mayoría está muy decepcionada con el resultado de la reducción de escala sin importar qué interpolación u otros RenderHints
se usen. Por otro lado, el escalado parece producir imágenes aceptables (lo mejor sería bicúbico). En la versión anterior de JDK (hablamos de 90s v1.1) se introdujo Image.getScaledInstance()
que proporcionó buenos resultados visuales con el parámetro SCALE_AREA_AVERAGING
pero no se recomienda su uso. Lea la explicación completa aquí .
Después de días de investigación, preferiría javaxt.
use La clase javaxt.io.Image
tiene un constructor como:
public Image(java.awt.image.BufferedImage bufferedImage)
para que pueda hacer ( another example
):
javaxt.io.Image image = new javaxt.io.Image(bufferedImage);
image.setWidth(50);
image.setOutputQuality(1);
Aquí está el resultado:
El resultado parece ser mejor (que el resultado de su programa), si aplica desenfoque gaussiano antes de cambiar el tamaño:
Este es el resultado que obtengo, con sigma * (scale factor) = 0.3
:
Con ImageJ el código para hacer esto es bastante corto:
import ij.IJ;
import ij.ImagePlus;
import ij.io.Opener;
import ij.process.ImageProcessor;
public class Resizer {
public static void main(String[] args) {
processPicture("X0aPT.jpg", "output.jpg", 0.0198, ImageProcessor.NONE, 0.3);
}
public static void processPicture(String inputFile, String outputFilePath, double scaleFactor, int interpolationMethod, double sigmaFactor) {
Opener opener = new Opener();
ImageProcessor ip = opener.openImage(inputFile).getProcessor();
ip.blurGaussian(sigmaFactor / scaleFactor);
ip.setInterpolationMethod(interpolationMethod);
ImageProcessor outputProcessor = ip.resize((int)(ip.getWidth() * scaleFactor), (int)(ip.getHeight()*scaleFactor));
IJ.saveAs(new ImagePlus("", outputProcessor), outputFilePath.substring(outputFilePath.lastIndexOf(''.'')+1), outputFilePath);
}
}
Por cierto: solo necesitas ij-1.49d.jar
(o equivalente para otra versión); no hay necesidad de instalar ImageJ.
No debemos olvidar una biblioteca de TwelveMonkeys
Contiene una colección de filtros realmente impresionante.
Ejemplo de uso:
BufferedImage input = ...; // Image to resample
int width, height = ...; // new width/height
BufferedImageOp resampler = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS);
BufferedImage output = resampler.filter(input, null);
Thumbnailator es una biblioteca que fue escrita para crear miniaturas de alta calidad de una manera simple, y hacer una conversión por lotes de imágenes existentes es uno de sus casos de uso.
Realizando cambio de tamaño de lote
Por ejemplo, para adaptar su ejemplo usando Thumbnailator, debe poder lograr resultados similares con el siguiente código:
File folder = new File("/Users/me/Desktop/images/");
Thumbnails.of(folder.listFiles())
.size(112, 75)
.outputFormat("jpg")
.toFiles(Rename.PREFIX_DOT_THUMBNAIL);
Esto continuará y toma todos los archivos en su directorio de images
y procede a procesarlos uno por uno, intente redimensionarlos para que quepan en las dimensiones de 112 x 75 e intentará preservar la relación de aspecto de la imagen original para evitar "deformación" de la imagen.
Thumbnailator continuará y leerá todos los archivos, independientemente de los tipos de imagen (siempre que Java Image IO sea compatible con el formato, Thumbnailator lo procesará), realizará la operación de cambio de tamaño y generará las miniaturas como archivos JPEG, mientras hace tachuelas en una thumbnail.
al comienzo del nombre del archivo.
La siguiente es una ilustración de cómo se usará el nombre de archivo del original en el nombre de archivo de la miniatura si se ejecuta el código anterior.
images/fireworks.jpg -> images/thumbnail.fireworks.jpg
images/illustration.png -> images/thumbnail.illustration.png
images/mountains.jpg -> images/thumbnail.mountains.jpg
Generando miniaturas de alta calidad
En términos de calidad de imagen, como se menciona en la respuesta de Marco13 , la técnica descrita por Chris Campbell en The Perils of Image.getScaledInstance () se implementa en Thumbnailator, lo que resulta en miniaturas de alta calidad sin requerir ningún procesamiento complicado.
La siguiente es la miniatura generada al cambiar el tamaño de la imagen de fuegos artificiales que se muestra en la pregunta original con Thumbnailator:
La imagen de arriba fue creada con el siguiente código:
BufferedImage thumbnail =
Thumbnails.of(new URL("http://i.stack.imgur.com/X0aPT.jpg"))
.height(75)
.asBufferedImage();
ImageIO.write(thumbnail, "png", new File("24745147.png"));
El código muestra que también puede aceptar URL como entrada, y que Thumbnailator también es capaz de crear BufferedImage
también.
Descargo de responsabilidad: soy el mantenedor de la biblioteca Thumbnailator .