Іноді мені потрібен програвач зменшення екрана без втрат


44

Іноді мені потрібно написати більше документації, ніж просто коментарі в коді. І іноді для цих пояснень потрібні знімки екрана. Іноді умови отримати такий скріншот настільки дивні, що я прошу розробника зробити знімок екрана для мене. Іноді скріншот не відповідає моїм характеристикам, і мені доводиться змінювати його розмір, щоб він виглядав красиво.

Як бачите, обставини необхідності магічного "Lossless Screenshot Resizer" дуже малоймовірні. У будь-якому випадку, мені здається, мені це потрібно щодня. Але вона ще не існує.

Я бачив, як ви тут, на 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
Це питання не приділяє належної уваги, перша відповідь була анонсована, оскільки це була єдина відповідь тривалий час. Наразі кількості голосів недостатньо, щоб представити популярність різних відповідей. Питання в тому, як можна примусити більше людей голосувати? Навіть я проголосував за відповідь.
Рольф ツ

1
@ Рольф ツ: Я розпочав щедрості на суму 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')

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


Нічого собі, навіть гольф ... (Сподіваюся, ви знали, що це змагання за популярність)
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 без втрат без потрібного розміру


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

Аргументи: "image.png" 800 600 6 10 вірно 60

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

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

Знімок екрана Xp до 800x600


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

Аргументи: "image.png" 700 300 6 10 вірно 60

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

Алгоритм без втрат видаляє 270 горизонтальних ліній, ніж алгоритм відновлює видалення вмісту, яке видаляє ще 29. Вертикальний використовується лише алгоритм без втрат.

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


Відображення вмісту скріншоту Windows 10 до 400x200 (тест)

Аргументи: "image.png" 400 200 5 10 правда 600

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

Це був тест, щоб побачити, як виглядатиме отримане зображення після жорсткого використання функції, орієнтованої на вміст. Результат сильно пошкоджений, але не впізнаваний.

Відображення вмісту скріншоту Windows 10 до 400x200 (тест)



Перший вихід не повністю оброблений. Стільки можна врізати справа
Optimizer

Це тому, що аргументи (моєї програми) говорять про те, що вона не повинна оптимізувати її більше ніж 800 пікселів :)
Рольф ツ

З цього попкону ви, мабуть, повинні показати найкращі результати :)
Оптимізатор

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

3

C #, алгоритм, як я це робив би вручну

Це моя перша програма обробки зображень, і для цього потрібно було деякий час, щоб реалізувати всі ці LockBitsречі тощо. Але я хотів, щоб це було швидко (з використанням Parallel.For), щоб отримати майже миттєвий зворотній зв'язок.

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

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

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

Результати

Це скріншот моєї програми з виявленими регіонами:

Знижувач екрана без втрат

І це результат для скріншоту Windows 10 і порогового значення 48 пікселів. Вихід становить 681 пікселів. На жаль, це не ідеально (див. "Пошук завантажень" та деякі вертикальні смуги стовпців).

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

І ще один із порогом 64 пікселів (ширина 567 пікселів). Це виглядає ще краще.

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

Загальний результат, застосовуючи обертання для обрізання також з усього дна (567x304 пікселів).

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

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

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-клаузі, для використання рекурсії, оскільки наша 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.