Java : 무손실을 시도하고 컨텐츠 인식으로 대체
(지금까지 최고의 무손실 결과!)
내가이 질문을 처음 보았을 때 나는 이것이 퍼즐이나 도전이 아니라고 생각했습니다. 프로그램을 필사적으로 필요로하는 사람이며 코드입니다. !
나는 다음과 같은 접근 방식과 알고리즘 조합을 생각해 냈습니다.
의사 코드에서는 다음과 같습니다.
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);
사용 된 기술 :
- 강도 그레이 스케일
- 팽창
- 동일한 열 검색 및 제거
- 심 조각
- 소벨 에지 감지
- 임계 값
프로그램
이 프로그램은 스크린 샷을 무손실로자를 수 있지만 100 % 무손실이 아닌 컨텐츠 인식 자르기로 대체 할 수있는 옵션이 있습니다. 더 나은 결과를 얻기 위해 프로그램의 주장을 조정할 수 있습니다.
참고 : 프로그램은 여러 가지 방법으로 향상 될 수 있습니다 (나는 여가 시간이별로 없습니다!)
인수
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
암호
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");
}
}
}
결과
원하는 크기없이 무손실 XP 스크린 샷 (최대 무손실 압축)
인수 : "image.png"1 1 5 10 false 0
결과 : 836 x 323
800x600의 XP 스크린 샷
인수 : "image.png"800600 6 10 true 60
결과 : 800 x 600
무손실 알고리즘은 알고리즘이 컨텐츠 인식 제거로 되돌아가는 것보다 약 155 개의 수평선을 제거하여 일부 아티팩트가 보일 수있다.
700x300에 Windows 10 스크린 샷
인수 : "image.png"700300 6 10 true 60
결과 : 700 x 300
무손실 알고리즘은 알고리즘이 다른 29를 제거하는 내용 인식 제거로 넘어가는 것보다 270 개의 수평선을 제거합니다. 수직 무손실 알고리즘 만 사용됩니다.
Windows 10 스크린 샷 컨텐츠 인식 400x200 (테스트)
인수 : "image.png"400200 5 10 true 600
결과 : 400 x 200
콘텐츠 인식 기능을 많이 사용한 후 결과 이미지가 어떻게 보이는지 확인하기위한 테스트였습니다. 결과는 심하게 손상되었지만 인식 할 수 없습니다.