Java: Kayıpsız ve içeriğe duyarlı geri dönüşü deneyin
(Şu ana kadar en iyi kayıpsız sonuç!)
Bu soruya ilk baktığımda, bunun bir bilmeceye ya da çaresizce programa ihtiyaç duyan birine meydan okuma olmadığını düşündüm;) !
Aşağıdaki yaklaşım ve algoritmaların kombinasyonu ile geldi.
Sahte kodda şöyle görünür:
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);
Kullanılan teknikler:
- Yoğunluk gri tonlamalı
- genişleme
- Eşit sütun arama ve kaldırma
- Derz oyma
- Sobel kenarı tespiti
- Eşik
Program
Program, ekran görüntülerini kayıpsız kırpabilir ancak% 100 kayıpsız olmayan içeriğe duyarlı kırpmaya geri dönme seçeneğine sahiptir. Programın argümanları daha iyi sonuçlar elde etmek için düzeltilebilir.
Not: Program birçok yönden geliştirilebilir (Çok fazla boş vaktim yok!)
Argümanlar
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
kod
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");
}
}
}
Sonuçlar
XP ekran görüntüsü istediğiniz boyutta kayıpsız (Maks kayıpsız sıkıştırma)
Bağımsız Değişkenler: "image.png" 1 1 5 10 false 0
Sonuç: 836 x 323
XP ekran görüntüsünden 800x600'e
Bağımsız Değişkenler: "image.png" 800 600 6 10 true 60
Sonuç: 800 x 600
Kayıpsız algoritma, algoritmanın içeriğe duyarlı kaldırmaya geri döndüğünden, bazı eserler görülebilmesi için yaklaşık 155 yatay çizgiyi kaldırır.
Windows 10 ekran görüntüsü ila 700x300
Bağımsız Değişkenler: "image.png" 700 300 6 10 true 60
Sonuç: 700 x 300
Kayıpsız algoritma, algoritmadan daha 270 yatay çizgiyi kaldırır, başka bir 29'u kaldıran içeriğe duyarlı kaldırmaya geri döner. Dikey yalnızca kayıpsız algoritma kullanılır.
Windows 10 ekran görüntüsü içeriği 400x200 (farkında) olan
Bağımsız Değişkenler: "image.png" 400 200 5 10 true 600
Sonuç: 400 x 200
Bu, sonuçtaki görüntünün, içeriğe duyarlı özelliğin şiddetli bir şekilde kullanımından sonra nasıl görüneceğini görmek için yapılan bir testti. Sonuç ağır hasar görmüştür ancak tanınmamaktadır.