Đôi khi tôi cần một Resizer Ảnh chụp màn hình lossless


44

Đôi khi tôi cần viết nhiều tài liệu hơn là chỉ nhận xét trong mã. Và đôi khi, những lời giải thích cần ảnh chụp màn hình. Đôi khi các điều kiện để có được một ảnh chụp màn hình như vậy rất kỳ lạ đến nỗi tôi yêu cầu một nhà phát triển chụp ảnh màn hình cho tôi. Đôi khi ảnh chụp màn hình không phù hợp với thông số kỹ thuật của tôi và tôi phải thay đổi kích thước để nó trông độc đáo.

Như bạn có thể thấy, các trường hợp cần một phép thuật "Resizer Ảnh chụp màn hình" là rất khó xảy ra. Dù sao, đối với tôi dường như tôi cần nó mỗi ngày. Nhưng nó chưa tồn tại.

Tôi đã thấy bạn ở đây trên PCG giải các câu đố đồ họa tuyệt vời trước đây, vì vậy tôi đoán rằng trò chơi này khá nhàm chán đối với bạn ...

Đặc điểm kỹ thuật

  • Chương trình lấy ảnh chụp màn hình của một cửa sổ làm đầu vào
  • Ảnh chụp màn hình không sử dụng hiệu ứng thủy tinh hoặc tương tự (vì vậy bạn không cần phải xử lý bất kỳ nội dung nền nào tỏa sáng)
  • Định dạng tệp đầu vào là PNG (hoặc bất kỳ định dạng lossless nào khác để bạn không phải đối phó với các tạo phẩm nén)
  • Định dạng tệp đầu ra giống như định dạng tệp đầu vào
  • Chương trình tạo ra một ảnh chụp màn hình có kích thước khác nhau như đầu ra. Yêu cầu tối thiểu là thu hẹp kích thước.
  • Người dùng phải chỉ định kích thước đầu ra dự kiến. Nếu bạn có thể đưa ra gợi ý về kích thước tối thiểu mà chương trình của bạn có thể tạo ra từ đầu vào đã cho, điều đó hữu ích.
  • Ảnh chụp màn hình đầu ra không được có ít thông tin hơn nếu được giải thích bởi con người. Bạn sẽ không xóa nội dung văn bản hoặc hình ảnh, nhưng bạn sẽ chỉ xóa các khu vực có nền. Xem ví dụ dưới đây.
  • Nếu không thể có được kích thước mong đợi, chương trình sẽ chỉ ra điều đó và không chỉ đơn giản là sự cố hoặc xóa thông tin mà không cần thông báo thêm.
  • Nếu chương trình chỉ ra các khu vực sẽ bị xóa vì lý do xác minh, điều đó sẽ làm tăng mức độ phổ biến của nó.
  • Chương trình có thể cần một số đầu vào của người dùng khác, ví dụ để xác định điểm bắt đầu để tối ưu hóa.

Quy tắc

Đây là một cuộc thi phổ biến. Câu trả lời với hầu hết các phiếu bầu vào 2015 / 03-08 được chấp nhận.

Ví dụ

Ảnh chụp màn hình Windows XP. Kích thước gốc: 1003x685 pixel.

Ảnh chụp màn hình XP lớn

Các khu vực ví dụ (đỏ: dọc, vàng: ngang) có thể được xóa mà không mất bất kỳ thông tin nào (văn bản hoặc hình ảnh). Lưu ý rằng thanh màu đỏ không liền kề nhau. Ví dụ này không chỉ ra tất cả các pixel có thể có khả năng bị xóa.

Các chỉ số loại bỏ ảnh chụp màn hình XP

Thay đổi kích thước không mất mát: 783x424 pixel.

Ảnh chụp màn hình XP nhỏ

Ảnh chụp màn hình Windows 10. Kích thước gốc: 999x593 pixel.

Ảnh chụp màn hình Windows 10 lớn

Các khu vực ví dụ có thể được gỡ bỏ.

Xóa ảnh chụp màn hình Windows 10

Ảnh chụp màn hình thay đổi kích thước không mất mát: 689x320 pixel.

Lưu ý rằng văn bản tiêu đề ("Tải xuống") và "Thư mục này trống" không còn là trung tâm nữa. Tất nhiên, nó sẽ đẹp hơn nếu nó là trung tâm, và nếu giải pháp của bạn cung cấp điều đó, nó sẽ trở nên phổ biến hơn.

Ảnh chụp màn hình Windows 10 nhỏ


3
Nhắc nhở tôi về tính năng " mở rộng nhận thức nội dung " của Photoshop .
bất cứ lúc nào 22/2/2015

Định dạng nào là đầu vào. Chúng tôi có thể chọn bất kỳ định dạng hình ảnh tiêu chuẩn?
HEGX64

@ThomasW đã nói "Tôi đoán cái này khá nhàm chán". Không đúng. Đây là ma quỷ
Logic Knight

1
Câu hỏi này không nhận được đủ sự quan tâm, câu trả lời đầu tiên đã được nêu lên vì đó là câu trả lời duy nhất trong một thời gian dài. Số lượng phiếu bầu tại thời điểm này không đủ để thể hiện mức độ phổ biến của các câu trả lời khác nhau. Câu hỏi là làm thế nào chúng ta có thể có được nhiều người bỏ phiếu? Ngay cả tôi đã bỏ phiếu cho một câu trả lời.
Rolf

1
@Rolf: Tôi đã bắt đầu một khoản tiền thưởng trị giá bằng 2/3 danh tiếng mà tôi đã kiếm được từ câu hỏi này cho đến nay. Tôi hy vọng đó là đủ công bằng.
Thomas Weller

Câu trả lời:


29

Con trăn

hàm delrowsxóa tất cả trừ một hàng trùng lặp và trả về hình ảnh được chuyển đổi, áp dụng nó hai lần cũng xóa các cột và chuyển nó trở lại. Ngoài ra, thresholdkiểm soát số lượng pixel có thể khác nhau để hai dòng vẫn được coi là giống nhau

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)))

nhập mô tả hình ảnh ở đây
nhập mô tả hình ảnh ở đây

Flipping so sánh trong masktừ >để <=sẽ thay đầu ra các lĩnh vực loại bỏ mà chủ yếu là không gian trống.

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

chơi gôn (vì tại sao không)
Thay vì so sánh từng pixel, nó chỉ nhìn vào tổng, vì một hiệu ứng phụ, điều này cũng chuyển đổi ảnh chụp màn hình thành thang độ xám và gặp rắc rối với hoán vị tổng, như mũi tên xuống trong thanh địa chỉ của Win8 ảnh chụp màn hình

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')

nhập mô tả hình ảnh ở đây
nhập mô tả hình ảnh ở đây


Wow, thậm chí đã chơi gôn ... (Tôi hy vọng bạn biết rằng đây là một cuộc thi phổ biến)
Thomas Weller

bạn có phiền xóa điểm golf không? Điều này có thể khiến mọi người nghĩ rằng đây là mã golf. Cảm ơn bạn.
Thomas Weller

1
@ThomasW. xóa điểm số và di chuyển nó xuống đáy, khuất tầm nhìn.
DenDenDo

15

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!)

Ảnh chụp màn hình XP lossless mà không có kích thước mong muốn

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

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 lossless mà không có kích thước mong muốn


Ả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 Xp tới 800x600


Ả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 lên 700x300


Ả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.

Ảnh chụp màn hình Windows 10 nhận biết nội dung tới 400x200 (kiểm tra)



Đầu ra đầu tiên không hoàn toàn được cắt. Tôi có thể cắt ngắn quá nhiều từ bên phải
Trình tối ưu hóa

Đó là bởi vì các đối số (trong chương trình của tôi) nói rằng nó không nên tối ưu hóa nó hơn 800 pixel :)
Rolf

Vì popcon này, có lẽ bạn sẽ hiển thị kết quả tốt nhất :)
Trình tối ưu hóa

Chương trình của tôi ban đầu giống như câu trả lời khác nhưng nó cũng có chức năng nhận biết nội dung để thậm chí giảm quy mô hơn nữa. Nó cũng có tùy chọn cắt theo chiều rộng và chiều cao mong muốn (xem câu hỏi).
Rolf

3

C #, thuật toán như tôi sẽ làm bằng tay

Đây là chương trình xử lý ảnh đầu tiên của tôi và phải mất một thời gian để thực hiện với tất cả những LockBitsthứ đó, v.v. Nhưng tôi muốn nó phải nhanh (sử dụng Parallel.For) để nhận được phản hồi gần như ngay lập tức.

Về cơ bản thuật toán của tôi dựa trên các quan sát về cách tôi loại bỏ pixel thủ công khỏi ảnh chụp màn hình:

  • Tôi đang bắt đầu từ cạnh phải, bởi vì khả năng cao hơn là các pixel không được sử dụng ở đó.
  • Tôi xác định ngưỡng phát hiện cạnh để chụp chính xác các nút hệ thống. Đối với ảnh chụp màn hình Windows 10, ngưỡng 48 pixel hoạt động tốt.
  • Sau khi cạnh được phát hiện (được đánh dấu màu đỏ bên dưới), tôi đang tìm kiếm các pixel cùng màu. Tôi lấy số pixel tối thiểu được tìm thấy và áp dụng nó cho tất cả các hàng (được đánh dấu màu tím).
  • Sau đó, tôi bắt đầu lại với phát hiện cạnh (được đánh dấu màu đỏ), pixel cùng màu (được đánh dấu màu xanh lam, sau đó màu xanh lá cây, sau đó màu vàng) và vv

Hiện tại tôi chỉ làm theo chiều ngang. Kết quả dọc có thể sử dụng cùng một thuật toán và hoạt động trên hình ảnh xoay 90 °, vì vậy theo lý thuyết là có thể.

Các kết quả

Đây là ảnh chụp màn hình ứng dụng của tôi với các khu vực được phát hiện:

Trình chụp ảnh màn hình lossless

Và đây là kết quả cho ảnh chụp màn hình Windows 10 và ngưỡng 48 pixel. Đầu ra rộng 681 pixel. Thật không may, nó không hoàn hảo (xem "Tải xuống tìm kiếm" và một số thanh cột dọc).

Kết quả Windows 10, ngưỡng 48 pixel

Và một số khác có ngưỡng 64 pixel (rộng 567 pixel). Điều này thậm chí còn tốt hơn.

Kết quả Windows 10, ngưỡng 64 pixel

Kết quả tổng thể áp dụng xoay vòng để cắt từ tất cả các đáy (567x304 pixel).

Kết quả Windows 10, ngưỡng 64 pixel, được xoay

Đối với Windows XP, tôi cần thay đổi mã một chút vì các pixel không chính xác bằng nhau. Tôi đang áp dụng ngưỡng tương tự là 8 (chênh lệch giá trị RGB). Lưu ý một số hiện vật trong các cột.

Trình chỉnh sửa ảnh chụp màn hình lossless với Windows XP

Kết quả Windows XP

Vâng, nỗ lực đầu tiên của tôi về xử lý hình ảnh. Có vẻ không tốt lắm phải không? Điều này chỉ liệt kê thuật toán cốt lõi, không phải giao diện người dùng và không xoay 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ách tiếp cận thú vị, tôi thích nó! Sẽ rất vui nếu một số thuật toán được đăng ở đây, như của tôi và của bạn, sẽ được kết hợp để đạt được kết quả tối ưu. Chỉnh sửa: C # là một con quái vật để đọc, tôi không phải lúc nào cũng chắc chắn nếu một cái gì đó là một lĩnh vực hoặc một chức năng / getter với logic.
Rolf

1

Haskell, sử dụng loại bỏ ngây thơ các dòng liên tiếp trùng lặp

Thật không may, mô-đun này chỉ cung cấp một chức năng với loại rất chung chung Eq a => [[a]] -> [[a]], vì tôi không biết làm thế nào để chỉnh sửa các tệp hình ảnh trong Haskell, tuy nhiên, tôi chắc chắn có thể chuyển hình ảnh PNG thành một [[Color]]giá trị và tôi tưởng tượng instance Eq Colorlà dễ dàng xác định

Các chức năng trong câu hỏi là resizeL.

Mã số:

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

Giải trình:

Lưu ý: a : b có nghĩa là phần tử được thêm a tiền tố vào danh sách loạia , dẫn đến một danh sách. Đây là xây dựng cơ bản của danh sách. []biểu thị danh sách trống.

Lưu ý: a :: b phương tiện alà loại b. Ví dụ, nếu a :: k, sau đó (a : []) :: [k], trong đó [x]biểu thị một danh sách có chứa những thứ thuộc loại x.
Điều này có nghĩa là (:)chính nó, mà không có bất kỳ đối số , :: a -> [a] -> [a]. Các ->biểu thị một hàm từ một cái gì đó để một cái gì đó.

Việc import Data.Listđơn giản là một số công việc mà một số người khác đã làm cho chúng tôi và cho phép chúng tôi sử dụng các chức năng của họ mà không cần viết lại chúng.

Đầu tiên, xác định một chức năng nubSequential :: Eq a => [a] -> [a].
Hàm này loại bỏ các phần tử tiếp theo của danh sách giống hệt nhau.
Vì vậy, nubSequential [1, 2, 2, 3] === [1, 2, 3]. Bây giờ chúng ta sẽ viết tắt chức năng này là nS.

Nếu nSđược áp dụng cho một danh sách trống, không có gì có thể được thực hiện và chúng tôi đơn giản trả về một danh sách trống.

Nếu nSđược áp dụng cho một danh sách có nội dung, thì việc xử lý thực tế có thể được thực hiện. Đối với điều này, chúng ta cần một hàm thứ hai, ở đây trong một wherephần, để sử dụng đệ quy, vì chúng nSta không theo dõi một phần tử để so sánh với.
Chúng tôi đặt tên cho chức năng này g. Nó hoạt động bằng cách so sánh đối số đầu tiên của nó với phần đầu của danh sách mà nó được đưa ra và loại bỏ phần đầu nếu chúng khớp và gọi chính nó ở phần đuôi với đối số đầu tiên cũ. Nếu họ không, nó nối đầu vào đuôi, truyền qua chính nó với đầu là đối số đầu tiên mới.
Để sử dụng g, chúng tôi cung cấp cho nó phần đầu của đối số nSvà phần đuôi là hai đối số của nó.

nSbây giờ là loại Eq a => [a] -> [a], lấy một danh sách và trả về một danh sách. Nó yêu cầu chúng ta có thể kiểm tra sự bằng nhau giữa các phần tử vì điều này được thực hiện trong định nghĩa hàm.

Sau đó, chúng tôi soạn các hàm nStransposesử dụng (.)toán tử.
Chức năng soạn thảo có nghĩa như sau : (f . g) x = f (g (x)).

Trong ví dụ của chúng tôi, transposexoay một bảng 90 °, nSloại bỏ tất cả các phần tử bằng nhau liên tiếp của danh sách, trong trường hợp này là các danh sách khác (đó là bảng là gì), transposexoay nó lại và nSloại bỏ các phần tử bằng nhau liên tiếp. Điều này về cơ bản là loại bỏ các hàng trùng lặp tiếp theo một cột.

Điều này là có thể bởi vì nếu acó thể kiểm tra được bằng ( instance Eq a), thì [a]cũng vậy.
Nói ngắn gọn:instance Eq a => Eq [a]

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.