A volte ho bisogno di un Lossless Screenshot Resizer


44

A volte ho bisogno di scrivere più documentazione che solo commenti nel codice. E a volte, quelle spiegazioni hanno bisogno di schermate. A volte le condizioni per ottenere uno screenshot del genere sono così strane che chiedo a uno sviluppatore di fare uno screenshot per me. A volte lo screenshot non si adatta alle mie specifiche e devo ridimensionarlo in modo che appaia bene.

Come puoi vedere, le circostanze per la necessità del magico "Lossless Screenshot Resizer" sono molto improbabili. Ad ogni modo, per me sembra che ne abbia bisogno ogni giorno. Ma non esiste ancora.

Ti ho già visto qui su PCG risolvere incredibili puzzle grafici prima, quindi immagino che questo sia piuttosto noioso per te ...

specificazione

  • Il programma prende uno screenshot di una singola finestra come input
  • Lo screenshot non utilizza effetti di vetro o simili (quindi non è necessario occuparsi di eventuali elementi di sfondo che traspare)
  • Il formato del file di input è PNG (o qualsiasi altro formato senza perdita di dati in modo da non dover avere a che fare con artefatti di compressione)
  • Il formato del file di output è lo stesso del formato del file di input
  • Il programma crea uno screenshot di diverse dimensioni come output. Il requisito minimo si sta riducendo in termini di dimensioni.
  • L'utente deve specificare la dimensione di output prevista. Se puoi dare suggerimenti sulla dimensione minima che il tuo programma può produrre dall'input dato, è utile.
  • Lo screenshot di output non deve contenere meno informazioni se interpretato da un essere umano. Non devi rimuovere il testo o il contenuto dell'immagine, ma rimuovi solo le aree con sfondo. Vedi esempi di seguito.
  • Se non è possibile ottenere la dimensione prevista, il programma dovrebbe indicarlo e non semplicemente arrestare o rimuovere le informazioni senza ulteriore avviso.
  • Se il programma indica le aree che verranno rimosse per motivi di verifica, ciò dovrebbe aumentare la sua popolarità.
  • Il programma potrebbe richiedere altri input da parte dell'utente, ad esempio per identificare il punto di partenza per l'ottimizzazione.

Regole

Questo è un concorso di popolarità. La risposta con più voti in data 08-03-2015 viene accettata.

Esempi

Schermata di Windows XP. Dimensioni originali: 1003x685 pixel.

Screenshot di XP grande

Aree di esempio (rosso: verticale, giallo: orizzontale) che possono essere rimosse senza perdere alcuna informazione (testo o immagini). Si noti che la barra rossa non è contigua. Questo esempio non indica tutti i possibili pixel che potrebbero essere potenzialmente rimossi.

Indicatori di rimozione screenshot XP

Ridimensionato senza perdita di dati: 783x424 pixel.

Screenshot di XP piccolo

Schermata di Windows 10. Dimensioni originali: 999x593 pixel.

Schermata di Windows 10 di grandi dimensioni

Esempi di aree che possono essere rimosse.

Rimozione dello screenshot di Windows 10 indicata

Schermata senza perdita di dimensioni: 689x320 pixel.

Va notato che il testo del titolo ("Download") e "Questa cartella è vuota" non sono più centrati. Certo, sarebbe meglio se fosse centrato, e se la tua soluzione lo prevede, dovrebbe diventare più popolare.

Schermata di Windows 10 piccola


3
Mi ricorda la funzione " ridimensionamento consapevole del contenuto " di Photoshop .
agtoever,

Quale formato è l'input. Possiamo scegliere qualsiasi formato di immagine standard?
HEGX64,

@ThomasW ha detto "Immagino che questo sia piuttosto noioso". Non vero. Questo è diabolico.
Logic Knight,

1
Questa domanda non riceve abbastanza attenzione, la prima risposta è stata votata perché è stata l'unica risposta per molto tempo. La quantità di voti non è al momento sufficiente per rappresentare la popolarità delle diverse risposte. La domanda è: come possiamo convincere più persone a votare? Anche io ho votato una risposta.
Rolf ツ

1
@Rolf ツ: ho iniziato una taglia del valore di 2/3 della reputazione che ho guadagnato finora da questa domanda. Spero sia abbastanza giusto.
Thomas Weller,

Risposte:


29

Pitone

la funzione delrowselimina tutte le righe tranne una duplicata e restituisce l'immagine trasposta, applicandola due volte elimina anche le colonne e la traspone indietro. thresholdControlla inoltre quanti pixel possono differire affinché due linee siano ancora considerate uguali

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

inserisci qui la descrizione dell'immagine
inserisci qui la descrizione dell'immagine

Capovolgendo il comparatore maskda >a <=si genereranno invece le aree rimosse che sono principalmente spazi vuoti.

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

golfed (perché perché no)
Invece di confrontare ogni pixel guarda solo la somma, come effetto collaterale converte anche lo screenshot in scala di grigi e ha problemi con le permutazioni di conservazione della somma, come la freccia giù nella barra degli indirizzi di Win8 immagine dello schermo

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

inserisci qui la descrizione dell'immagine
inserisci qui la descrizione dell'immagine


Wow, anche golf ... (Spero che tu fossi consapevole che si tratta di una gara di popolarità)
Thomas Weller,

ti dispiacerebbe rimuovere il punteggio di golf? Questo potrebbe lasciare la gente a pensare che questo sia il codice golf. Grazie.
Thomas Weller,

1
@ThomasW. rimosso il punteggio e spostato verso il basso, fuori dalla vista.
DenDenDo,

15

Java: prova senza perdita di dati e fallback a riconoscere i contenuti

(Il miglior risultato senza perdite finora!)

Screenshot di XP senza perdita di dimensioni desiderato

Quando ho guardato per la prima volta questa domanda, ho pensato che questo non fosse un enigma o una sfida, ma solo qualcuno che aveva un disperato bisogno di un programma ed è un codice;) Ma è nella mia natura risolvere problemi di vista, quindi non potevo impedire a me stesso di provare questa sfida !

Ho escogitato il seguente approccio e la combinazione di algoritmi.

In pseudo-codice è simile al seguente:

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

Tecniche utilizzate:

  • Scala di grigi dell'intensità
  • dilatazione
  • Ricerca e rimozione della colonna uguale
  • Cucitura intaglio
  • Rilevamento dei bordi di Sobel
  • thresholding

Il programma

Il programma può ritagliare gli screenshot senza perdita di dati, ma ha un'opzione per il fallback al ritaglio consapevole del contenuto che non è senza perdita al 100%. Gli argomenti del programma possono essere modificati per ottenere risultati migliori.

Nota: il programma può essere migliorato in molti modi (non ho molto tempo libero!)

argomenti

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

Codice

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

risultati


Schermata XP senza perdita delle dimensioni desiderate (compressione senza perdita massima)

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

Risultato: 836 x 323

Screenshot di XP senza perdita di dimensioni desiderato


Screenshot di XP a 800x600

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

Risultato: 800 x 600

L'algoritmo senza perdita rimuove circa 155 linee orizzontali rispetto all'algoritmo che ricade sulla rimozione consapevole del contenuto per cui è possibile vedere alcuni artefatti.

Screenshot di XP a 800x600


Schermata di Windows 10 a 700x300

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

Risultato: 700 x 300

L'algoritmo senza perdita rimuove 270 linee orizzontali rispetto all'algoritmo ricade nella rimozione consapevole del contenuto che ne rimuove un'altra 29. Verticale viene utilizzato solo l'algoritmo senza perdita.

Schermata di Windows 10 a 700x300


Screenshot di Windows 10 compatibile con il contenuto fino a 400x200 (test)

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

Risultato: 400 x 200

Questo è stato un test per vedere come sarebbe stata l'immagine risultante dopo un uso intensivo della funzionalità sensibile al contenuto. Il risultato è fortemente danneggiato ma non irriconoscibile.

Screenshot di Windows 10 compatibile con il contenuto fino a 400x200 (test)



Il primo output non è completamente tagliato. Posso troncare così tanto da destra
Ottimizzatore

Questo perché gli argomenti (del mio programma) dicono che non dovrebbe ottimizzarlo oltre 800 pixel :)
Rolf ツ

Dal momento che questo popcon, dovresti probabilmente mostrare i migliori risultati :)
Ottimizzatore

Il mio programma ha lo stesso principio dell'altra risposta, ma ha anche una funzione sensibile ai contenuti per un ulteriore ridimensionamento. Ha anche la possibilità di ritagliare la larghezza e l'altezza desiderate (vedi domanda).
Rolf ツ

3

C #, algoritmo come lo farei manualmente

Questo è il mio primo programma di elaborazione delle immagini e ci è voluto un po 'di tempo per implementarlo con tutta quella LockBitsroba ecc. Ma volevo che fosse veloce (usando Parallel.For) per ottenere un feedback quasi istantaneo.

Fondamentalmente il mio algoritmo si basa su osservazioni su come rimuovo manualmente i pixel da uno screenshot:

  • Sto iniziando dal bordo destro, perché ci sono maggiori probabilità che ci siano pixel inutilizzati.
  • Definisco una soglia per il rilevamento dei bordi per acquisire correttamente i pulsanti di sistema. Per lo screenshot di Windows 10, una soglia di 48 pixel funziona bene.
  • Dopo che il bordo è stato rilevato (contrassegnato in rosso sotto), cerco pixel dello stesso colore. Prendo il numero minimo di pixel trovati e lo applico a tutte le righe (contrassegnato in viola).
  • Quindi ricomincio da capo con il rilevamento dei bordi (contrassegnato in rosso), i pixel dello stesso colore (contrassegnati in blu, quindi in verde, quindi in giallo) e così via

Al momento lo faccio solo in orizzontale. Il risultato verticale può utilizzare lo stesso algoritmo e operare su un'immagine ruotata di 90 °, quindi in teoria è possibile.

risultati

Questo è uno screenshot della mia applicazione con le regioni rilevate:

Lossless Screenshot Resizer

E questo è il risultato per lo screenshot di Windows 10 e la soglia di 48 pixel. L'output è largo 681 pixel. Sfortunatamente non è perfetto (vedi "Cerca download" e alcune delle barre delle colonne verticali).

Risultato di Windows 10, soglia di 48 pixel

E un altro con soglia di 64 pixel (567 pixel di larghezza). Questo sembra ancora meglio.

Risultato di Windows 10, soglia di 64 pixel

Risultato complessivo che applica la rotazione al ritaglio anche da tutto il fondo (567x304 pixel).

Risultato di Windows 10, soglia di 64 pixel, ruotato

Per Windows XP, avevo bisogno di cambiare un po 'il codice poiché i pixel non sono esattamente uguali. Sto applicando una soglia di somiglianza di 8 (differenza nel valore RGB). Nota alcuni artefatti nelle colonne.

Lossless Screenshot Resizer con screenshot di Windows XP caricato

Risultato di Windows XP

Codice

Bene, il mio primo tentativo di elaborazione delle immagini. Non sembra molto bello, vero? Questo elenca solo l'algoritmo core, non l'interfaccia utente e non la rotazione di 90 °.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;

namespace LosslessScreenshotResizer.BL
{
    internal class PixelAreaSearcher
    {
        private readonly Bitmap _originalImage;

        private readonly int _edgeThreshold;
        readonly Color _edgeColor = Color.FromArgb(128, 255, 0, 0);
        readonly Color[] _iterationIndicatorColors =
        {
            Color.FromArgb(128, 0, 0, 255), 
            Color.FromArgb(128, 0, 255, 255), 
            Color.FromArgb(128, 0, 255, 0),
            Color.FromArgb(128, 255, 255, 0)
        };

        public PixelAreaSearcher(Bitmap originalImage, int edgeThreshold)
        {
            _originalImage = originalImage;
            _edgeThreshold = edgeThreshold;

            // cache width and height. Also need to do that because of some GDI exceptions during LockBits
            _imageWidth = _originalImage.Width;
            _imageHeight = _originalImage.Height;            
        }

        public Bitmap SearchHorizontal()
        {
            return Search();
        }

        /// <summary>
        /// Find areas of pixels to keep and to remove. You can get that information via <see cref="PixelAreas"/>.
        /// The result of this operation is a bitmap of the original picture with an overlay of the areas found.
        /// </summary>
        /// <returns></returns>
        private unsafe Bitmap Search()
        {
            // FastBitmap is a wrapper around Bitmap with LockBits enabled for fast operation.
            var input = new FastBitmap(_originalImage);
            // transparent overlay
            var overlay = new FastBitmap(_originalImage.Width, _originalImage.Height);

            _pixelAreas = new List<PixelArea>(); // save the raw data for later so that the image can be cropped
            int startCoordinate = _imageWidth - 1; // start at the right edge
            int iteration = 0; // remember the iteration to apply different colors
            int minimum;
            do
            {
                var indicatorColor = GetIterationColor(iteration);

                // Detect the edge which is not removable
                var edgeStartCoordinates = new PixelArea(_imageHeight) {AreaType = AreaType.Keep};
                Parallel.For(0, _imageHeight, y =>
                {
                    edgeStartCoordinates[y] = DetectEdge(input, y, overlay, _edgeColor, startCoordinate);
                }
                    );
                _pixelAreas.Add(edgeStartCoordinates);

                // Calculate how many pixels can theoretically be removed per line
                var removable = new PixelArea(_imageHeight) {AreaType = AreaType.Dummy};
                Parallel.For(0, _imageHeight, y =>
                {
                    removable[y] = CountRemovablePixels(input, y, edgeStartCoordinates[y]);
                }
                    );

                // Calculate the practical limit
                // We can only remove the same amount of pixels per line, otherwise we get a non-rectangular image
                minimum = removable.Minimum;
                Debug.WriteLine("Can remove {0} pixels", minimum);

                // Apply the practical limit: calculate the start coordinates of removable areas
                var removeStartCoordinates = new PixelArea(_imageHeight) { AreaType = AreaType.Remove };
                removeStartCoordinates.Width = minimum;
                for (int y = 0; y < _imageHeight; y++) removeStartCoordinates[y] = edgeStartCoordinates[y] - minimum;
                _pixelAreas.Add(removeStartCoordinates);

                // Paint the practical limit onto the overlay for demo purposes
                Parallel.For(0, _imageHeight, y =>
                {
                    PaintRemovableArea(y, overlay, indicatorColor, minimum, removeStartCoordinates[y]);
                }
                    );

                // Move the left edge before starting over
                startCoordinate = removeStartCoordinates.Minimum;
                var remaining = new PixelArea(_imageHeight) { AreaType = AreaType.Keep };
                for (int y = 0; y < _imageHeight; y++) remaining[y] = startCoordinate;
                _pixelAreas.Add(remaining);

                iteration++;
            } while (minimum > 1);


            input.GetBitmap(); // TODO HACK: release Lockbits on the original image 
            return overlay.GetBitmap();
        }

        private Color GetIterationColor(int iteration)
        {
            return _iterationIndicatorColors[iteration%_iterationIndicatorColors.Count()];
        }

        /// <summary>
        /// Find a minimum number of contiguous pixels from the right side of the image. Everything behind that is an edge.
        /// </summary>
        /// <param name="input">Input image to get pixel data from</param>
        /// <param name="y">The row to be analyzed</param>
        /// <param name="output">Output overlay image to draw the edge on</param>
        /// <param name="edgeColor">Color for drawing the edge</param>
        /// <param name="startCoordinate">Start coordinate, defining the maximum X</param>
        /// <returns>X coordinate where the edge starts</returns>
        private int DetectEdge(FastBitmap input, int y, FastBitmap output, Color edgeColor, int startCoordinate)
        {
            var repeatCount = 0;
            var lastColor = Color.DodgerBlue;
            int x;

            for (x = startCoordinate; x >= 0; x--)
            {
                var currentColor = input.GetPixel(x, y);
                if (almostEquals(lastColor,currentColor))
                {
                    repeatCount++;
                }
                else
                {
                    lastColor = currentColor;
                    repeatCount = 0;
                    for (int i = x; i < startCoordinate; i++)
                    {
                        output.SetPixel(i,y,edgeColor);
                    }
                }

                if (repeatCount > _edgeThreshold)
                {
                    return x + _edgeThreshold;
                }
            }
            return repeatCount;
        }

        /// <summary>
        /// Counts the number of contiguous pixels in a row, starting on the right and going to the left
        /// </summary>
        /// <param name="input">Input image to get pixels from</param>
        /// <param name="y">The current row</param>
        /// <param name="startingCoordinate">X coordinate to start from</param>
        /// <returns>Number of equal pixels found</returns>
        private int CountRemovablePixels(FastBitmap input, int y, int startingCoordinate)
        {
            var lastColor = input.GetPixel(startingCoordinate, y);
            for (int x=startingCoordinate; x >= 0; x--)
            {
                var currentColor = input.GetPixel(x, y);
                if (!almostEquals(currentColor,lastColor)) 
                {
                    return startingCoordinate-x; 
                }
            }
            return startingCoordinate;
        }

        /// <summary>
        /// Calculates color equality.
        /// Workaround for Windows XP screenshots which do not have 100% equal pixels.
        /// </summary>
        /// <returns>True if the RBG value is similar (maximum R+G+B difference is 8)</returns>
        private bool almostEquals(Color c1, Color c2)
        {
            int r = c1.R;
            int g = c1.G;
            int b = c1.B;
            int diff = (Math.Abs(r - c2.R) + Math.Abs(g - c2.G) + Math.Abs(b - c2.B));
            return (diff < 8) ;
        }

        /// <summary>
        /// Paint pixels that can be removed, starting at the X coordinate and painting to the right
        /// </summary>
        /// <param name="y">The current row</param>
        /// <param name="output">Overlay output image to draw on</param>
        /// <param name="removableColor">Color to use for drawing</param>
        /// <param name="width">Number of pixels that can be removed</param>
        /// <param name="start">Starting coordinate to begin drawing</param>
        private void PaintRemovableArea(int y, FastBitmap output, Color removableColor, int width, int start)
        {
            for(int i=start;i<start+width;i++)
            {
                output.SetPixel(i, y, removableColor);
            }
        }

        private readonly int _imageHeight;
        private readonly int _imageWidth;
        private List<PixelArea> _pixelAreas;

        public List<PixelArea> PixelAreas
        {
            get { return _pixelAreas; }
        }
    }
}

1
+1 Approccio interessante, mi piace! Sarebbe divertente se alcuni degli algoritmi pubblicati qui, come il mio e il tuo, fossero combinati per ottenere risultati ottimali. Modifica: C # è un mostro da leggere, non sono sempre sicuro se qualcosa è un campo o una funzione / getter con la logica.
Rolf ツ

1

Haskell, usando l'ingenua rimozione di linee sequenziali duplicate

Sfortunatamente, questo modulo fornisce solo una funzione con un tipo molto generico Eq a => [[a]] -> [[a]], dal momento che non ho idea di come modificare i file di immagine in Haskell, tuttavia, sono certo che è possibile trasformare un'immagine PNG in un [[Color]]valore e immagino instance Eq Colordi essere facilmente definibile.

La funzione in questione è resizeL.

Codice:

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

Spiegazione:

Nota: a : b significa elemento con a prefisso all'elenco del tipo dia , risultante in un elenco. Questa è la costruzione fondamentale delle liste. []indica l'elenco vuoto.

Nota: a :: b significa che aè di tipo b. Ad esempio, se a :: k, quindi (a : []) :: [k], dove [x]indica un elenco contenente elementi di tipo x.
Ciò significa che (:)se stessa, senza alcun argomento, :: a -> [a] -> [a]. La ->denota una funzione da qualcosa a qualcosa.

Il import Data.Listottiene semplicemente un po 'di lavoro alcune altre persone hanno fatto per noi e ci consente di utilizzare le loro funzioni senza riscrivere.

Innanzitutto, definire una funzione nubSequential :: Eq a => [a] -> [a].
Questa funzione rimuove gli elementi successivi di un elenco identici.
Quindi nubSequential [1, 2, 2, 3] === [1, 2, 3]. Ora abbreviamo questa funzione come nS.

Se nSviene applicato a un elenco vuoto, non è possibile eseguire alcuna operazione e restituiamo semplicemente un elenco vuoto.

Se nSviene applicato a un elenco con contenuti, è possibile eseguire l'elaborazione effettiva. Per questo, abbiamo bisogno di una seconda funzione, qui in un where-clause, per usare la ricorsione, poiché il nostro nSnon tiene traccia di un elemento con cui confrontarlo.
Chiamiamo questa funzione g. Funziona confrontando il suo primo argomento con il capo della lista che è stato dato, e scartando il capo se corrispondono e chiamandosi sulla coda con il vecchio primo argomento. In caso contrario, aggiunge la testa alla coda, passava attraverso se stessa con la testa come nuovo primo argomento.
Per usare g, gli diamo la testa dell'argomento nSe la coda come suoi due argomenti.

nSè ora di tipo Eq a => [a] -> [a], prendendo un elenco e restituendo un elenco. Richiede che possiamo verificare l'uguaglianza tra gli elementi, come avviene nella definizione della funzione.

Quindi, componiamo le funzioni nSe transposeutilizziamo l' (.)operatore.
Funzioni comporre significa la seguente: (f . g) x = f (g (x)).

Nel nostro esempio, transposeruota una tabella di 90 °, nSrimuove tutti gli elementi uguali sequenziali dell'elenco, in questo caso altri elenchi (che è una tabella), la transposeruota indietro e nSrimuove nuovamente elementi uguali sequenziali. Ciò sta essenzialmente rimuovendo le successive righe duplicate e colonne.

Questo è possibile perché se aè verificabile per uguaglianza ( instance Eq a), lo [a]è anche.
In breve:instance Eq a => Eq [a]

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.