때로는 무손실 스크린 샷 Resizer가 필요합니다


44

때로는 코드의 주석보다 더 많은 문서를 작성해야합니다. 때로는 이러한 설명에 스크린 샷이 필요합니다. 때때로 그러한 스크린 샷을 얻는 조건이 너무 이상하여 개발자에게 스크린 샷을 찍도록 요청합니다. 때때로 스크린 샷이 내 사양에 맞지 않아서보기 좋게 보이도록 크기를 조정해야합니다.

보다시피, "손실없는 스크린 샷 리사이 저"마법이 필요한 상황은 거의 없습니다. 어쨌든, 나는 매일 필요로하는 것 같습니다. 그러나 아직 존재하지 않습니다.

PCG에서 당신이 전에 멋진 그래픽 퍼즐을 해결하는 것을 보았습니다 . 그래서 이것은 당신에게 다소 지루하다고 생각합니다 ...

사양

  • 프로그램은 단일 창의 스크린 샷을 입력으로 사용합니다.
  • 스크린 샷은 유리 효과 또는 이와 유사한 것을 사용하지 않습니다 (따라서 빛나는 배경을 다룰 필요가 없습니다)
  • 입력 파일 형식은 PNG (또는 압축 아티팩트를 처리 할 필요가없는 다른 무손실 형식)입니다.
  • 출력 파일 형식은 입력 파일 형식과 동일합니다
  • 프로그램은 다른 크기의 스크린 샷을 출력으로 만듭니다. 최소 요구 사항이 축소되고 있습니다.
  • 사용자는 예상되는 출력 크기를 지정해야합니다. 주어진 입력에서 프로그램이 생성 할 수있는 최소 크기에 대한 힌트를 줄 수 있다면 도움이됩니다.
  • 사람이 해석하는 경우 출력 스크린 샷에 정보가 부족해서는 안됩니다. 텍스트 나 이미지 내용은 제거 할 수 없지만 배경이있는 영역 만 제거해야합니다. 아래 예를 참조하십시오.
  • 예상 크기를 얻을 수없는 경우 프로그램은이를 표시하고 추가 통지없이 단순히 정보를 충돌 시키거나 제거하지 않아야합니다.
  • 프로그램이 검증을 위해 제거 될 영역을 표시하면 인기가 높아질 것입니다.
  • 프로그램은 예를 들어 최적화의 시작점을 식별하기 위해 다른 사용자 입력이 필요할 수 있습니다.

규칙

이것은 인기 콘테스트입니다. 2015-03-08의 대부분의 투표에 대한 답변이 수락됩니다.

Windows XP 스크린 샷. 원본 크기 : 1003x685 픽셀.

XP 스크린 샷 큰

정보 (텍스트 또는 이미지)를 잃지 않고 제거 할 수있는 예제 영역 (빨간색 : 세로, 노랑 : 가로) 빨간색 막대는 연속적이지 않습니다. 이 예는 잠재적으로 제거 될 수있는 모든 가능한 픽셀을 나타내지는 않습니다.

XP 스크린 샷 제거 표시기

무손실 크기 : 783x424 픽셀.

XP 스크린 샷 작은

Windows 10 스크린 샷. 원본 크기 : 999x593 픽셀

Windows 10 스크린 샷 큰

제거 할 수있는 영역 예.

Windows 10 스크린 샷 제거 표시

무손실 크기의 스크린 샷 : 689x320 픽셀.

제목 텍스트 ( "다운로드")와 "이 폴더가 비어 있습니다"가 더 이상 가운데에 있지 않은 것이 좋습니다. 물론 중심에 있으면 더 좋을 것이고 솔루션이 제공하면 더 인기가 있어야합니다.

작은 Windows 10 스크린 샷


3
Photoshop의 " 컨텐츠 인식 스케일링 "기능을 상기시킵니다.
agtoever

입력 형식은 무엇입니까? 표준 이미지 형식을 선택할 수 있습니까?
HEGX64

@ThomasW는 "이것은 다소 지루하다고 생각한다"고 말했다. 사실이 아니다. 이것은 악마입니다.
논리 기사

1
이 질문은 충분히 주목받지 못합니다. 첫 번째 답변은 오랫동안 유일하게 답변되었으므로 찬성되었습니다. 현재 투표 수는 다른 답변의 인기를 나타내는 데 충분하지 않습니다. 문제는 더 많은 사람들이 투표 할 수있는 방법입니다. 심지어 대답에 투표했습니다.
Rolf ツ

1
@Rolf ツ : 지금까지이 질문에서 얻은 명성의 2/3에 해당하는 현상금을 시작했습니다. 나는 그것이 충분히 공정하기를 바랍니다.
토머스 웰러

답변:


29

파이썬

이 함수는 delrows하나의 중복 행을 제외한 모든 행을 삭제하고 조옮김 된 이미지를 반환하고, 두 번 적용하면 열도 삭제하고 다시 조 옮깁니다. 또한 threshold두 줄이 여전히 동일하게 간주 될 수있는 픽셀 수를 제어합니다.

from scipy import misc
from pylab import *

im7 = misc.imread('win7.png')
im8 = misc.imread('win8.png')

def delrows(im, threshold=0):
    d = diff(im, axis=0)
    mask = where(sum((d!=0), axis=(1,2))>threshold)
    return transpose(im[mask], (1,0,2))

imsave('crop7.png', delrows(delrows(im7)))
imsave('crop8.png', delrows(delrows(im8)))

여기에 이미지 설명을 입력하십시오
여기에 이미지 설명을 입력하십시오

비교기를 mask시작 >하여 <=대신 빈 공간 인 제거 된 영역을 출력합니다.

여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오

golfed (이유가 아니기 때문에)
각 픽셀을 비교하는 대신 합계 만 보지만 부작용으로 스크린 샷을 그레이 스케일로 변환하고 Win8의 주소 표시 줄에있는 아래쪽 화살표와 같은 합계 보존 순열에 문제가 있습니다 스크린 샷

from scipy import misc
from pylab import*
f=lambda M:M[where(diff(sum(M,1)))].T
imsave('out.png', f(f(misc.imread('in.png',1))),cmap='gray')

여기에 이미지 설명을 입력하십시오
여기에 이미지 설명을 입력하십시오


와우, 심지어 golfed ... (난 당신이이 인기 투표는 것을 알고 있었다 희망)
토마스 웰러

골프 점수를 제거 하시겠습니까? 이것은 사람들이 이것이 코드 골프라고 생각하게 할 수 있습니다. 감사합니다.
Thomas Weller

1
@ThomasW. 점수를 제거하고 눈에 띄지 않게 바닥으로 옮겼습니다.
DenDenDo

15

Java : 무손실을 시도하고 컨텐츠 인식으로 대체

(지금까지 최고의 무손실 결과!)

원하는 크기가없는 XP 스크린 샷

내가이 질문을 처음 보았을 때 나는 이것이 퍼즐이나 도전이 아니라고 생각했습니다. 프로그램을 필사적으로 필요로하는 사람이며 코드입니다. !

나는 다음과 같은 접근 방식과 알고리즘 조합을 생각해 냈습니다.

의사 코드에서는 다음과 같습니다.

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

원하는 크기가없는 XP 스크린 샷


800x600의 XP 스크린 샷

인수 : "image.png"800600 6 10 true 60

결과 : 800 x 600

무손실 알고리즘은 알고리즘이 컨텐츠 인식 제거로 되돌아가는 것보다 약 155 개의 수평선을 제거하여 일부 아티팩트가 보일 수있다.

800x600으로 XP 스크린 샷


700x300에 Windows 10 스크린 샷

인수 : "image.png"700300 6 10 true 60

결과 : 700 x 300

무손실 알고리즘은 알고리즘이 다른 29를 제거하는 내용 인식 제거로 넘어가는 것보다 270 개의 수평선을 제거합니다. 수직 무손실 알고리즘 만 사용됩니다.

700x300에 Windows 10 스크린 샷


Windows 10 스크린 샷 컨텐츠 인식 400x200 (테스트)

인수 : "image.png"400200 5 10 true 600

결과 : 400 x 200

콘텐츠 인식 기능을 많이 사용한 후 결과 이미지가 어떻게 보이는지 확인하기위한 테스트였습니다. 결과는 심하게 손상되었지만 인식 할 수 없습니다.

Windows 10 스크린 샷 컨텐츠 인식 400x200 (테스트)



첫 번째 출력이 완전히 트리밍되지 않았습니다. 너무 많이 잘릴 수 있습니다
Optimizer

그것은 내 프로그램의 주장이 800 픽셀 이상으로 최적화해서는 안된다고
Rolf ツ

이 popcon 이후로, 당신은 아마 최상의 결과를 보여 주어야 할 것입니다 :)
Optimizer

내 프로그램은 다른 답변과 초기에 동일하지만 더 많은 다운 스케일링을위한 내용 인식 기능이 있습니다. 또한 원하는 너비와 높이로 자르는 옵션도 있습니다 (질문 참조).
Rolf ツ

3

C #, 알고리즘은 수동으로 수행합니다.

이것은 첫 번째 이미지 처리 프로그램이며 모든 LockBits것들 등 으로 구현하는 데 시간이 걸렸습니다 . 그러나 Parallel.For거의 즉각적인 피드백을 얻기 위해 (을 사용하여 ) 신속하게 처리하기를 원했습니다 .

기본적으로 내 알고리즘은 스크린 샷에서 수동으로 픽셀을 제거하는 방법에 대한 관찰을 기반으로합니다.

  • 사용하지 않는 픽셀이있을 가능성이 높기 때문에 오른쪽 가장자리에서 시작합니다.
  • 시스템 버튼을 올바르게 캡처하기 위해 가장자리 감지에 대한 임계 값을 정의합니다. Windows 10 스크린 샷의 경우 48 픽셀의 임계 값이 적합합니다.
  • 가장자리가 감지 된 후 (아래 빨간색으로 표시) 동일한 색상의 픽셀을 찾고 있습니다. 발견 된 최소 픽셀 수를 가져 와서 모든 행에 적용합니다 (보라색으로 표시).
  • 그런 다음 가장자리 감지 (빨간색 표시), 같은 색상의 픽셀 (파란색, 녹색, 노란색 표시) 등으로 다시 시작합니다.

현재는 수평으로 만 수행합니다. 수직 결과는 동일한 알고리즘을 사용하고 90 ° 회전 된 이미지에서 작동하므로 이론적으로 가능합니다.

결과

이것은 감지 된 영역이있는 내 응용 프로그램의 스크린 샷입니다.

무손실 스크린 샷 Resizer

그리고 이것은 Windows 10 스크린 샷 및 48 픽셀 임계 값의 결과입니다. 출력 너비는 681 픽셀입니다. 불행히도 완벽하지는 않습니다 ( "검색 다운로드"및 일부 세로 막대 표시 줄 참조).

Windows 10 결과, 48 픽셀 임계 값

그리고 64 픽셀 임계 값 (폭 567 픽셀)을 가진 또 하나. 이것은 더 좋아 보인다.

Windows 10 결과, 64 픽셀 임계 값

모든 바닥에서 자르기에도 회전을 적용한 전체 결과 (567x304 픽셀).

Windows 10 결과, 64 픽셀 임계 값, 회전

Windows XP의 경우 픽셀이 정확히 같지 않기 때문에 코드를 약간 변경해야했습니다. 유사성 임계 값 8 (RGB 값의 차이)을 적용하고 있습니다. 열의 일부 아티팩트에 유의하십시오.

Windows XP 스크린 샷이있는 Lossless Screenshot Resizer

Windows XP 결과

암호

글쎄, 이미지 처리에 대한 나의 첫 번째 시도. 잘 안보이나요? 여기에는 UI가 아닌 핵심 알고리즘 만 나열되고 90 ° 회전은 표시되지 않습니다.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;

namespace LosslessScreenshotResizer.BL
{
    internal class PixelAreaSearcher
    {
        private readonly Bitmap _originalImage;

        private readonly int _edgeThreshold;
        readonly Color _edgeColor = Color.FromArgb(128, 255, 0, 0);
        readonly Color[] _iterationIndicatorColors =
        {
            Color.FromArgb(128, 0, 0, 255), 
            Color.FromArgb(128, 0, 255, 255), 
            Color.FromArgb(128, 0, 255, 0),
            Color.FromArgb(128, 255, 255, 0)
        };

        public PixelAreaSearcher(Bitmap originalImage, int edgeThreshold)
        {
            _originalImage = originalImage;
            _edgeThreshold = edgeThreshold;

            // cache width and height. Also need to do that because of some GDI exceptions during LockBits
            _imageWidth = _originalImage.Width;
            _imageHeight = _originalImage.Height;            
        }

        public Bitmap SearchHorizontal()
        {
            return Search();
        }

        /// <summary>
        /// Find areas of pixels to keep and to remove. You can get that information via <see cref="PixelAreas"/>.
        /// The result of this operation is a bitmap of the original picture with an overlay of the areas found.
        /// </summary>
        /// <returns></returns>
        private unsafe Bitmap Search()
        {
            // FastBitmap is a wrapper around Bitmap with LockBits enabled for fast operation.
            var input = new FastBitmap(_originalImage);
            // transparent overlay
            var overlay = new FastBitmap(_originalImage.Width, _originalImage.Height);

            _pixelAreas = new List<PixelArea>(); // save the raw data for later so that the image can be cropped
            int startCoordinate = _imageWidth - 1; // start at the right edge
            int iteration = 0; // remember the iteration to apply different colors
            int minimum;
            do
            {
                var indicatorColor = GetIterationColor(iteration);

                // Detect the edge which is not removable
                var edgeStartCoordinates = new PixelArea(_imageHeight) {AreaType = AreaType.Keep};
                Parallel.For(0, _imageHeight, y =>
                {
                    edgeStartCoordinates[y] = DetectEdge(input, y, overlay, _edgeColor, startCoordinate);
                }
                    );
                _pixelAreas.Add(edgeStartCoordinates);

                // Calculate how many pixels can theoretically be removed per line
                var removable = new PixelArea(_imageHeight) {AreaType = AreaType.Dummy};
                Parallel.For(0, _imageHeight, y =>
                {
                    removable[y] = CountRemovablePixels(input, y, edgeStartCoordinates[y]);
                }
                    );

                // Calculate the practical limit
                // We can only remove the same amount of pixels per line, otherwise we get a non-rectangular image
                minimum = removable.Minimum;
                Debug.WriteLine("Can remove {0} pixels", minimum);

                // Apply the practical limit: calculate the start coordinates of removable areas
                var removeStartCoordinates = new PixelArea(_imageHeight) { AreaType = AreaType.Remove };
                removeStartCoordinates.Width = minimum;
                for (int y = 0; y < _imageHeight; y++) removeStartCoordinates[y] = edgeStartCoordinates[y] - minimum;
                _pixelAreas.Add(removeStartCoordinates);

                // Paint the practical limit onto the overlay for demo purposes
                Parallel.For(0, _imageHeight, y =>
                {
                    PaintRemovableArea(y, overlay, indicatorColor, minimum, removeStartCoordinates[y]);
                }
                    );

                // Move the left edge before starting over
                startCoordinate = removeStartCoordinates.Minimum;
                var remaining = new PixelArea(_imageHeight) { AreaType = AreaType.Keep };
                for (int y = 0; y < _imageHeight; y++) remaining[y] = startCoordinate;
                _pixelAreas.Add(remaining);

                iteration++;
            } while (minimum > 1);


            input.GetBitmap(); // TODO HACK: release Lockbits on the original image 
            return overlay.GetBitmap();
        }

        private Color GetIterationColor(int iteration)
        {
            return _iterationIndicatorColors[iteration%_iterationIndicatorColors.Count()];
        }

        /// <summary>
        /// Find a minimum number of contiguous pixels from the right side of the image. Everything behind that is an edge.
        /// </summary>
        /// <param name="input">Input image to get pixel data from</param>
        /// <param name="y">The row to be analyzed</param>
        /// <param name="output">Output overlay image to draw the edge on</param>
        /// <param name="edgeColor">Color for drawing the edge</param>
        /// <param name="startCoordinate">Start coordinate, defining the maximum X</param>
        /// <returns>X coordinate where the edge starts</returns>
        private int DetectEdge(FastBitmap input, int y, FastBitmap output, Color edgeColor, int startCoordinate)
        {
            var repeatCount = 0;
            var lastColor = Color.DodgerBlue;
            int x;

            for (x = startCoordinate; x >= 0; x--)
            {
                var currentColor = input.GetPixel(x, y);
                if (almostEquals(lastColor,currentColor))
                {
                    repeatCount++;
                }
                else
                {
                    lastColor = currentColor;
                    repeatCount = 0;
                    for (int i = x; i < startCoordinate; i++)
                    {
                        output.SetPixel(i,y,edgeColor);
                    }
                }

                if (repeatCount > _edgeThreshold)
                {
                    return x + _edgeThreshold;
                }
            }
            return repeatCount;
        }

        /// <summary>
        /// Counts the number of contiguous pixels in a row, starting on the right and going to the left
        /// </summary>
        /// <param name="input">Input image to get pixels from</param>
        /// <param name="y">The current row</param>
        /// <param name="startingCoordinate">X coordinate to start from</param>
        /// <returns>Number of equal pixels found</returns>
        private int CountRemovablePixels(FastBitmap input, int y, int startingCoordinate)
        {
            var lastColor = input.GetPixel(startingCoordinate, y);
            for (int x=startingCoordinate; x >= 0; x--)
            {
                var currentColor = input.GetPixel(x, y);
                if (!almostEquals(currentColor,lastColor)) 
                {
                    return startingCoordinate-x; 
                }
            }
            return startingCoordinate;
        }

        /// <summary>
        /// Calculates color equality.
        /// Workaround for Windows XP screenshots which do not have 100% equal pixels.
        /// </summary>
        /// <returns>True if the RBG value is similar (maximum R+G+B difference is 8)</returns>
        private bool almostEquals(Color c1, Color c2)
        {
            int r = c1.R;
            int g = c1.G;
            int b = c1.B;
            int diff = (Math.Abs(r - c2.R) + Math.Abs(g - c2.G) + Math.Abs(b - c2.B));
            return (diff < 8) ;
        }

        /// <summary>
        /// Paint pixels that can be removed, starting at the X coordinate and painting to the right
        /// </summary>
        /// <param name="y">The current row</param>
        /// <param name="output">Overlay output image to draw on</param>
        /// <param name="removableColor">Color to use for drawing</param>
        /// <param name="width">Number of pixels that can be removed</param>
        /// <param name="start">Starting coordinate to begin drawing</param>
        private void PaintRemovableArea(int y, FastBitmap output, Color removableColor, int width, int start)
        {
            for(int i=start;i<start+width;i++)
            {
                output.SetPixel(i, y, removableColor);
            }
        }

        private readonly int _imageHeight;
        private readonly int _imageWidth;
        private List<PixelArea> _pixelAreas;

        public List<PixelArea> PixelAreas
        {
            get { return _pixelAreas; }
        }
    }
}

1
+1 재미있는 접근법, 마음에 듭니다! 여기에 게시 된 알고리즘 중 일부와 내 알고리즘이 최적의 결과를 얻기 위해 결합되면 재미있을 것입니다. 편집 : C #은 읽을 수있는 괴물입니다. 논리가있는 필드 또는 함수 / 게터인지 항상 확실하지는 않습니다.
Rolf ツ

1

Haskell, 중복 순차 라인을 순진하게 제거

불행히도,이 모듈은 매우 일반적인 유형의 함수 만 제공 Eq a => [[a]] -> [[a]]합니다. 하스켈에서 이미지 파일을 편집하는 방법을 모르기 때문에 PNG 이미지를 [[Color]]값으로 변환 할 수 있다고 확신합니다 instance Eq Color. 쉽게 정의 할 수 있습니다.

해당 기능은 resizeL입니다.

암호:

import Data.List

nubSequential []    = []
nubSequential (a:b) = a : g a b where
 g x (h:t)  | x == h =     g x t
            | x /= h = h : g h t
 g x []     = []

resizeL     = nubSequential . transpose . nubSequential . transpose

설명:

참고 : 의 유형 목록에 접두사가 붙은 요소를a : b 의미 하므로 목록이 생성됩니다. 이것이 목록의 기본 구성입니다. 빈 목록을 나타냅니다. aa[]

참고 : a :: b means a는 유형 b입니다. 예를 들어, if a :: k, (a : []) :: [k]여기서, 여기서는 [x]유형의 항목을 포함하는 목록을 나타냅니다 x.
이것은 그 (:)자체가 아무런 논증도없는 것을 의미합니다 :: a -> [a] -> [a]. 는 ->무엇인가 뭔가에서 함수를 의미한다.

이는 import Data.List단순히 다른 사람들이 우리를 위해 한 일을 가져 와서 다시 작성하지 않고도 기능을 사용할 수있게합니다.

먼저 함수를 정의하십시오 nubSequential :: Eq a => [a] -> [a].
이 함수는 동일한 목록의 후속 요소를 제거합니다.
그래서 nubSequential [1, 2, 2, 3] === [1, 2, 3]. 이제이 기능을로 표시 nS합니다.

경우 nS빈 목록에 적용, 아무것도 우리 간단한 반환을 빈 목록을 수행하지 않고, 할 수 있습니다.

nS내용이있는 목록에가 적용 되면 실제 처리를 수행 할 수 있습니다. 이를 where위해 재귀를 사용하려면 여기에 -clause 로 두 번째 함수가 필요합니다 nS. 비교할 요소를 추적하지 않기 때문입니다.
우리는이 함수의 이름을 지정합니다 g. 첫 번째 인수를 주어진 목록의 헤드와 비교하고 이전의 첫 번째 인수와 꼬리를 맞추고 호출하면 헤드를 버립니다. 그렇지 않은 경우 머리를 꼬리에 추가하고 머리를 새 첫 번째 인수로 전달합니다.
를 사용하기 위해 g우리는 그것을 논쟁의 머리 nS와 꼬리를 그것의 두 논쟁으로 제공합니다.

nS이제 유형 Eq a => [a] -> [a]이되어 목록을 가져 와서 목록을 반환합니다. 함수 정의에서 수행되므로 요소 간의 동등성을 확인할 수 있어야합니다.

그런 다음, 우리는 구성하는 기능을 nS하고 transpose사용하여 (.)연산자를.
구성 기능은 다음을 의미합니다 (f . g) x = f (g (x))..

이 예에서는 transpose표를 90 ° 회전 nS하고 목록의 모든 순차적 인 등가 요소를 제거합니다.이 경우 다른 목록 (즉, 테이블)은 다시 transpose회전 한 후 nS순차적 인 등가 요소를 제거합니다. 이것은 본질적으로 열의 후속 중복 행을 제거합니다.

a동등성 ( instance Eq a)을 확인할 수있는 경우에도 가능하기 때문에 가능합니다 [a].
한마디로 :instance Eq a => Eq [a]

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.