Иногда мне нужно изменить размер скриншота без потерь


44

Иногда мне нужно написать больше документации, чем просто комментарии в коде. И иногда для этих объяснений нужны скриншоты. Иногда условия получения такого скриншота настолько странные, что я прошу разработчика сделать для меня скриншот. Иногда скриншот не соответствует моим характеристикам, и мне нужно изменить его размер, чтобы он выглядел красиво.

Как видите, обстоятельства для волшебного «Lossless Screenshot Resizer» крайне маловероятны. Во всяком случае, мне кажется, что мне это нужно каждый день. Но его пока нет.

Я видел, как вы здесь, на PCG, решали удивительные графические головоломки , так что я думаю, что это довольно скучно для вас ...

Спецификация

  • Программа делает скриншот одного окна в качестве ввода
  • На скриншоте не используются стеклянные эффекты или аналогичные элементы (поэтому вам не нужно разбираться с фоновыми материалами, которые просвечивают)
  • Формат входного файла - PNG (или любой другой формат без потерь, чтобы вам не приходилось иметь дело с артефактами сжатия)
  • Формат выходного файла такой же, как формат входного файла
  • Программа создает скриншот разного размера в качестве вывода. Минимальное требование уменьшается в размере.
  • Пользователь должен указать ожидаемый размер вывода. Если вы можете дать подсказки о минимальном размере, который ваша программа может создать из заданного ввода, это полезно.
  • Выходной скриншот не должен содержать меньше информации, если интерпретируется человеком. Вы не должны удалять текстовое или графическое содержимое, но должны удалять только области с фоном. Смотрите примеры ниже.
  • Если невозможно получить ожидаемый размер, программа должна указать это, а не просто вывести из строя или удалить информацию без дальнейшего уведомления.
  • Если программа указывает области, которые будут удалены по причинам проверки, это должно увеличить ее популярность.
  • Программа может нуждаться в некотором другом пользовательском вводе, например, чтобы определить начальную точку для оптимизации.

правила

Это конкурс популярности. Ответ с большинством голосов 2015-03-08 принимается.

Примеры

Скриншот Windows XP. Оригинальный размер: 1003х685 пикселей.

Скриншот XP большой

Примеры областей (красный: вертикальный, желтый: горизонтальный), которые можно удалить без потери какой-либо информации (текста или изображений). Обратите внимание, что красная полоса не является смежной. В этом примере не указаны все возможные пиксели, которые потенциально могут быть удалены.

Индикаторы удаления скриншотов XP

Изменение без потерь: 783x424 пикселей.

Скриншот XP маленький

Скриншот Windows 10. Оригинальный размер: 999x593 пикселей.

Большой скриншот Windows 10

Примеры областей, которые можно удалить.

Указано удаление скриншота Windows 10

Снимок экрана без потерь: 689x320 пикселей.

Обратите внимание, что текст заголовка («Загрузки») и «Эта папка пуста» больше не центрируются. Конечно, было бы лучше, если бы оно было центрировано, и если ваше решение обеспечивает это, оно должно стать более популярным.

Скриншот Windows 10 маленький


3
Напоминает мне о функции масштабирования с учетом содержания в Photoshop .
agtoever

Какой формат ввода. Можем ли мы выбрать любой стандартный формат изображения?
HEGX64

@ThomasW сказал: «Думаю, это довольно скучно». Не правда. Это дьявольское.
Логика Найт

1
Этот вопрос не получает достаточного внимания, первый ответ был отклонен, потому что это был единственный ответ в течение длительного времени. Количество голосов на данный момент недостаточно для представления популярности различных ответов. Вопрос в том, как заставить больше людей голосовать? Даже я проголосовал за ответ.
Рольф ツ

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из в >к <=вместо будет выводить удаленные области, которые в основном пустое пространство.

введите описание изображения здесь введите описание изображения здесь

игра в гольф (потому что почему бы и нет)
Вместо того, чтобы сравнивать каждый пиксель, он смотрит только на сумму, в качестве побочного эффекта он также преобразует снимок экрана в оттенки серого и имеет проблемы с сохраняющими сумму перестановками, такими как стрелка вниз в адресной строке 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')

введите описание изображения здесь
введите описание изображения здесь


Вау, даже в гольф ... (надеюсь, вы знали, что это конкурс популярности)
Томас Веллер

Вы не против убрать счет в гольфе? Это может заставить людей думать, что это кодовый гольф. Спасибо.
Томас Уэллер

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

Используемые методики:

  • Интенсивность оттенков серого
  • расширение
  • Равный столбец поиска и удаления
  • Шов по дереву
  • Обнаружение края Собеля
  • Thresholding

Программа

Программа может обрезать скриншоты без потерь, но имеет возможность вернуться к контентной обрезке, которая не без потерь на 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 х 323

Скриншот XP без потерь без желаемого размера


Скриншот XP до 800x600

Аргументы: "image.png" 800 600 6 10 true 60

Результат: 800 х 600

Алгоритм без потерь удаляет около 155 горизонтальных линий, а алгоритм возвращается к удалению с учетом содержимого, поэтому можно увидеть некоторые артефакты.

Скриншот Xp до 800x600


Скриншот Windows 10 до 700x300

Аргументы: "image.png" 700 300 6 10 true 60

Результат: 700 х 300

Алгоритм без потерь удаляет 270 горизонтальных линий, а алгоритм возвращается к удалению с учетом содержимого, которое удаляет еще 29. По вертикали используется только алгоритм без потерь.

Скриншот Windows 10 до 700x300


Скриншот Windows 10 с учетом содержимого до 400x200 (тест)

Аргументы: "image.png" 400 200 5 10 true 600

Результат: 400 х 200

Это был тест, чтобы увидеть, как будет выглядеть полученное изображение после интенсивного использования контентно-зависимой функции. Результат сильно поврежден, но не узнаваем.

Скриншот Windows 10 с учетом содержимого до 400x200 (тест)



Первый вывод не полностью урезан. Так много можно усечь справа
Оптимизатор

Это потому, что аргументы (моей программы) говорят, что она не должна оптимизировать ее дальше 800 пикселей :)
Рольф olf

Так как этот popcon, вы, вероятно, должны показать лучшие результаты :)
Оптимизатор

Моя программа выполняет инициализацию так же, как и другой ответ, но она также имеет функцию с учетом содержимого для еще большего уменьшения масштаба. Он также имеет возможность обрезать до желаемой ширины и высоты (см. Вопрос).
Рольф ツ

3

C #, алгоритм, как я бы сделал это вручную

Это моя первая программа для обработки изображений, и мне потребовалось некоторое время, чтобы реализовать ее LockBitsи т. Д. Но я хотел, чтобы она была быстрой (с использованием Parallel.For), чтобы получить почти мгновенную обратную связь.

В основном мой алгоритм основан на наблюдениях за тем, как я удаляю пиксели вручную из скриншота:

  • Я начинаю с правого края, потому что больше шансов, что есть неиспользованные пиксели.
  • Я определяю порог для обнаружения края, чтобы правильно фиксировать системные кнопки. Для скриншота Windows 10 порог в 48 пикселей работает хорошо.
  • После обнаружения края (отмечен красным цветом ниже), я ищу пиксели того же цвета. Я беру минимальное количество найденных пикселей и применяю его ко всем строкам (отмечен фиолетовым).
  • Затем я снова начинаю с определения края (отмечен красным), пикселей того же цвета (отмечен синим, затем зеленым, затем желтым) и т. Д.

На данный момент я делаю это только по горизонтали. Вертикальный результат может использовать тот же алгоритм и работать с повернутым на 90 ° изображением, поэтому теоретически это возможно.

Полученные результаты

Это скриншот моего приложения с обнаруженными регионами:

Lossless Screenshot Resizer

И это результат для скриншота Windows 10 и порога 48 пикселей. Вывод составляет 681 пикселей в ширину. К сожалению, он не идеален (см. «Поиск загрузок» и некоторые вертикальные столбцы).

Результат Windows 10, порог 48 пикселей

И еще один с порогом в 64 пикселя (шириной 567 пикселей). Это выглядит даже лучше.

Результат Windows 10, порог 64 пикселя

Общий результат применения поворота для обрезки со всего дна (567x304 пикселей).

Результат Windows 10, порог 64 пикселя, повернут

Для Windows XP мне нужно было немного изменить код, поскольку пиксели не совсем равны. Я применяю порог сходства 8 (разница в значении RGB). Обратите внимание на некоторые артефакты в столбцах.

Lossless Screenshot Resizer с загруженным скриншотом Windows XP

Результат Windows XP

Код

Ну, моя первая попытка обработки изображений. Выглядит не очень хорошо, правда? Здесь перечислены только основные алгоритмы, а не пользовательский интерфейс и не поворот на 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 # - монстр для чтения, я не всегда уверен, является ли что-то полем или функцией / получателем с логикой.
Рольф ツ

1

Haskell, используя наивное удаление повторяющихся последовательных строк

К сожалению, этот модуль предоставляет только функцию с очень общим типом Eq a => [[a]] -> [[a]], так как я не знаю, как редактировать файлы изображений в Haskell, однако я уверен, что можно преобразовать изображение 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 означает элемент с a префиксом в списке типаa , в результате чего список. Это фундаментальная конструкция списков. []обозначает пустой список.

Примечание: a :: b средство aотносится к типу b. Например, если 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]

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.