Czasami potrzebuję Lossless Screenshot Resizer

44

Czasami muszę napisać więcej dokumentacji niż tylko komentarze w kodzie. Czasami te wyjaśnienia wymagają zrzutów ekranu. Czasami warunki uzyskania takiego zrzutu ekranu są tak dziwne, że proszę dewelopera o zrobienie zrzutu ekranu. Czasami zrzut ekranu nie pasuje do moich specyfikacji i muszę zmienić jego rozmiar, aby ładnie wyglądał.

Jak widać, okoliczności, w których potrzebny jest magiczny „Lossless Screenshot Resizer”, są bardzo mało prawdopodobne. W każdym razie wydaje mi się, że potrzebuję go każdego dnia. Ale jeszcze nie istnieje.

Widziałem cię tutaj na PCG, która wcześniej rozwiązywała niesamowite łamigłówki graficzne , więc myślę, że ta jest dla ciebie nudna ...

Specyfikacja

  • Program pobiera zrzut ekranu pojedynczego okna jako dane wejściowe
  • Zrzut ekranu nie wykorzystuje efektów szklanych ani podobnych (więc nie musisz zajmować się żadnymi elementami, które prześwitują)
  • Format pliku wejściowego to PNG (lub inny bezstratny format, dzięki czemu nie musisz zajmować się artefaktami kompresji)
  • Format pliku wyjściowego jest taki sam jak format pliku wejściowego
  • Program tworzy zrzut ekranu o innym rozmiarze jako wynik. Minimalne wymaganie zmniejsza się.
  • Użytkownik określa oczekiwany rozmiar wyjściowy. Jeśli możesz podać wskazówki dotyczące minimalnego rozmiaru, jaki program może wyprodukować na podstawie podanych danych wejściowych, jest to pomocne.
  • Zrzut ekranu wyjściowego nie może zawierać mniej informacji, jeśli jest interpretowany przez człowieka. Nie usuniesz treści tekstowych ani graficznych, ale usuniesz obszary tylko z tłem. Zobacz przykłady poniżej.
  • Jeśli uzyskanie oczekiwanego rozmiaru nie jest możliwe, program powinien to zaznaczyć, a nie po prostu zawiesić lub usunąć informacje bez uprzedzenia.
  • Jeśli program wskazuje obszary, które zostaną usunięte w celu weryfikacji, powinno to zwiększyć jego popularność.
  • Program może wymagać innych danych wejściowych od użytkownika, np. W celu ustalenia punktu początkowego optymalizacji.

Zasady

To konkurs popularności. Odpowiedź z największą liczbą głosów w dniu 08.03.2015 zostaje zaakceptowana.

Przykłady

Zrzut ekranu systemu Windows XP. Rozmiar oryginału: 1003 x 685 pikseli.

Duży zrzut ekranu XP

Przykładowe obszary (czerwony: pionowy, żółty: poziomy), które można usunąć bez utraty jakichkolwiek informacji (tekstu lub obrazów). Zauważ, że czerwony pasek nie jest ciągły. Ten przykład nie wskazuje wszystkich możliwych pikseli, które można potencjalnie usunąć.

Wskaźniki usuwania zrzutów ekranu XP

Bezstratnie zmieniono rozmiar: 783 x 424 pikseli.

Mały zrzut ekranu XP

Zrzut ekranu systemu Windows 10. Rozmiar oryginału: 999 x 593 pikseli.

Duży zrzut ekranu systemu Windows 10

Przykładowe obszary, które można usunąć.

Wskazane usunięcie zrzutu ekranu systemu Windows 10

Zrzut ekranu bezstratnie: 689 x 320 pikseli.

Zauważ, że jest ok, że tekst tytułu („Pobieranie”) i „Ten folder jest pusty” nie są już wyśrodkowane. Oczywiście byłoby lepiej, gdyby było wyśrodkowane, a jeśli twoje rozwiązanie to zapewnia, powinno stać się bardziej popularne.

Mały zrzut ekranu Windows 10

Thomas Weller
źródło
3
Przypomina mi funkcję skalowania świadomego treści w programie Photoshop .
agtoever
Jakim formatem jest dane wejściowe. Czy możemy wybrać dowolny standardowy format obrazu?
HEGX64
@ThomasW powiedział: „Myślę, że to raczej nudne”. Nie prawda. To jest diabelskie.
Logic Knight
1
Na to pytanie nie zwraca się wystarczającej uwagi, pierwsza odpowiedź została oceniona, ponieważ była to jedyna odpowiedź przez długi czas. Liczba głosów nie jest w tej chwili wystarczająca, aby reprezentować popularność różnych odpowiedzi. Pytanie brzmi: w jaki sposób możemy zmusić więcej osób do głosowania? Nawet głosowałem na odpowiedź.
Rolf ツ
1
@Rolf ツ: Założyłem nagrodę o wartości 2/3 reputacji, którą zdobyłem do tej pory z tego pytania. Mam nadzieję, że to wystarczy.
Thomas Weller

Odpowiedzi:

29

Pyton

funkcja delrowsusuwa wszystkie zduplikowane wiersze oprócz jednego i zwraca transponowany obraz, zastosowanie go dwa razy usuwa również kolumny i transponuje z powrotem. Dodatkowo thresholdkontroluje, ile pikseli może się różnić, aby dwie linie były nadal uważane za takie same

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

wprowadź opis zdjęcia tutaj
wprowadź opis zdjęcia tutaj

Odwrócenie komparatora maskod >do <=spowoduje wyświetlenie usuniętych obszarów, które są w większości pustymi miejscami.

wprowadź opis zdjęcia tutaj wprowadź opis zdjęcia tutaj

golfed (bo dlaczego nie)
Zamiast porównywać każdy piksel, patrzy tylko na sumę, jako efekt uboczny powoduje to również konwersję zrzutu ekranu do skali szarości i ma problemy z permutacjami zachowującymi sumę, jak strzałka w dół na pasku adresu Win8 zrzut ekranu

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

wprowadź opis zdjęcia tutaj
wprowadź opis zdjęcia tutaj

DenDenDo
źródło
Wow, nawet grałem w golfa ... (Mam nadzieję, że wiesz, że to konkurs popularności)
Thomas Weller
czy mógłbyś usunąć wynik golfa? To może sprawić, że ludzie będą myśleć, że to jest golf golfowy. Dziękuję Ci.
Thomas Weller
1
@ThomasW. usunął partyturę i przesunął ją na dół, poza zasięg wzroku.
DenDenDo
15

Java: spróbuj bezstratnie i wrócić do świadomej zawartości

(Najlepszy jak dotąd bezstratny wynik!)

Zrzut ekranu XP bezstratny bez pożądanego rozmiaru

Kiedy po raz pierwszy spojrzałem na to pytanie, pomyślałem, że to nie jest łamigłówka ani wyzwanie, tylko ktoś desperacko potrzebujący programu i jego kodu;) Ale leży w mojej naturze rozwiązywanie problemów ze wzrokiem, więc nie mogłem powstrzymać się od podjęcia tego wyzwania. !

Wymyśliłem następujące podejście i kombinację algorytmów.

W pseudokodzie wygląda to tak:

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

Zastosowane techniki:

  • Intensywna skala szarości
  • Rozszerzanie się
  • Równe wyszukiwanie i usuwanie kolumn
  • Rzeźba szwów
  • Wykrywanie krawędzi Sobela
  • Próg

Program

Program może przycinać zrzuty ekranu bezstratnie, ale ma opcję powrotu do przycinania uwzględniającego treść, która nie jest w 100% bezstratna. Argumenty programu można dostosować, aby uzyskać lepsze wyniki.

Uwaga: Program można ulepszyć na wiele sposobów (nie mam tyle wolnego czasu!)

Argumenty

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

Kod

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

Wyniki


Zrzut ekranu XP bezstratny bez pożądanego rozmiaru (Maksymalna kompresja bezstratna)

Argumenty: „image.png” 1 1 5 10 false 0

Wynik: 836 x 323

Zrzut ekranu XP bezstratny bez pożądanego rozmiaru


Zrzut ekranu XP do 800x600

Argumenty: „image.png” 800 600 6 10 prawda 60

Wynik: 800 x 600

Bezstratny algorytm usuwa około 155 linii poziomych, po czym algorytm wraca do usuwania treści, dzięki czemu można zobaczyć niektóre artefakty.

Zrzut ekranu XP do 800x600


Zrzut ekranu systemu Windows 10 do 700x300

Argumenty: „image.png” 700 300 6 10 prawda 60

Wynik: 700 x 300

Algorytm bezstratny usuwa 270 linii poziomych, niż algorytm wraca do usuwania treści, które usuwa kolejne 29. W pionie używany jest tylko algorytm bezstratny.

Zrzut ekranu systemu Windows 10 do 700x300


Zrzut ekranu systemu Windows 10 z uwzględnieniem zawartości do 400 x 200 (test)

Argumenty: „image.png” 400 200 5 10 prawda 600

Wynik: 400 x 200

Był to test mający na celu sprawdzenie, jak będzie wyglądał obraz wynikowy po intensywnym użyciu funkcji rozpoznawania zawartości. Rezultat jest mocno uszkodzony, ale nie nierozpoznany.

Zrzut ekranu systemu Windows 10 z uwzględnieniem zawartości do 400 x 200 (test)


Rolf ツ
źródło
Pierwsze wyjście nie jest całkowicie przycięte. Tak wiele mogę obciąć od prawej
Optymalizator
To dlatego, że argumenty (mojego programu) mówią, że nie powinien go optymalizować dalej niż 800 pikseli :)
Rolf ツ
Od tego popcona powinieneś chyba pokazać najlepsze wyniki :)
Optymalizator
Mój program wykonuje inicjał tak samo jak inna odpowiedź, ale ma także funkcję rozpoznawania zawartości, która pozwala na dalsze skalowanie w dół. Ma również opcję przycinania do pożądanej szerokości i wysokości (patrz pytanie).
Rolf ツ
3

C #, algorytm jakbym zrobił to ręcznie

To jest mój pierwszy program do przetwarzania obrazu i jego wdrożenie zajęło trochę czasu LockBitsitp. Ale chciałem, żeby był szybki (używał Parallel.For), aby uzyskać niemal natychmiastową informację zwrotną.

Zasadniczo mój algorytm opiera się na spostrzeżeniach dotyczących ręcznego usuwania pikseli ze zrzutu ekranu:

  • Zaczynam od prawej krawędzi, ponieważ są większe szanse, że będą tam niewykorzystane piksele.
  • Określam próg wykrywania krawędzi, aby poprawnie uchwycić przyciski systemowe. W przypadku zrzutu ekranu systemu Windows 10 próg 48 pikseli działa dobrze.
  • Po wykryciu krawędzi (zaznaczonej poniżej kolorem czerwonym) szukam pikseli tego samego koloru. Biorę minimalną liczbę znalezionych pikseli i stosuję ją do wszystkich wierszy (zaznaczonych na fioletowo).
  • Następnie zaczynam od nowa z wykrywaniem krawędzi (zaznaczone na czerwono), pikselami tego samego koloru (zaznaczone na niebiesko, potem na zielono, potem na żółto) i tak dalej

W tej chwili robię to tylko w poziomie. Wynik pionowy może wykorzystywać ten sam algorytm i działać na obrazie obróconym o 90 °, więc teoretycznie jest to możliwe.

Wyniki

Oto zrzut ekranu mojej aplikacji z wykrytymi regionami:

Lossless Screenshot Resizer

I to jest wynik dla zrzutu ekranu systemu Windows 10 i progu 48 pikseli. Wyjście ma szerokość 681 pikseli. Niestety nie jest doskonały (patrz „Wyszukaj pliki do pobrania” i niektóre pionowe słupki kolumn).

Wynik Windows 10, próg 48 pikseli

I jeszcze jeden z progiem 64 pikseli (szerokość 567 pikseli). To wygląda jeszcze lepiej.

Wynik Windows 10, próg 64 pikseli

Ogólny wynik zastosowania rotacji do kadrowania również ze wszystkich dna (567 x 304 pikseli).

Wynik Windows 10, próg 64 pikseli, obrócony

W systemie Windows XP musiałem nieco zmienić kod, ponieważ piksele nie są dokładnie równe. Stosuję próg podobieństwa wynoszący 8 (różnica w wartości RGB). Zwróć uwagę na niektóre artefakty w kolumnach.

Lossless Screenshot Resizer z załadowanym zrzutem ekranu Windows XP

Wynik systemu Windows XP

Kod

Cóż, moja pierwsza próba przetwarzania obrazu. Nie wygląda dobrze, prawda? Zawiera tylko główny algorytm, a nie interfejs użytkownika, a nie obrót o 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; }
        }
    }
}
Thomas Weller
źródło
1
+1 Ciekawe podejście, podoba mi się! Byłoby fajnie, gdyby niektóre z opublikowanych tutaj algorytmów, takie jak mój i twój, zostały połączone, aby osiągnąć optymalne wyniki. Edycja: C # to potwór do czytania, nie zawsze jestem pewien, czy coś jest polem lub funkcją / funkcją pobierającą z logiką.
Rolf ツ
1

Haskell, używając naiwnego usuwania podwójnych linii sekwencyjnych

Niestety, ten moduł udostępnia jedynie funkcję z typem bardzo rodzajowe Eq a => [[a]] -> [[a]], ponieważ nie mam pojęcia jak do edycji plików graficznych w Haskell, jednak jestem pewien, że to możliwe, aby tranform obrazu PNG do [[Color]]wartości, a ja sobie wyobrazić, instance Eq Coloraby być łatwo definiowalne.

Ta funkcja to resizeL.

Kod:

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

Wyjaśnienie:

Uwaga: a : b oznacza element a poprzedzony listą typua , w wyniku czego powstaje lista. To jest podstawowa konstrukcja list. []oznacza pustą listę.

Uwaga: a :: b oznacza arodzaj b. Na przykład jeśli a :: k, (a : []) :: [k]gdzie, [x]oznacza listę zawierającą elementy typu x.
Oznacza to, że (:)sam, bez żadnych argumentów :: a -> [a] -> [a]. ->Oznacza funkcję z czegoś do czegoś.

Po import Data.Listprostu dostaje trochę pracy, którą inni wykonali dla nas i pozwala nam korzystać z ich funkcji bez ich przepisywania.

Najpierw zdefiniuj funkcję nubSequential :: Eq a => [a] -> [a].
Ta funkcja usuwa identyczne kolejne elementy listy.
Więc nubSequential [1, 2, 2, 3] === [1, 2, 3]. Teraz skrócimy tę funkcję jako nS.

Jeśli nSzostanie zastosowany do pustej listy, nic nie da się zrobić, a my po prostu zwracamy pustą listę.

Jeśli nSzostanie zastosowany do listy z zawartością, wówczas można wykonać rzeczywiste przetwarzanie. W tym celu potrzebujemy drugiej funkcji, tutaj w whereklauzuli-, do użycia rekurencji, ponieważ nasza nSnie śledzi elementu do porównania.
Nazywamy tę funkcję g. Działa poprzez porównanie pierwszego argumentu z nagłówkiem podanej mu listy i odrzucenie głowy, jeśli się zgadzają, i wywołanie samego ogona ze starym pierwszym argumentem. Jeśli nie, dołącza głowę do ogona, przechodzi przez nią z głową jako nowy pierwszy argument.
Aby go użyć g, podajemy go jako argument argumentu nSi ogon jako dwa argumenty.

nSjest teraz typu Eq a => [a] -> [a], bierze listę i zwraca listę. Wymaga to, abyśmy mogli sprawdzić równość między elementami, tak jak dzieje się to w definicji funkcji.

Następnie komponujemy funkcje nSi transposeposługujemy się (.)operatorem.
Komponowanie funkcje oznaczają: (f . g) x = f (g (x)).

W naszym przykładzie transposeobraca tabelę o 90 °, nSusuwa wszystkie kolejne równe elementy listy, w tym przypadku inne listy (taka właśnie jest tabela), transposeobraca ją z powrotem i nSponownie usuwa kolejne równe elementy. To zasadniczo usuwa kolejne zduplikowane wiersze i kolumny.

Jest to możliwe, ponieważ jeśli amożna sprawdzić równość ( instance Eq a), [a]to również.
W skrócie:instance Eq a => Eq [a]

schuelermina
źródło