A veces necesito un Resizer de captura de pantalla sin pérdida

44

A veces necesito escribir más documentación que solo comentarios en el código. Y a veces, esas explicaciones necesitan capturas de pantalla. A veces, las condiciones para obtener una captura de pantalla son tan extrañas que le pido a un desarrollador que me tome una captura de pantalla. A veces, la captura de pantalla no se ajusta a mis especificaciones y tengo que cambiar su tamaño para que se vea bien.

Como puede ver, las circunstancias para la necesidad del mágico "Ressless Screenshot Resizer" son muy poco probables. De todos modos, para mí parece que lo necesito todos los días. Pero aún no existe.

Te he visto aquí en PCG resolver asombrosos rompecabezas gráficos antes, así que supongo que este es bastante aburrido para ti ...

Especificación

  • El programa toma una captura de pantalla de una sola ventana como entrada
  • La captura de pantalla no utiliza efectos de vidrio o similares (por lo que no necesita lidiar con ningún material de fondo que brille)
  • El formato del archivo de entrada es PNG (o cualquier otro formato sin pérdidas para que no tenga que lidiar con artefactos de compresión)
  • El formato del archivo de salida es el mismo que el formato del archivo de entrada.
  • El programa crea una captura de pantalla de diferente tamaño como salida. El requisito mínimo se está reduciendo de tamaño.
  • El usuario deberá especificar el tamaño de salida esperado. Si puede dar pistas sobre el tamaño mínimo que su programa puede producir a partir de la entrada dada, eso es útil.
  • La captura de pantalla de salida no debe tener menos información si es interpretada por un humano. No eliminará el contenido de texto o imagen, pero eliminará áreas con fondo solamente. Ver ejemplos a continuación.
  • Si no es posible obtener el tamaño esperado, el programa debe indicar eso y no simplemente bloquear o eliminar información sin previo aviso.
  • Si el programa indica las áreas que se eliminarán por razones de verificación, eso debería aumentar su popularidad.
  • El programa puede necesitar alguna otra entrada del usuario, por ejemplo, para identificar el punto de partida para la optimización.

Reglas

Este es un concurso de popularidad. Se acepta la respuesta con más votos el 2015-03-08.

Ejemplos

Captura de pantalla de Windows XP. Tamaño original: 1003x685 píxeles.

Captura de pantalla XP grande

Áreas de ejemplo (rojo: vertical, amarillo: horizontal) que se pueden eliminar sin perder ninguna información (texto o imágenes). Tenga en cuenta que la barra roja no es contigua. Este ejemplo no indica todos los píxeles posibles que podrían eliminarse potencialmente.

Indicadores de eliminación de capturas de pantalla XP

Se redimensionó sin pérdidas: 783x424 píxeles.

Captura de pantalla XP pequeña

Captura de pantalla de Windows 10. Tamaño original: 999x593 píxeles.

Captura de pantalla de Windows 10 grande

Áreas de ejemplo que se pueden eliminar.

Se indica la eliminación de la captura de pantalla de Windows 10

Captura de pantalla redimensionada sin pérdidas: 689x320 píxeles.

Tenga en cuenta que está bien que el texto del título ("Descargas") y "Esta carpeta esté vacía" ya no estén centrados. Por supuesto, sería mejor si estuviera centrado, y si su solución lo proporciona, debería ser más popular.

Captura de pantalla de Windows 10 pequeña

Thomas Weller
fuente
3
Me recuerda a la función de " escalado consciente del contenido " de Photoshop .
2015
De qué formato es la entrada. ¿Podemos elegir cualquier formato de imagen estándar?
HEGX64
@ThomasW dijo "Supongo que este es bastante aburrido". No es verdad. Esto es diabólico.
Logic Knight
1
Esta pregunta no recibe suficiente atención, la primera respuesta ha sido votada porque fue la única respuesta durante mucho tiempo. La cantidad de votos por el momento no es suficiente para representar la popularidad de las diferentes respuestas. La pregunta es ¿cómo podemos lograr que más personas voten? Incluso voté por una respuesta.
Rolf ツ
1
@Rolf ツ: He comenzado una recompensa que vale 2/3 de la reputación que me he ganado de esta pregunta hasta ahora. Espero que sea lo suficientemente justo.
Thomas Weller

Respuestas:

29

Pitón

la función delrowselimina todas las filas duplicadas menos una y devuelve la imagen transpuesta, al aplicarla dos veces también elimina las columnas y la transpone nuevamente. Además thresholdcontrola cuántos píxeles pueden diferir para que dos líneas sigan siendo consideradas iguales

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

ingrese la descripción de la imagen aquí
ingrese la descripción de la imagen aquí

Al voltear el comparador maskdesde >a, se <=generarán las áreas eliminadas, que en su mayoría son espacios en blanco.

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

golfed (porque por qué no)
En lugar de comparar cada píxel, solo mira la suma, como efecto secundario esto también convierte la captura de pantalla en escala de grises y tiene problemas con permutaciones de preservación de la suma, como la flecha hacia abajo en la barra de direcciones del Win8 captura de pantalla

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

ingrese la descripción de la imagen aquí
ingrese la descripción de la imagen aquí

DenDenDo
fuente
Wow, incluso jugó al golf ... (espero que hayas sabido que este es un concurso de popularidad)
Thomas Weller
¿te importaría eliminar el puntaje de golf? Esto podría dejar a la gente pensando que esto es código golf. Gracias.
Thomas Weller
1
@ThomasW. eliminó el puntaje y lo movió al fondo, fuera de la vista.
DenDenDo
15

Java: intente sin pérdidas y recurra a contenido

(¡El mejor resultado sin pérdidas hasta ahora!)

Captura de pantalla XP sin pérdida sin el tamaño deseado

Cuando examiné por primera vez esta pregunta, pensé que no era un rompecabezas o un desafío, solo alguien que necesitaba desesperadamente un programa y su código;) Pero está en mi naturaleza resolver problemas de visión, por lo que no pude evitar que este intentara este desafío. !

Se me ocurrió el siguiente enfoque y combinación de algoritmos.

En pseudocódigo se ve así:

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

Técnicas utilizadas

  • Intensidad en escala de grises
  • Dilatación
  • Igual columna de búsqueda y eliminar
  • Tallado de costuras
  • Detección de bordes sobel
  • Umbral

El programa

El programa puede recortar capturas de pantalla sin pérdidas, pero tiene una opción para recurrir a recortes conscientes del contenido que no son 100% sin pérdidas. Los argumentos del programa se pueden ajustar para lograr mejores resultados.

Nota: El programa se puede mejorar de muchas maneras (¡no tengo tanto tiempo libre!)

Argumentos

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

Código

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

Resultados


Captura de pantalla XP sin pérdida sin el tamaño deseado (compresión máxima sin pérdida)

Argumentos: "image.png" 1 1 5 10 falso 0

Resultado: 836 x 323

Captura de pantalla XP sin pérdida sin el tamaño deseado


Captura de pantalla de XP a 800x600

Argumentos: "image.png" 800600 6 10 verdadero 60

Resultado: 800 x 600

El algoritmo sin pérdida elimina alrededor de 155 líneas horizontales de las que el algoritmo recurre a la eliminación consciente del contenido para que se puedan ver algunos artefactos.

Captura de pantalla de XP a 800x600


Captura de pantalla de Windows 10 a 700x300

Argumentos: "image.png" 700300 6 10 verdadero 60

Resultado: 700 x 300

El algoritmo sin pérdida elimina 270 líneas horizontales de las que el algoritmo recurre a la eliminación de contenido que elimina otras 29. Vertical solo se utiliza el algoritmo sin pérdida.

Captura de pantalla de Windows 10 a 700x300


Captura de pantalla de Windows 10 con reconocimiento de contenido a 400x200 (prueba)

Argumentos: "image.png" 400200 5 10 verdadero 600

Resultado: 400 x 200

Esta fue una prueba para ver cómo se vería la imagen resultante después del uso severo de la función de contenido. El resultado está muy dañado pero no es irreconocible.

Captura de pantalla de Windows 10 con reconocimiento de contenido a 400x200 (prueba)


Rolf ツ
fuente
La primera salida no está completamente recortada. Tanto puedo truncar desde la derecha
Optimizer
Eso es porque los argumentos (de mi programa) dicen que no debería optimizarlo más allá de 800 píxeles :)
Rolf ツ
Desde este popcon, probablemente debería mostrar los mejores resultados :)
Optimizer
Mi programa inicial es igual que la otra respuesta, pero también tiene una función de contenido para reducir aún más la escala. También tiene la opción de recortar al ancho y alto deseados (ver pregunta).
Rolf ツ
3

C #, algoritmo como lo haría manualmente

Este es mi primer programa de procesamiento de imágenes y me llevó un tiempo implementarlo con todo eso, LockBitsetc. Pero quería que fuera rápido (usando Parallel.For) para obtener una respuesta casi instantánea.

Básicamente, mi algoritmo se basa en observaciones sobre cómo elimino píxeles manualmente de una captura de pantalla:

  • Estoy comenzando desde el borde derecho, porque hay más posibilidades de que haya píxeles no utilizados.
  • Defino un umbral para la detección de bordes para capturar los botones del sistema correctamente. Para la captura de pantalla de Windows 10, un umbral de 48 píxeles funciona bien.
  • Después de detectar el borde (marcado en color rojo a continuación), busco píxeles del mismo color. Tomo el número mínimo de píxeles encontrados y lo aplico a todas las filas (marcadas en violeta).
  • Luego empiezo de nuevo con detección de bordes (marcado en rojo), píxeles del mismo color (marcado en azul, luego en verde, luego en amarillo), etc.

Por el momento lo hago solo horizontalmente. El resultado vertical puede usar el mismo algoritmo y operar en una imagen girada a 90 °, por lo que en teoría es posible.

Resultados

Esta es una captura de pantalla de mi aplicación con regiones detectadas:

Redimensionador de captura de pantalla sin pérdida

Y este es el resultado de la captura de pantalla de Windows 10 y el umbral de 48 píxeles. La salida tiene 681 píxeles de ancho. Lamentablemente, no es perfecto (consulte "Buscar descargas" y algunas de las barras de columnas verticales).

Resultado de Windows 10, umbral de 48 píxeles

Y otro con umbral de 64 píxeles (567 píxeles de ancho). Esto se ve aún mejor.

Resultado de Windows 10, umbral de 64 píxeles

Resultado general aplicando rotación para recortar también desde abajo (567x304 píxeles).

Resultado de Windows 10, umbral de 64 píxeles, girado

Para Windows XP, necesitaba cambiar un poco el código ya que los píxeles no son exactamente iguales. Estoy aplicando un umbral de similitud de 8 (diferencia en el valor RGB). Tenga en cuenta algunos artefactos en las columnas.

Captura de pantalla sin pérdida de tamaño con la captura de pantalla de Windows XP cargada

Resultado de Windows XP

Código

Bueno, mi primer intento de procesamiento de imágenes. No se ve muy bien, ¿verdad? Esto solo enumera el algoritmo central, no la interfaz de usuario y no la rotación de 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
fuente
1
+1 Enfoque interesante, ¡me gusta! Sería divertido si algunos de los algoritmos publicados aquí, como el mío y el tuyo, se combinen para lograr resultados óptimos. Editar: C # es un monstruo para leer, no siempre estoy seguro de si algo es un campo o una función / getter con lógica.
Rolf ツ
1

Haskell, utilizando la eliminación ingenua de líneas secuenciales duplicadas

Desafortunadamente, este módulo solo proporciona una función con un tipo muy genérico Eq a => [[a]] -> [[a]], ya que no tengo idea de cómo editar archivos de imagen en Haskell, sin embargo, estoy seguro de que es posible transformar una imagen PNG a un [[Color]]valor y me imagino instance Eq Colorque sería Fácilmente definible.

La función en cuestión es resizeL.

Código:

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

Explicación:

Nota: a : b significa elemento a prefijado a la lista de tipo dea , lo que resulta en una lista. Esta es la construcción fundamental de las listas. []denota la lista vacía.

Nota: a :: b significa aes de tipo b. Por ejemplo, si a :: k, entonces (a : []) :: [k], donde [x]denota una lista que contiene cosas de tipo x.
Esto significa que (:)sí mismo, sin ningún argumento, :: a -> [a] -> [a]. El ->denota una función de algo a algo.

El import Data.Listsimplemente consigue un trabajo a otras personas hicieron por nosotros y nos permite utilizar sus funciones sin tener que reescribir ellos.

Primero, defina una función nubSequential :: Eq a => [a] -> [a].
Esta función elimina elementos posteriores de una lista que son idénticos.
Por lo tanto, nubSequential [1, 2, 2, 3] === [1, 2, 3]. Ahora abreviaremos esta función como nS.

Si nSse aplica a una lista vacía, no se puede hacer nada, y simplemente devolvemos una lista vacía.

Si nSse aplica a una lista con contenido, se puede realizar el procesamiento real. Para esto, necesitamos una segunda función, aquí en una wherecláusula-, para usar la recursividad, ya que nuestra nSno realiza un seguimiento de un elemento para comparar.
Nombramos esta función g. Funciona comparando su primer argumento con el encabezado de la lista que se le ha dado, y descartando el encabezado si coinciden y llamándose a sí mismo con el primer argumento anterior. Si no lo hacen, agrega la cabeza a la cola, se pasa a través de sí misma con la cabeza como el nuevo primer argumento.
Para usar g, le damos la cabeza del argumento nSy la cola como sus dos argumentos.

nSahora es de tipo Eq a => [a] -> [a], toma una lista y devuelve una lista. Requiere que podamos verificar la igualdad entre los elementos, ya que esto se hace en la definición de la función.

Luego, componimos las funciones nSy utilizamos transposeel (.)operador.
Composición de funciones significa lo siguiente: (f . g) x = f (g (x)).

En nuestro ejemplo, transposegira una tabla 90 °, nSelimina todos los elementos iguales secuenciales de la lista, en este caso otras listas (eso es lo que es una tabla), la transposegira hacia atrás y nSnuevamente elimina elementos iguales secuenciales. Esto es esencialmente eliminar las filas duplicadas posteriores y las columnas.

Esto es posible porque si ase puede verificar la igualdad ( instance Eq a), también lo [a]es.
En breve:instance Eq a => Eq [a]

schuelermine
fuente