Parfois, j'ai besoin d'un programme de résolution d'écran sans perte

44

Parfois, j'ai besoin d'écrire plus de documentation que de simples commentaires dans le code. Et parfois, ces explications ont besoin de captures d'écran. Parfois, les conditions pour obtenir une telle capture d'écran sont si étranges que je demande à un développeur de prendre une capture d'écran pour moi. Parfois, la capture d’écran ne correspond pas à mes spécifications et je dois la redimensionner pour qu’elle ait une belle apparence.

Comme vous pouvez le constater, les circonstances justifiant le besoin de la magie "Lossless Screenshot Resizer" sont très improbables. Quoi qu'il en soit, il me semble que j'en ai besoin tous les jours. Mais ça n'existe pas encore.

Je t'ai déjà vu sur PCG résoudre des énigmes graphiques impressionnantes auparavant, alors je suppose que celui-ci est plutôt ennuyeux pour toi ...

spécification

  • Le programme prend une capture d'écran d'une seule fenêtre en tant qu'entrée
  • La capture d'écran ne fait pas appel à des effets de verre ou similaires (vous n'avez donc pas besoin de vous occuper des éléments d'arrière-plan qui transparaissent)
  • Le format de fichier en entrée est PNG (ou tout autre format sans perte afin que vous n'ayez pas à traiter d'artefacts de compression).
  • Le format du fichier de sortie est le même que celui du fichier d'entrée
  • Le programme crée une capture d'écran de taille différente en sortie. La taille minimale requise est réduite.
  • L'utilisateur doit spécifier la taille de sortie attendue. Si vous pouvez donner des indications sur la taille minimale que votre programme peut produire à partir d'une entrée donnée, c'est utile.
  • La capture d'écran de sortie ne doit pas contenir moins d'informations si interprétée par un humain. Vous ne devez pas supprimer le texte ou le contenu de l'image, mais vous devez supprimer les zones avec uniquement un arrière-plan. Voir les exemples ci-dessous.
  • S'il n'est pas possible d'obtenir la taille attendue, le programme doit l'indiquer et non pas simplement bloquer ou supprimer des informations sans préavis.
  • Si le programme indique les zones qui seront supprimées pour des raisons de vérification, sa popularité augmentera.
  • Le programme peut nécessiter une autre saisie de l'utilisateur, par exemple pour identifier le point de départ de l'optimisation.

Règles

C'est un concours de popularité. La réponse avec le plus de votes le 08/03/2015 est acceptée.

Exemples

Capture d'écran de Windows XP. Taille originale: 1003x685 pixels.

XP capture d'écran grande

Exemple de zones (rouge: vertical, jaune: horizontal) pouvant être supprimées sans perte d’informations (texte ou images). Notez que la barre rouge n'est pas contiguë. Cet exemple n'indique pas tous les pixels possibles pouvant éventuellement être supprimés.

Indicateurs de suppression de capture d'écran XP

Redimensionné sans perte: 783x424 pixels.

XP capture d'écran petite

Windows 10 capture d'écran. Taille originale: 999x593 pixels.

Windows 10 capture d'écran large

Exemple de zones pouvant être supprimées.

Windows 10 capture d'écran indiqué

Capture d'écran redimensionnée sans perte: 689x320 pixels.

Notez que le texte du titre ("Téléchargements") et "Ce dossier est vide" ne sont plus centrés. Bien sûr, il serait plus judicieux qu’il soit centré et si votre solution le permet, elle devrait devenir plus populaire.

Windows 10 capture d'écran petite

Thomas Weller
la source
3
Cela me rappelle la fonction de " mise à l'échelle en fonction du contenu " de Photoshop .
agtoever
Quel format est l'entrée. Peut-on choisir n'importe quel format d'image standard?
HEGX64
@ThomasW a déclaré "Je suppose que celui-ci est plutôt ennuyeux". Pas vrai. C'est diabolique.
Logic Knight le
1
Cette question ne reçoit pas assez d'attention, la première réponse a été votée parce que c'était la seule réponse pendant une longue période. Le nombre de voix est pour le moment insuffisant pour représenter la popularité des différentes réponses. La question est de savoir comment pouvons-nous amener plus de gens à voter? Même j'ai voté sur une réponse.
Rolf
1
@Rolf ツ: J'ai commencé une prime d'une valeur de 2/3 de la réputation que j'ai tirée de cette question jusqu'à présent. J'espère que c'est assez juste.
Thomas Weller

Réponses:

29

Python

la fonction delrowssupprime toutes les lignes dupliquées sauf une et renvoie l'image transposée. En l'appliquant deux fois, les colonnes sont également supprimées et transposées. De plus, thresholdcontrôle le nombre de pixels pouvant différer pour que deux lignes soient toujours considérées comme identiques.

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

entrez la description de l'image ici
entrez la description de l'image ici

En retournant le comparateur maskde >à, vous <=obtiendrez les zones supprimées, qui sont généralement des espaces vierges.

entrez la description de l'image ici entrez la description de l'image ici

joué au golf (parce que pourquoi pas)
Au lieu de comparer chaque pixel, il ne regarde que la somme. Cet effet secondaire convertit également la capture d'écran en niveaux de gris et ne permet pas de conserver les permutations de la somme, comme la flèche vers le bas dans la barre d'adresse du Win8. capture d'écran

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

entrez la description de l'image ici
entrez la description de l'image ici

DenDenDo
la source
Wow, même golfé ... (J'espère que vous saviez qu'il s'agit d'un concours de popularité)
Thomas Weller
cela vous dérangerait-il de retirer le score de golf? Cela pourrait laisser les gens penser qu'il s'agit d'un code de golf. Merci.
Thomas Weller
1
@ThomasW. enlevé le score et déplacé vers le bas, hors de vue.
DenDenDo
15

Java: essayez sans perte et faites appel au contenu

(Meilleur résultat sans perte jusqu'à présent!)

Capture d'écran XP sans perte sans la taille souhaitée

Lorsque j’ai examiné cette question pour la première fois, j’ai pensé que ce n’était pas un casse-tête ou un défi, mais bien une personne qui avait désespérément besoin d’un programme et de son code;) Mais c’est dans ma nature de résoudre des problèmes de vision afin que je ne puisse m'empêcher d’essayer ce défi !

Je suis venu avec l'approche suivante et la combinaison d'algorithmes.

En pseudo-code, cela ressemble à ceci:

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

Techniques utilisées:

  • Intensité en niveaux de gris
  • Dilatation
  • Recherche de colonne égale et suppression
  • Découpage des coutures
  • Détection de bord Sobel
  • Seuillage

Le programme

Le programme peut recadrer des captures d'écran sans perte, mais dispose d'une option permettant de recadrer un recadrage en fonction du contenu, qui n'est pas sans perte à 100%. Les arguments du programme peuvent être peaufinés pour obtenir de meilleurs résultats.

Remarque: le programme peut être amélioré de nombreuses manières (je n'ai pas beaucoup de temps libre!)

Arguments

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

Résultats


Capture d'écran XP sans perte sans la taille souhaitée (compression maximale sans perte)

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

Résultat: 836 x 323

Capture d'écran XP sans perte sans la taille souhaitée


XP capture d'écran à 800x600

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

Résultat: 800 x 600

L'algorithme sans perte supprime environ 155 lignes horizontales et ne prend plus en compte le contenu, ce qui permet de voir certains artefacts.

Xp capture d'écran à 800x600


Windows 10 capture d'écran à 700x300

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

Résultat: 700 x 300

L'algorithme sans perte supprime 270 lignes horizontales, puis retombe en suppression basée sur le contenu, ce qui en supprime 29 autres. Verticalement, seul l'algorithme sans perte est utilisé.

Windows 10 capture d'écran à 700x300


Capture d'écran compatible avec le contenu de Windows 10 à 400x200 (test)

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

Résultat: 400 x 200

Il s'agissait d'un test permettant de voir à quoi ressemblerait l'image résultante après une utilisation intensive de la fonctionnalité prenant en compte le contenu. Le résultat est lourdement endommagé mais pas méconnaissable.

Capture d'écran compatible avec le contenu de Windows 10 à 400x200 (test)


Rolf
la source
La première sortie n'est pas complètement ajustée. Tant de choses peuvent me tronquer de la droite
Optimiseur
C'est parce que les arguments (de mon programme) disent qu'il ne devrait pas être optimisé au-delà de 800 pixels :)
Rolf ツ
Depuis cette popcon, vous devriez probablement montrer les meilleurs résultats :)
Optimizer
Mon programme commence par les mêmes réponses que l’autre réponse, mais il dispose également d’une fonction de détection du contenu qui permet une réduction encore plus grande. Il a également la possibilité de rogner à la largeur et à la hauteur désirées (voir question).
Rolf
3

C #, algorithme comme je le ferais manuellement

C’est mon premier programme de traitement d’image et il a fallu un certain temps pour le mettre en oeuvre LockBits, etc. Mais je voulais que ce soit rapide (avec Parallel.For) pour obtenir un retour presque instantané.

Fondamentalement, mon algorithme est basé sur des observations sur la façon dont je supprime manuellement les pixels d'une capture d'écran:

  • Je pars du bord droit, car les chances que les pixels inutilisés soient plus élevés sont plus élevées.
  • Je définis un seuil pour la détection de bord afin de capturer correctement les boutons du système. Pour la capture d'écran Windows 10, un seuil de 48 pixels convient bien.
  • Une fois le bord détecté (indiqué en rouge ci-dessous), je recherche des pixels de la même couleur. Je prends le nombre minimum de pixels trouvé et l'applique à toutes les lignes (en violet).
  • Ensuite, je recommence avec la détection des contours (marquée en rouge), les pixels de la même couleur (marquée en bleu, puis en vert, puis en jaune), etc.

Pour le moment je le fais horizontalement seulement. Le résultat vertical peut utiliser le même algorithme et fonctionner sur une image tournée de 90 °, donc en théorie, c'est possible.

Résultats

Voici une capture d'écran de mon application avec les régions détectées:

Capture sans perte Resizer

Et voici le résultat pour la capture d'écran de Windows 10 et le seuil de 48 pixels. La sortie mesure 681 pixels de large. Malheureusement, ce n'est pas parfait (voir "Recherche de téléchargements" et quelques barres de colonnes verticales).

Résultat Windows 10, seuil de 48 pixels

Et un autre avec un seuil de 64 pixels (567 pixels de large). Cela a l'air encore mieux.

Résultat Windows 10, seuil de 64 pixels

Résultat global en appliquant la rotation à la culture de tous les fonds (567x304 pixels).

Résultat Windows 10, seuil de 64 pixels, pivoté

Pour Windows XP, je devais changer un peu le code car les pixels ne sont pas exactement égaux. J'applique un seuil de similarité de 8 (différence de la valeur RVB). Notez quelques artefacts dans les colonnes.

Capture sans perte Resizer avec la capture d'écran de Windows XP chargée

Résultat Windows XP

Code

Eh bien, ma première tentative de traitement d’image. Ne semble pas très bien, n'est-ce pas? Cela ne fait que lister l'algorithme principal, pas l'interface utilisateur et pas la rotation à 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
la source
1
+1 approche intéressante, je l'aime! Il serait amusant que certains des algorithmes affichés ici, comme le mien et le vôtre, soient combinés pour obtenir des résultats optimaux. Edit: C # est un monstre à lire, je ne suis pas toujours sûr de savoir si quelque chose est un champ ou une fonction / getter avec une logique.
Rolf ツ
1

Haskell, en utilisant la suppression naïve de lignes séquentielles en double

Malheureusement, ce module ne fournit qu'une fonction de type très générique Eq a => [[a]] -> [[a]], car je ne sais pas comment éditer des fichiers image dans Haskell. Cependant, je suis certain qu'il est possible de transformer une image PNG en une [[Color]]valeur et j'imagine instance Eq Colorque facilement définissable.

La fonction en question est 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

Explication:

Remarque: a : b signifie un élément a préfixé à la liste de typesa , résultant en une liste. C'est la construction fondamentale des listes. []dénote la liste vide.

Note: le a :: b moyen aest de type b. Par exemple, si a :: k, alors (a : []) :: [k], où [x]indique une liste contenant des objets de type x.
Cela signifie que (:)lui - même, sans aucun argument, :: a -> [a] -> [a]. Le ->dénote une fonction de quelque chose à quelque chose.

Ils import Data.Listobtiennent simplement du travail que d'autres personnes ont fait pour nous et nous permettent d'utiliser leurs fonctions sans les réécrire.

Tout d'abord, définissez une fonction nubSequential :: Eq a => [a] -> [a].
Cette fonction supprime les éléments suivants d'une liste identiques.
Ainsi, nubSequential [1, 2, 2, 3] === [1, 2, 3]. Nous allons maintenant abréger cette fonction en nS.

Si nSest appliqué à une liste vide, rien ne peut être fait, et nous retournons simplement une liste vide.

Si nSest appliqué à une liste avec un contenu, le traitement réel peut être effectué. Pour cela, nous avons besoin d’une deuxième fonction, ici dans une whereclause, pour utiliser la récursivité, car notre nSne garde pas la trace d’un élément à comparer.
Nous nommons cette fonction g. Cela fonctionne en comparant son premier argument à la tête de la liste qui lui a été donnée, et en écartant la tête si elles correspondent, et en s'appelant à la queue avec l'ancien premier argument. Si ce n'est pas le cas, il ajoute la tête sur la queue et passe à travers lui-même avec la tête comme premier argument.
Pour l'utiliser g, nous lui donnons la tête de l'argument de nSet la queue comme ses deux arguments.

nSest maintenant de type Eq a => [a] -> [a], prenant une liste et retournant une liste. Cela nécessite que nous puissions vérifier l’égalité entre les éléments, comme cela est fait dans la définition de la fonction.

Ensuite, nous composons les fonctions nSet en transposeutilisant l' (.)opérateur.
Fonctions de composition signifie ce qui suit: (f . g) x = f (g (x)).

Dans notre exemple, transposefait pivoter un tableau de 90 °, nSsupprime tous les éléments séquentiels égaux de la liste, dans ce cas d’autres listes (c’est ce qu’est un tableau), le transposefait pivoter en arrière et nSsupprime à nouveau les éléments égaux séquentiels. Cela consiste essentiellement à supprimer les lignes et colonnes en double suivantes.

Ceci est possible car si aest vérifiable pour l'égalité ( instance Eq a), alors l' [a]est aussi.
En bref:instance Eq a => Eq [a]

schuelermine
la source