Java: Coba lossless dan mundur ke sadar konten
(Hasil lossless terbaik sejauh ini!)
Ketika saya pertama kali melihat pertanyaan ini, saya pikir ini bukan teka-teki atau tantangan, hanya seseorang yang sangat membutuhkan program dan kode itu;) Tetapi sudah menjadi sifat saya untuk menyelesaikan masalah penglihatan sehingga saya tidak dapat menghentikan diri saya untuk mencoba tantangan ini. !
Saya datang dengan pendekatan dan kombinasi algoritma berikut.
Dalam pseudo-code tampilannya seperti ini:
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);
Teknik yang digunakan:
- Skala abu-abu intensitas
- Pelebaran
- Cari kolom yang sama dan hapus
- Ukiran jahitan
- Deteksi tepi sobel
- Ambang batas
Program
Program ini dapat memotong tangkapan layar lossless tetapi memiliki opsi untuk mundur ke pemotongan konten-sadar yang tidak 100% lossless. Argumen dari program ini dapat disesuaikan untuk mencapai hasil yang lebih baik.
Catatan: Program ini dapat ditingkatkan dengan banyak cara (saya tidak punya banyak waktu luang!)
Argumen
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
Kode
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");
}
}
}
Hasil
Tangkapan layar XP lossless tanpa ukuran yang diinginkan (Max lossless compression)
Argumen: "image.png" 1 1 5 10 false 0
Hasil: 836 x 323
Tangkapan layar XP hingga 800x600
Argumen: "image.png" 800 600 6 10 true 60
Hasil: 800 x 600
Algoritma lossless menghilangkan sekitar 155 garis horizontal daripada algoritma kembali ke penghapusan konten-sadar sehingga beberapa artefak dapat dilihat.
Tangkapan layar Windows 10 hingga 700x300
Argumen: "image.png" 700 300 6 10 true 60
Hasil: 700 x 300
Algoritma lossless menghilangkan 270 garis horizontal daripada algoritma kembali ke penghapusan konten-sadar yang menghilangkan 29 lainnya. Vertikal hanya algoritma lossless yang digunakan.
Screenshot Windows 10 menyadari konten hingga 400x200 (uji)
Argumen: "image.png" 400 200 5 10 true 600
Hasil: 400 x 200
Ini adalah tes untuk melihat bagaimana gambar yang dihasilkan akan terlihat setelah penggunaan fitur sadar konten yang parah. Hasilnya sangat rusak tetapi tidak bisa dikenali.