Java: essayez sans perte et faites appel au contenu
(Meilleur résultat sans perte jusqu'à présent!)
Lorsque j’ai examiné cette question pour la première fois, j’ai pensé que ce n’était pas un casse-tête ou un défi, mais bien une personne qui avait désespérément besoin d’un programme et de son code;) Mais c’est dans ma nature de résoudre des problèmes de vision afin que je ne puisse m'empêcher d’essayer ce défi !
Je suis venu avec l'approche suivante et la combinaison d'algorithmes.
En pseudo-code, cela ressemble à ceci:
function crop(image, desired) {
int sizeChange = 1;
while(sizeChange != 0 and image.width > desired){
Look for a repeating and connected set of lines (top to bottom) with a minimum of x lines
Remove all the lines except for one
sizeChange = image.width - newImage.width
image = newImage;
}
if(image.width > desired){
while(image.width > 2 and image.width > desired){
Create a "pixel energy" map of the image
Find the path from the top of the image to the bottom which "costs" the least amount of "energy"
Remove the lowest cost path from the image
image = newImage;
}
}
}
int desiredWidth = ?
int desiredHeight = ?
Image image = input;
crop(image, desiredWidth);
rotate(image, 90);
crop(image, desiredWidth);
rotate(image, -90);
Techniques utilisées:
- Intensité en niveaux de gris
- Dilatation
- Recherche de colonne égale et suppression
- Découpage des coutures
- Détection de bord Sobel
- Seuillage
Le programme
Le programme peut recadrer des captures d'écran sans perte, mais dispose d'une option permettant de recadrer un recadrage en fonction du contenu, qui n'est pas sans perte à 100%. Les arguments du programme peuvent être peaufinés pour obtenir de meilleurs résultats.
Remarque: le programme peut être amélioré de nombreuses manières (je n'ai pas beaucoup de temps libre!)
Arguments
File name = file
Desired width = number > 0
Desired height = number > 0
Min slice width = number > 1
Compare threshold = number > 0
Use content aware = boolean
Max content aware cycles = number >= 0
Code
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
/**
* @author Rolf Smit
* Share and adapt as you like, but don't forget to credit the author!
*/
public class MagicWindowCropper {
public static void main(String[] args) {
if(args.length != 7){
throw new IllegalArgumentException("At least 7 arguments are required: (file, desiredWidth, desiredHeight, minSliceSize, sliceThreshold, forceRemove, maxForceRemove)!");
}
File file = new File(args[0]);
int minSliceSize = Integer.parseInt(args[3]); //4;
int desiredWidth = Integer.parseInt(args[1]); //400;
int desiredHeight = Integer.parseInt(args[2]); //400;
boolean forceRemove = Boolean.parseBoolean(args[5]); //true
int maxForceRemove = Integer.parseInt(args[6]); //40
MagicWindowCropper.MATCH_THRESHOLD = Integer.parseInt(args[4]); //3;
try {
BufferedImage result = ImageIO.read(file);
System.out.println("Horizontal cropping");
//Horizontal crop
result = doDuplicateColumnsMagic(result, minSliceSize, desiredWidth);
if (result.getWidth() != desiredWidth && forceRemove) {
result = doSeamCarvingMagic(result, maxForceRemove, desiredWidth);
}
result = getRotatedBufferedImage(result, false);
System.out.println("Vertical cropping");
//Vertical crop
result = doDuplicateColumnsMagic(result, minSliceSize, desiredHeight);
if (result.getWidth() != desiredHeight && forceRemove) {
result = doSeamCarvingMagic(result, maxForceRemove, desiredHeight);
}
result = getRotatedBufferedImage(result, true);
showBufferedImage("Result", result);
ImageIO.write(result, "png", getNewFileName(file));
} catch (IOException e) {
e.printStackTrace();
}
}
private static BufferedImage doSeamCarvingMagic(BufferedImage inputImage, int max, int desired) {
System.out.println("Seam Carving magic:");
int maxChange = Math.min(inputImage.getWidth() - desired, max);
BufferedImage last = inputImage;
int total = 0, change;
do {
int[][] energy = getPixelEnergyImage(last);
BufferedImage out = removeLowestSeam(energy, last);
change = last.getWidth() - out.getWidth();
total += change;
System.out.println("Carves removed: " + total);
last = out;
} while (change != 0 && total < maxChange);
return last;
}
private static BufferedImage doDuplicateColumnsMagic(BufferedImage inputImage, int minSliceWidth, int desired) {
System.out.println("Duplicate columns magic:");
int maxChange = inputImage.getWidth() - desired;
BufferedImage last = inputImage;
int total = 0, change;
do {
BufferedImage out = removeDuplicateColumn(last, minSliceWidth, desired);
change = last.getWidth() - out.getWidth();
total += change;
System.out.println("Columns removed: " + total);
last = out;
} while (change != 0 && total < maxChange);
return last;
}
/*
* Duplicate column methods
*/
private static BufferedImage removeDuplicateColumn(BufferedImage inputImage, int minSliceWidth, int desiredWidth) {
if (inputImage.getWidth() <= minSliceWidth) {
throw new IllegalStateException("The image width is smaller than the minSliceWidth! What on earth are you trying to do?!");
}
int[] stamp = null;
int sliceStart = -1, sliceEnd = -1;
for (int x = 0; x < inputImage.getWidth() - minSliceWidth + 1; x++) {
stamp = getHorizontalSliceStamp(inputImage, x, minSliceWidth);
if (stamp != null) {
sliceStart = x;
sliceEnd = x + minSliceWidth - 1;
break;
}
}
if (stamp == null) {
return inputImage;
}
BufferedImage out = deepCopyImage(inputImage);
for (int x = sliceEnd + 1; x < inputImage.getWidth(); x++) {
int[] row = getHorizontalSliceStamp(inputImage, x, 1);
if (equalsRows(stamp, row)) {
sliceEnd = x;
} else {
break;
}
}
//Remove policy
int canRemove = sliceEnd - (sliceStart + 1) + 1;
int mayRemove = inputImage.getWidth() - desiredWidth;
int dif = mayRemove - canRemove;
if (dif < 0) {
sliceEnd += dif;
}
int mustRemove = sliceEnd - (sliceStart + 1) + 1;
if (mustRemove <= 0) {
return out;
}
out = removeHorizontalRegion(out, sliceStart + 1, sliceEnd);
out = removeLeft(out, out.getWidth() - mustRemove);
return out;
}
private static BufferedImage removeHorizontalRegion(BufferedImage image, int startX, int endX) {
int width = endX - startX + 1;
if (endX + 1 > image.getWidth()) {
endX = image.getWidth() - 1;
}
if (endX < startX) {
throw new IllegalStateException("Invalid removal parameters! Wow this error message is genius!");
}
BufferedImage out = deepCopyImage(image);
for (int x = endX + 1; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
out.setRGB(x - width, y, image.getRGB(x, y));
out.setRGB(x, y, 0xFF000000);
}
}
return out;
}
private static int[] getHorizontalSliceStamp(BufferedImage inputImage, int startX, int sliceWidth) {
int[] initial = new int[inputImage.getHeight()];
for (int y = 0; y < inputImage.getHeight(); y++) {
initial[y] = inputImage.getRGB(startX, y);
}
if (sliceWidth == 1) {
return initial;
}
for (int s = 1; s < sliceWidth; s++) {
int[] row = new int[inputImage.getHeight()];
for (int y = 0; y < inputImage.getHeight(); y++) {
row[y] = inputImage.getRGB(startX + s, y);
}
if (!equalsRows(initial, row)) {
return null;
}
}
return initial;
}
private static int MATCH_THRESHOLD = 3;
private static boolean equalsRows(int[] left, int[] right) {
for (int i = 0; i < left.length; i++) {
int rl = (left[i]) & 0xFF;
int gl = (left[i] >> 8) & 0xFF;
int bl = (left[i] >> 16) & 0xFF;
int rr = (right[i]) & 0xFF;
int gr = (right[i] >> 8) & 0xFF;
int br = (right[i] >> 16) & 0xFF;
if (Math.abs(rl - rr) > MATCH_THRESHOLD
|| Math.abs(gl - gr) > MATCH_THRESHOLD
|| Math.abs(bl - br) > MATCH_THRESHOLD) {
return false;
}
}
return true;
}
/*
* Seam carving methods
*/
private static BufferedImage removeLowestSeam(int[][] input, BufferedImage image) {
int lowestValue = Integer.MAX_VALUE; //Integer overflow possible when image height grows!
int lowestValueX = -1;
// Here be dragons
for (int x = 1; x < input.length - 1; x++) {
int seamX = x;
int value = input[x][0];
for (int y = 1; y < input[x].length; y++) {
if (seamX < 1) {
int top = input[seamX][y];
int right = input[seamX + 1][y];
if (top <= right) {
value += top;
} else {
seamX++;
value += right;
}
} else if (seamX > input.length - 2) {
int top = input[seamX][y];
int left = input[seamX - 1][y];
if (top <= left) {
value += top;
} else {
seamX--;
value += left;
}
} else {
int left = input[seamX - 1][y];
int top = input[seamX][y];
int right = input[seamX + 1][y];
if (top <= left && top <= right) {
value += top;
} else if (left <= top && left <= right) {
seamX--;
value += left;
} else {
seamX++;
value += right;
}
}
}
if (value < lowestValue) {
lowestValue = value;
lowestValueX = x;
}
}
BufferedImage out = deepCopyImage(image);
int seamX = lowestValueX;
shiftRow(out, seamX, 0);
for (int y = 1; y < input[seamX].length; y++) {
if (seamX < 1) {
int top = input[seamX][y];
int right = input[seamX + 1][y];
if (top <= right) {
shiftRow(out, seamX, y);
} else {
seamX++;
shiftRow(out, seamX, y);
}
} else if (seamX > input.length - 2) {
int top = input[seamX][y];
int left = input[seamX - 1][y];
if (top <= left) {
shiftRow(out, seamX, y);
} else {
seamX--;
shiftRow(out, seamX, y);
}
} else {
int left = input[seamX - 1][y];
int top = input[seamX][y];
int right = input[seamX + 1][y];
if (top <= left && top <= right) {
shiftRow(out, seamX, y);
} else if (left <= top && left <= right) {
seamX--;
shiftRow(out, seamX, y);
} else {
seamX++;
shiftRow(out, seamX, y);
}
}
}
return removeLeft(out, out.getWidth() - 1);
}
private static void shiftRow(BufferedImage image, int startX, int y) {
for (int x = startX; x < image.getWidth() - 1; x++) {
image.setRGB(x, y, image.getRGB(x + 1, y));
}
}
private static int[][] getPixelEnergyImage(BufferedImage image) {
// Convert Image to gray scale using the luminosity method and add extra
// edges for the Sobel filter
int[][] grayScale = new int[image.getWidth() + 2][image.getHeight() + 2];
for (int x = 0; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
int rgb = image.getRGB(x, y);
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = (rgb & 0xFF);
int luminosity = (int) (0.21 * r + 0.72 * g + 0.07 * b);
grayScale[x + 1][y + 1] = luminosity;
}
}
// Sobel edge detection
final double[] kernelHorizontalEdges = new double[] { 1, 2, 1, 0, 0, 0, -1, -2, -1 };
final double[] kernelVerticalEdges = new double[] { 1, 0, -1, 2, 0, -2, 1, 0, -1 };
int[][] energyImage = new int[image.getWidth()][image.getHeight()];
for (int x = 1; x < image.getWidth() + 1; x++) {
for (int y = 1; y < image.getHeight() + 1; y++) {
int k = 0;
double horizontal = 0;
for (int ky = -1; ky < 2; ky++) {
for (int kx = -1; kx < 2; kx++) {
horizontal += ((double) grayScale[x + kx][y + ky] * kernelHorizontalEdges[k]);
k++;
}
}
double vertical = 0;
k = 0;
for (int ky = -1; ky < 2; ky++) {
for (int kx = -1; kx < 2; kx++) {
vertical += ((double) grayScale[x + kx][y + ky] * kernelVerticalEdges[k]);
k++;
}
}
if (Math.sqrt(horizontal * horizontal + vertical * vertical) > 127) {
energyImage[x - 1][y - 1] = 255;
} else {
energyImage[x - 1][y - 1] = 0;
}
}
}
//Dilate the edge detected image a few times for better seaming results
//Current value is just 1...
for (int i = 0; i < 1; i++) {
dilateImage(energyImage);
}
return energyImage;
}
private static void dilateImage(int[][] image) {
for (int x = 0; x < image.length; x++) {
for (int y = 0; y < image[x].length; y++) {
if (image[x][y] == 255) {
if (x > 0 && image[x - 1][y] == 0) {
image[x - 1][y] = 2; //Note: 2 is just a placeholder value
}
if (y > 0 && image[x][y - 1] == 0) {
image[x][y - 1] = 2;
}
if (x + 1 < image.length && image[x + 1][y] == 0) {
image[x + 1][y] = 2;
}
if (y + 1 < image[x].length && image[x][y + 1] == 0) {
image[x][y + 1] = 2;
}
}
}
}
for (int x = 0; x < image.length; x++) {
for (int y = 0; y < image[x].length; y++) {
if (image[x][y] == 2) {
image[x][y] = 255;
}
}
}
}
/*
* Utilities
*/
private static void showBufferedImage(String windowTitle, BufferedImage image) {
JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(image)), windowTitle, JOptionPane.PLAIN_MESSAGE, null);
}
private static BufferedImage deepCopyImage(BufferedImage input) {
ColorModel cm = input.getColorModel();
return new BufferedImage(cm, input.copyData(null), cm.isAlphaPremultiplied(), null);
}
private static final BufferedImage getRotatedBufferedImage(BufferedImage img, boolean back) {
double oldW = img.getWidth(), oldH = img.getHeight();
double newW = img.getHeight(), newH = img.getWidth();
BufferedImage out = new BufferedImage((int) newW, (int) newH, img.getType());
Graphics2D g = out.createGraphics();
g.translate((newW - oldW) / 2.0, (newH - oldH) / 2.0);
g.rotate(Math.toRadians(back ? -90 : 90), oldW / 2.0, oldH / 2.0);
g.drawRenderedImage(img, null);
g.dispose();
return out;
}
private static BufferedImage removeLeft(BufferedImage image, int startX) {
int removeWidth = image.getWidth() - startX;
BufferedImage out = new BufferedImage(image.getWidth() - removeWidth,
image.getHeight(), image.getType());
for (int x = 0; x < startX; x++) {
for (int y = 0; y < out.getHeight(); y++) {
out.setRGB(x, y, image.getRGB(x, y));
}
}
return out;
}
private static File getNewFileName(File in) {
String name = in.getName();
int i = name.lastIndexOf(".");
if (i != -1) {
String ext = name.substring(i);
String n = name.substring(0, i);
return new File(in.getParentFile(), n + "-cropped" + ext);
} else {
return new File(in.getParentFile(), name + "-cropped");
}
}
}
Résultats
Capture d'écran XP sans perte sans la taille souhaitée (compression maximale sans perte)
Arguments: "image.png" 1 1 5 10 false 0
Résultat: 836 x 323
XP capture d'écran à 800x600
Arguments: "image.png" 800 600 6 10 true 60
Résultat: 800 x 600
L'algorithme sans perte supprime environ 155 lignes horizontales et ne prend plus en compte le contenu, ce qui permet de voir certains artefacts.
Windows 10 capture d'écran à 700x300
Arguments: "image.png" 700 300 6 10 true 60
Résultat: 700 x 300
L'algorithme sans perte supprime 270 lignes horizontales, puis retombe en suppression basée sur le contenu, ce qui en supprime 29 autres. Verticalement, seul l'algorithme sans perte est utilisé.
Capture d'écran compatible avec le contenu de Windows 10 à 400x200 (test)
Arguments: "image.png" 400 200 5 10 true 600
Résultat: 400 x 200
Il s'agissait d'un test permettant de voir à quoi ressemblerait l'image résultante après une utilisation intensive de la fonctionnalité prenant en compte le contenu. Le résultat est lourdement endommagé mais pas méconnaissable.