Manchmal brauche ich einen verlustfreien Screenshot Resizer

44

Manchmal muss ich mehr Dokumentation schreiben als nur Kommentare im Code. Und manchmal benötigen diese Erklärungen Screenshots. Manchmal sind die Bedingungen für einen solchen Screenshot so seltsam, dass ich einen Entwickler auffordere, einen Screenshot für mich zu machen. Manchmal entspricht der Screenshot nicht meinen Spezifikationen und ich muss seine Größe ändern, damit er gut aussieht.

Wie Sie sehen, ist es sehr unwahrscheinlich, dass der magische "Lossless Screenshot Resizer" benötigt wird. Wie auch immer, für mich scheint es, dass ich es jeden Tag brauche. Aber es existiert noch nicht.

Ich habe dich hier auf PCG schon einmal beim Lösen großartiger Grafik-Rätsel gesehen , also denke ich, dass dieses für dich ziemlich langweilig ist ...

Spezifikation

  • Das Programm erstellt einen Screenshot eines einzelnen Fensters als Eingabe
  • Der Screenshot verwendet keine Glaseffekte oder ähnliches (Sie müssen sich also nicht mit Hintergrundmaterial befassen, das durchscheint).
  • Das Eingabedateiformat ist PNG (oder ein anderes verlustfreies Format, damit Sie keine Komprimierungsartefakte verarbeiten müssen).
  • Das Ausgabedateiformat ist dasselbe wie das Eingabedateiformat
  • Das Programm erstellt als Ausgabe einen Screenshot unterschiedlicher Größe. Die Mindestanforderung ist eine Größenverringerung.
  • Der Benutzer muss die erwartete Ausgabegröße angeben. Wenn Sie Hinweise zur Mindestgröße geben können, die Ihr Programm anhand der angegebenen Eingabe erzeugen kann, ist dies hilfreich.
  • Der Ausgabe-Screenshot darf nicht weniger Informationen enthalten, wenn er von einem Menschen interpretiert wird. Sie dürfen keine Text- oder Bildinhalte entfernen, sondern nur Bereiche mit Hintergrund. Siehe Beispiele unten.
  • Wenn es nicht möglich ist, die erwartete Größe zu erhalten, sollte das Programm dies anzeigen und nicht einfach abstürzen oder Informationen ohne weitere Ankündigung entfernen.
  • Wenn das Programm die Bereiche angibt, die aus Überprüfungsgründen entfernt werden, sollte dies seine Popularität erhöhen.
  • Das Programm benötigt möglicherweise eine andere Benutzereingabe, z. B. um den Startpunkt für die Optimierung zu ermitteln.

Regeln

Dies ist ein Beliebtheitswettbewerb. Die Antwort mit den meisten Stimmen am 08.03.2015 wird angenommen.

Beispiele

Windows XP-Screenshot. Originalgröße: 1003 x 685 Pixel.

XP Screenshot groß

Beispielbereiche (rot: vertikal, gelb: horizontal), die entfernt werden können, ohne dass Informationen (Text oder Bilder) verloren gehen. Beachten Sie, dass der rote Balken nicht zusammenhängend ist. Dieses Beispiel zeigt nicht alle möglichen Pixel an, die möglicherweise entfernt werden könnten.

XP Screenshot Entfernungsanzeigen

Verlustfrei verkleinert: 783x424 Pixel.

XP Screenshot klein

Windows 10-Screenshot. Originalgröße: 999 x 593 Pixel.

Windows 10 Screenshot groß

Beispielbereiche, die entfernt werden können.

Windows 10 Screenshot Entfernung angegeben

Verlustfrei verkleinerter Screenshot: 689x320 Pixel.

Beachten Sie, dass der Titeltext ("Downloads") und "Dieser Ordner ist leer" nicht mehr zentriert sind. Natürlich wäre es schöner, wenn es zentriert wäre, und wenn Ihre Lösung dies vorsieht, sollte es populärer werden.

Windows 10 Screenshot klein

Thomas Weller
quelle
3
Erinnert mich an die " content aware scaling " -Funktion von Photoshop .
Am
Welches Format ist die Eingabe. Können wir ein Standard-Bildformat auswählen?
HEGX64,
@ThomasW sagte: "Ich denke, das ist ziemlich langweilig". Nicht wahr. Das ist teuflisch.
Logic Knight
1
Diese Frage wird nicht genügend beachtet, die erste Antwort wurde positiv bewertet, da dies die einzige Antwort seit langer Zeit war. Die Anzahl der Stimmen ist im Moment nicht ausreichend, um die Popularität der verschiedenen Antworten wiederzugeben. Die Frage ist, wie wir mehr Menschen zur Abstimmung bringen können. Sogar ich habe über eine Antwort abgestimmt.
Rolf ツ
1
@Rolf ツ: Ich habe ein Kopfgeld im Wert von 2/3 des Rufs erhalten, den ich mir mit dieser Frage bisher verdient habe. Ich hoffe das ist fair genug.
Thomas Weller

Antworten:

29

Python

Die Funktion delrowslöscht alle bis auf eine doppelte Zeile und gibt das transponierte Bild zurück. Durch zweimaliges Anwenden werden auch die Spalten gelöscht und wieder transponiert. Steuert zusätzlich threshold, wie viele Pixel sich unterscheiden können, damit zwei Zeilen immer noch als gleich gelten

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

Bildbeschreibung hier eingeben
Bildbeschreibung hier eingeben

Wenn Sie den Komparator maskvon >bis einklappen , <=werden stattdessen die entfernten Bereiche ausgegeben, bei denen es sich hauptsächlich um leere Bereiche handelt.

Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben

golfed (weil warum nicht)
Anstatt jedes Pixel zu vergleichen, wird nur die Summe betrachtet. Als Nebeneffekt wird dadurch auch der Screenshot in Graustufen umgewandelt, und es treten Probleme mit summenerhaltenden Permutationen auf, z. B. mit dem Abwärtspfeil in der Adressleiste von Win8 Bildschirmfoto

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

Bildbeschreibung hier eingeben
Bildbeschreibung hier eingeben

DenDenDo
quelle
Wow, sogar Golf gespielt ... (Ich hoffe, Sie wussten, dass dies ein Beliebtheitswettbewerb ist)
Thomas Weller
Würde es Ihnen etwas ausmachen, den Golf Score zu entfernen? Dies könnte die Leute denken lassen, dass dies Codegolf ist. Danke.
Thomas Weller
1
@ThomasW. entfernte die Partitur und schob sie außer Sichtweite nach unten.
DenDenDo
15

Java: Versuchen Sie es verlustfrei und greifen Sie auf inhaltsbewusste Funktionen zurück

(Bisher bestes verlustfreies Ergebnis!)

XP Screenshot verlustfrei ohne gewünschte Größe

Als ich diese Frage zum ersten Mal betrachtete, dachte ich, dies sei kein Rätsel oder eine Herausforderung, nur jemand, der dringend ein Programm und Code benötigt !

Ich habe mir den folgenden Ansatz und die Kombination von Algorithmen ausgedacht.

Im Pseudocode sieht es so aus:

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

Verwendete Techniken:

  • Intensität Graustufen
  • Erweiterung
  • Gleiche Spalte suchen und entfernen
  • Nahtschnitzen
  • Sobel-Kantenerkennung
  • Schwellenwert

Das Programm

Das Programm kann Screenshots verlustfrei zuschneiden, kann jedoch auf inhaltsbewusstes Zuschneiden zurückgreifen, das nicht zu 100% verlustfrei ist. Die Argumente des Programms können optimiert werden, um bessere Ergebnisse zu erzielen.

Hinweis: Das Programm kann auf viele Arten verbessert werden (ich habe nicht so viel Freizeit!)

Argumente

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

Code

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

Ergebnisse


XP Screenshot verlustfrei ohne gewünschte Größe (Max verlustfreie Komprimierung)

Argumente: "image.png" 1 1 5 10 false 0

Ergebnis: 836 x 323

XP Screenshot verlustfrei ohne gewünschte Größe


XP Screenshot auf 800x600

Argumente: "image.png" 800 600 6 10 true 60

Ergebnis: 800 x 600

Der verlustfreie Algorithmus entfernt ungefähr 155 horizontale Linien, als der Algorithmus auf inhaltsbewusstes Entfernen zurückgreift, wodurch einige Artefakte sichtbar werden.

XP-Screenshot auf 800x600


Windows 10 Screenshot auf 700x300

Argumente: "image.png" 700 300 6 10 true 60

Ergebnis: 700 x 300

Der verlustfreie Algorithmus entfernt 270 horizontale Linien, als der Algorithmus auf inhaltsbewusstes Entfernen zurückgreift, wodurch weitere 29 entfernt werden. Vertikal wird nur der verlustfreie Algorithmus verwendet.

Windows 10 Screenshot auf 700x300


Windows 10 Screenshot inhaltsbewusst bis 400x200 (Test)

Argumente: "image.png" 400 200 5 10 true 600

Ergebnis: 400 x 200

Dies war ein Test, um zu sehen, wie das resultierende Bild nach strenger Verwendung der inhaltsbewussten Funktion aussehen würde. Das Ergebnis ist stark beschädigt, aber nicht unkenntlich.

Windows 10 Screenshot inhaltsbewusst bis 400x200 (Test)


Rolf ツ
quelle
Die erste Ausgabe wird nicht vollständig abgeschnitten. So viel kann ich von rechts abschneiden
Optimierer
Das liegt daran, dass die Argumente (meines Programms) besagen, dass es nicht weiter als 800 Pixel optimiert werden sollte :)
Rolf ツ
Seit diesem Popcon sollten Sie wahrscheinlich die besten Ergebnisse zeigen :)
Optimizer
Mein Programm hat die gleiche Initialisierung wie die andere Antwort, verfügt jedoch auch über eine inhaltsbezogene Funktion zur weiteren Verkleinerung. Es besteht auch die Möglichkeit, auf die gewünschte Breite und Höhe zuzuschneiden (siehe Frage).
Rolf ツ
3

C #, Algorithmus wie ich es manuell machen würde

Dies ist mein erstes Bildbearbeitungsprogramm und es hat eine Weile gedauert, bis es mit all dem LockBitsZeug usw. implementiert wurde . Aber ich wollte, dass es schnell (mit Parallel.For) ist, um ein fast sofortiges Feedback zu erhalten.

Grundsätzlich basiert mein Algorithmus auf Beobachtungen, wie ich Pixel manuell aus einem Screenshot entferne:

  • Ich fange am rechten Rand an, weil die Wahrscheinlichkeit höher ist, dass nicht verwendete Pixel vorhanden sind.
  • Ich definiere einen Schwellenwert für die Kantenerkennung, um die Systemtasten korrekt zu erfassen. Für den Windows 10-Screenshot eignet sich ein Schwellenwert von 48 Pixel.
  • Nachdem die Kante erkannt wurde (unten rot markiert), suche ich nach Pixeln der gleichen Farbe. Ich nehme die minimale Anzahl gefundener Pixel und wende sie auf alle Zeilen an (violett markiert).
  • Dann beginne ich erneut mit der Kantenerkennung (rot markiert), Pixeln derselben Farbe (blau markiert, dann grün, dann gelb) und so weiter

Im Moment mache ich es nur horizontal. Das vertikale Ergebnis kann denselben Algorithmus verwenden und mit einem um 90 ° gedrehten Bild arbeiten. Theoretisch ist dies also möglich.

Ergebnisse

Dies ist ein Screenshot meiner Anwendung mit erkannten Regionen:

Verlustfreier Screenshot Resizer

Und dies ist das Ergebnis für den Windows 10-Screenshot und die 48-Pixel-Schwelle. Die Ausgabe ist 681 Pixel breit. Leider ist es nicht perfekt (siehe "Downloads durchsuchen" und einige der vertikalen Spaltenbalken).

Windows 10-Ergebnis, 48 ​​Pixel Schwelle

Und noch eine mit 64 Pixel Schwelle (567 Pixel breit). Das sieht noch besser aus.

Windows 10-Ergebnis, 64 Pixel Schwelle

Gesamtergebnis beim Drehen des Zuschnitts auch von unten (567 x 304 Pixel).

Windows 10 Ergebnis, 64 Pixel Schwelle, gedreht

Unter Windows XP musste ich den Code etwas ändern, da die Pixel nicht genau gleich sind. Ich wende eine Ähnlichkeitsschwelle von 8 (Unterschied im RGB-Wert) an. Beachten Sie einige Artefakte in den Spalten.

Lossless Screenshot Resizer mit geladenem Windows XP-Screenshot

Windows XP Ergebnis

Code

Naja, mein erster Versuch zur Bildbearbeitung. Sieht nicht besonders gut aus, oder? Hier wird nur der Kernalgorithmus aufgeführt, nicht die Benutzeroberfläche und nicht die 90 ° -Drehung.

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
quelle
1
+1 Interessanter Ansatz, ich mag es! Es würde Spaß machen, wenn einige der hier veröffentlichten Algorithmen, wie meine und Ihre, kombiniert würden, um optimale Ergebnisse zu erzielen. Edit: C # ist ein Monster zum Lesen, ich bin mir nicht immer sicher, ob etwas ein Feld oder eine Funktion / Getter mit Logik ist.
Rolf ツ
1

Haskell, mit naiver Entfernung von doppelten sequentiellen Linien

Leider bietet dieses Modul nur eine Funktion mit der sehr allgemeinen Art Eq a => [[a]] -> [[a]], da ich keine Ahnung, wie zu bearbeiten Bilddateien in Haskell, aber ich bin sicher , es ist möglich , ein PNG - Bild zu einem tranform [[Color]]Wert und ich kann mich vorstellen , instance Eq Colorzu sein leicht definierbar.

Die fragliche Funktion ist resizeL.

Code:

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

Erläuterung:

Hinweis: a : b bedeutet , dass der Liste des Typs des Elements ein a Präfix vorangestellt wird , was zu einer Liste führt. Dies ist die grundlegende Konstruktion von Listen. bezeichnet die leere Liste.a[]

Hinweis: a :: b bedeutet aist vom Typ b. Zum Beispiel, wenn a :: k, dann (a : []) :: [k], wo [x]bezeichnet eine Liste, die Dinge vom Typ enthält x.
Dies bedeutet, dass (:)sich ohne Argumente :: a -> [a] -> [a]. Das ->bezeichnet eine Funktion von etwas zu etwas.

Das import Data.Listbringt einfach etwas Arbeit, die andere für uns getan haben, und lässt uns ihre Funktionen nutzen, ohne sie neu zu schreiben.

Definieren Sie zunächst eine Funktion nubSequential :: Eq a => [a] -> [a].
Diese Funktion entfernt nachfolgende Elemente einer Liste, die identisch sind.
Also nubSequential [1, 2, 2, 3] === [1, 2, 3]. Wir werden diese Funktion nun abkürzen als nS.

Wenn nSauf eine leere Liste angewendet wird, kann nichts getan werden, und wir geben einfach eine leere Liste zurück.

Wenn nSauf eine Liste mit Inhalten angewendet wird, kann die eigentliche Verarbeitung erfolgen. Dazu benötigen wir eine zweite Funktion, hier in einer where-Klausel, um die Rekursion zu verwenden, da wir nSkein Element verfolgen , mit dem wir vergleichen können.
Wir nennen diese Funktion g. Es vergleicht sein erstes Argument mit dem Kopf der Liste, die ihm gegeben wurde, und verwirft den Kopf, wenn er übereinstimmt, und ruft sich selbst mit dem alten ersten Argument auf. Wenn dies nicht der Fall ist, wird der Kopf an den Schwanz angehängt und der Kopf als neues erstes Argument durch ihn hindurchgeführt.
Um es zu benutzen g, geben wir den Kopf des Arguments von nSund den Schwanz als seine zwei Argumente an.

nSist jetzt vom Typ Eq a => [a] -> [a], nimmt eine Liste und gibt eine Liste zurück. Es erfordert, dass wir die Gleichheit zwischen den Elementen überprüfen können, wie dies in der Funktionsdefinition geschieht.

Dann wir komponieren die Funktionen nSund transposemit dem (.)Betreiber.
Composing Funktionen bedeutet folgendes: (f . g) x = f (g (x)).

In unserem Beispiel wird transposeeine Tabelle um 90 ° gedreht, nSalle sequentiell gleichen Elemente aus der Liste entfernt, in diesem Fall andere Listen (das ist eine Tabelle), transposezurückgedreht und nSerneut sequentiell gleiche Elemente entfernt. Dies bedeutet im Wesentlichen, nachfolgende doppelte Zeilen und Spalten zu entfernen.

Dies ist möglich, weil, wenn aes auf equality ( instance Eq a) überprüfbar [a]ist, dies auch ist.
Zusamenfassend:instance Eq a => Eq [a]

schuelermine
quelle