Java: Hãy thử lossless và dự phòng để nhận biết nội dung
(Kết quả tốt nhất cho đến nay!)
Khi tôi lần đầu tiên nhìn vào câu hỏi này, tôi đã nghĩ rằng đây không phải là một câu đố hay thử thách chỉ là một người rất cần một chương trình và đó là mã;) Nhưng đó là bản chất của tôi để giải quyết các vấn đề về tầm nhìn nên tôi không thể ngăn mình thử thách này !
Tôi đã đưa ra cách tiếp cận và kết hợp các thuật toán sau đây.
Trong mã giả nó trông như thế này:
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);
Kỹ thuật sử dụng:
- Cường độ thang độ xám
- Sự giãn nở
- Tìm kiếm và loại bỏ cột bằng nhau
- Đường may
- Phát hiện cạnh Sobel
- Ngưỡng
Chương trình
Chương trình có thể cắt ảnh chụp màn hình lossless nhưng có tùy chọn để dự phòng cắt xén nhận thức nội dung mà không phải là 100% lossless. Các đối số của chương trình có thể được điều chỉnh để đạt được kết quả tốt hơn.
Lưu ý: Chương trình có thể được cải thiện theo nhiều cách (Tôi không có nhiều thời gian rảnh!)
Tranh luận
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
Mã
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");
}
}
}
Các kết quả
Ảnh chụp màn hình XP lossless mà không có kích thước mong muốn (Nén tối đa không mất dữ liệu)
Đối số: "image.png" 1 1 5 10 sai 0
Kết quả: 836 x 323
Ảnh chụp màn hình XP lên 800x600
Đối số: "image.png" 800 600 6 10 đúng 60
Kết quả: 800 x 600
Thuật toán lossless loại bỏ khoảng 155 đường ngang so với thuật toán rơi trở lại loại bỏ nhận thức nội dung do đó có thể nhìn thấy một số tạo tác.
Ảnh chụp màn hình Windows 10 lên 700x300
Đối số: "image.png" 700 300 6 10 đúng 60
Kết quả: 700 x 300
Thuật toán lossless loại bỏ 270 đường ngang so với thuật toán rơi trở lại loại bỏ nhận thức nội dung, loại bỏ 29. Chỉ sử dụng thuật toán lossless dọc.
Ảnh chụp màn hình Windows 10 nhận biết nội dung tới 400x200 (kiểm tra)
Đối số: "image.png" 400 200 5 10 đúng 600
Kết quả: 400 x 200
Đây là một thử nghiệm để xem hình ảnh thu được sẽ trông như thế nào sau khi sử dụng nghiêm trọng tính năng nhận biết nội dung. Kết quả là thiệt hại nặng nề nhưng không thể nhận ra.