Às vezes eu preciso de um redimensionador de captura de tela sem perdas

44

Às vezes, preciso escrever mais documentação do que apenas comentários no código. E, às vezes, essas explicações precisam de capturas de tela. Às vezes, as condições para obter uma captura de tela são tão estranhas que peço a um desenvolvedor que faça uma captura de tela para mim. Às vezes, a captura de tela não se encaixa nas minhas especificações e eu tenho que redimensioná-la para que fique bem.

Como você pode ver, as circunstâncias para uma necessidade do mágico "Lossless Screenshot Resizer" são muito improváveis. Enfim, para mim parece que eu preciso todos os dias. Mas ainda não existe.

Eu já vi você aqui no PCG resolver quebra-cabeças gráficos impressionantes antes, então acho que este é um pouco chato para você ...

Especificação

  • O programa tira uma captura de tela de uma única janela como entrada
  • A captura de tela não utiliza efeitos de vidro ou similares (portanto, você não precisa lidar com nenhum material de fundo que brilhe)
  • O formato do arquivo de entrada é PNG (ou qualquer outro formato sem perdas, para que você não precise lidar com artefatos de compactação)
  • O formato do arquivo de saída é o mesmo que o formato do arquivo de entrada
  • O programa cria uma captura de tela de tamanho diferente como saída. O requisito mínimo está diminuindo de tamanho.
  • O usuário deve especificar o tamanho de saída esperado. Se você pode dar dicas sobre o tamanho mínimo que seu programa pode produzir a partir da entrada fornecida, isso é útil.
  • A captura de tela de saída não deve ter menos informações se interpretada por um ser humano. Você não deve remover o conteúdo de texto ou imagem, mas deve remover áreas apenas com segundo plano. Veja exemplos abaixo.
  • Se não for possível obter o tamanho esperado, o programa deve indicar isso e não simplesmente travar ou remover informações sem aviso prévio.
  • Se o programa indicar as áreas que serão removidas por motivos de verificação, isso deve aumentar sua popularidade.
  • O programa pode precisar de alguma outra entrada do usuário, por exemplo, para identificar o ponto de partida para otimização.

Regras

Este é um concurso de popularidade. A resposta com mais votos em 08/03/2015 é aceita.

Exemplos

Captura de tela do Windows XP. Tamanho original: 1003x685 pixels.

Captura de tela do XP grande

Áreas de exemplo (vermelho: vertical, amarelo: horizontal) que podem ser removidas sem perder nenhuma informação (texto ou imagens). Observe que a barra vermelha não é contígua. Este exemplo não indica todos os pixels possíveis que poderiam ser removidos.

Indicadores de remoção de captura de tela do XP

Redimensionado sem perdas: 783x424 pixels.

Pequena tela do XP

Captura de tela do Windows 10. Tamanho original: 999x593 pixels.

Grande tela do Windows 10

Áreas de exemplo que podem ser removidas.

Remoção da captura de tela do Windows 10 indicada

Captura de tela redimensionada sem perdas: 689x320 pixels.

Observe que não há problema em que o texto do título ("Downloads") e "Esta pasta esteja vazia" não estejam mais centralizados. Obviamente, seria melhor se estiver centrado e, se sua solução fornecer isso, ela se tornará mais popular.

Tela do Windows 10 pequena

Thomas Weller
fonte
3
Me lembra o recurso " redimensionamento sensível ao conteúdo " do Photoshop .
agtoever 22/02
Qual formato é a entrada. Podemos escolher qualquer formato de imagem padrão?
HEGX64 22/02
@ThomasW disse: "Acho que esse é um pouco chato". Não é verdade. Isso é diabólico.
Logic Knight
1
Esta pergunta não recebe atenção suficiente, a primeira resposta foi votada porque foi a única resposta por um longo tempo. A quantidade de votos no momento não é suficiente para representar a popularidade das diferentes respostas. A questão é como podemos conseguir que mais pessoas votem? Até eu votei em uma resposta.
Rolfツ
1
@Rolf ツ: Comecei uma recompensa no valor de 2/3 da reputação que ganhei com essa pergunta até agora. Espero que seja justo o suficiente.
Thomas Weller

Respostas:

29

Pitão

a função delrowsexclui todas as linhas, exceto uma duplicada, e retorna a imagem transposta, aplicando-a duas vezes também exclui as colunas e as transpõe de volta. thresholdControla adicionalmente quantos pixels podem diferir para que duas linhas ainda sejam consideradas iguais

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

insira a descrição da imagem aqui
insira a descrição da imagem aqui

Virar o comparador maskde >para <=produzirá as áreas removidas, que são principalmente espaços em branco.

insira a descrição da imagem aqui insira a descrição da imagem aqui

golfed (porque porque não)
Em vez de comparar cada pixel, ele olha apenas para a soma; como efeito colateral, isso também converte a captura de tela em escala de cinza e tem problemas com permutações de preservação de soma, como a seta para baixo na barra de endereço do Win8. captura de tela

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

insira a descrição da imagem aqui
insira a descrição da imagem aqui

DenDenDo
fonte
Uau, mesmo golfed ... (espero que estavam cientes de que este é um concurso de popularidade)
Thomas Weller
você se importaria de remover a pontuação do golfe? Isso pode deixar as pessoas pensando que isso é código de golfe. Obrigado.
Thomas Weller
1
@ThomasW. removeu o placar e o moveu para o fundo, fora da vista.
DenDenDo
15

Java: Tente sem perdas e fallback para reconhecimento de conteúdo

(Melhor resultado sem perdas até agora!)

Captura de tela do XP sem perdas, sem o tamanho desejado

Quando olhei pela primeira vez para essa pergunta, pensei que não era um quebra-cabeça ou um desafio, apenas alguém que precisava desesperadamente de um programa e seu código;) Mas é da minha natureza resolver problemas de visão, para que eu não possa parar de tentar esse desafio. !

Eu vim com a seguinte abordagem e combinação de algoritmos.

No pseudo-código, fica assim:

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:

  • Intensidade em escala de cinza
  • Dilatação
  • Pesquisa e remoção iguais de coluna
  • Costura
  • Detecção de borda sobel
  • Limiar

O programa

O programa pode cortar capturas de tela sem perda, mas tem uma opção de recorrer ao corte com reconhecimento de conteúdo, que não é 100% sem perda. Os argumentos do programa podem ser ajustados para obter melhores resultados.

Nota: O programa pode ser aprimorado de várias maneiras (não tenho muito tempo livre!)

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 tela do XP sem perdas sem tamanho desejado (compactação sem perdas máxima)

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

Resultado: 836 x 323

Captura de tela do XP sem perdas, sem o tamanho desejado


Captura de tela do XP para 800x600

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

Resultado: 800 x 600

O algoritmo sem perdas remove cerca de 155 linhas horizontais do que o algoritmo volta à remoção com reconhecimento de conteúdo, pelo que alguns artefatos podem ser vistos.

Captura de tela do XP para 800x600


Captura de tela do Windows 10 para 700x300

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

Resultado: 700 x 300

O algoritmo sem perdas remove 270 linhas horizontais. O algoritmo volta para a remoção com reconhecimento de conteúdo, que remove outras 29. Na vertical, apenas o algoritmo sem perdas é usado.

Captura de tela do Windows 10 para 700x300


Captura de tela do Windows 10 com reconhecimento de conteúdo para 400x200 (teste)

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

Resultado: 400 x 200

Este foi um teste para ver como a imagem resultante ficaria com o uso severo do recurso de reconhecimento de conteúdo. O resultado está fortemente danificado, mas não irreconhecível.

Captura de tela do Windows 10 com reconhecimento de conteúdo para 400x200 (teste)


Rolf ツ
fonte
A primeira saída não é completamente aparada. Tanto posso me truncado da direita
Optimizer
Isso porque os argumentos (do meu programa) dizem que ele não deve otimizá-lo mais longe do que 800 pixels :)
Rolfツ
Desde este popcon, você provavelmente deve mostrar os melhores resultados :)
Otimizador
Meu programa inicializa da mesma forma que a outra resposta, mas também possui uma função com reconhecimento de conteúdo para reduzir ainda mais a escala. Ele também tem a opção de cortar com a largura e altura desejadas (consulte a pergunta).
Rolfツ
3

C #, algoritmo como eu faria manualmente

Este é o meu primeiro programa de processamento de imagens e demorou um pouco para implementar com tudo isso LockBits, etc. Mas eu queria que fosse rápido (usando Parallel.For) para obter um feedback quase instantâneo.

Basicamente, meu algoritmo é baseado em observações sobre como eu removo pixels manualmente de uma captura de tela:

  • Estou começando pela borda direita, porque as chances são maiores de pixels não utilizados.
  • Defino um limite para a detecção de bordas para capturar os botões do sistema corretamente. Para a captura de tela do Windows 10, um limite de 48 pixels funciona bem.
  • Depois que a borda é detectada (marcada na cor vermelha abaixo), procuro pixels da mesma cor. Pego o número mínimo de pixels encontrado e o aplico a todas as linhas (violeta marcada).
  • Então, recomeço com a detecção de borda (marcada em vermelho), pixels da mesma cor (marcada em azul, depois verde e depois amarela) e assim por diante

No momento, faço apenas horizontalmente. O resultado vertical pode usar o mesmo algoritmo e operar em uma imagem girada em 90 °, portanto, em teoria, é possível.

Resultados

Esta é uma captura de tela do meu aplicativo com regiões detectadas:

Redimensionador de captura de tela sem perdas

E este é o resultado da captura de tela do Windows 10 e do limite de 48 pixels. A saída tem 681 pixels de largura. Infelizmente, não é perfeito (consulte "Downloads de pesquisa" e algumas das barras verticais das colunas).

Resultado do Windows 10, limite de 48 pixels

E outro com limite de 64 pixels (567 pixels de largura). Isso parece ainda melhor.

Resultado do Windows 10, limite de 64 pixels

Resultado geral da aplicação da rotação para cortar também de toda a parte inferior (567x304 pixels).

Resultado do Windows 10, limite de 64 pixels, girado

Para o Windows XP, eu precisava alterar um pouco o código, pois os pixels não são exatamente iguais. Estou aplicando um limite de similaridade de 8 (diferença no valor RGB). Observe alguns artefatos nas colunas.

Screenshot Lossizer Resizer com Windows XP screenshot carregado

Resultado do Windows XP

Código

Bem, minha primeira tentativa no processamento de imagens. Não parece muito bom, não é? Isso lista apenas o algoritmo principal, não a interface do usuário e nem a rotação 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
fonte
1
+1 Abordagem interessante, eu gosto! Seria divertido se alguns dos algoritmos postados aqui, como o meu e o seu, fossem combinados para obter melhores resultados. Edit: C # é um monstro para ler, nem sempre tenho certeza se algo é um campo ou uma função / getter com lógica.
Rolf #
1

Haskell, usando a remoção ingênua de linhas sequenciais duplicadas

Infelizmente, este módulo fornece apenas uma função com o tipo muito genérico Eq a => [[a]] -> [[a]], já que não tenho idéia de como editar arquivos de imagem em Haskell, no entanto, tenho certeza de que é possível transformar uma imagem PNG em um [[Color]]valor e eu imagino instance Eq Colorque seja facilmente definível.

A função em questão é 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

Explicação:

Nota: a : b significa elemento a prefixado para a lista do tipo dea , resultando em uma lista. Esta é a construção fundamental de listas. []denota a lista vazia.

Nota: a :: b significa aé do tipo b. Por exemplo, se a :: k, então (a : []) :: [k], onde [x]denota uma lista contendo itens do tipo x.
Isso significa que (:), sem argumentos :: a -> [a] -> [a],. O ->denota uma função de algo para algo.

O import Data.Listsimplesmente recebe algum trabalho algumas outras pessoas fizeram por nós e nos permite utilizar as suas funções sem reescrevê-los.

Primeiro, defina uma função nubSequential :: Eq a => [a] -> [a].
Esta função remove os elementos subseqüentes de uma lista que são idênticos.
Então nubSequential [1, 2, 2, 3] === [1, 2, 3],. Vamos agora abreviar esta função como nS.

Se nSaplicado a uma lista vazia, nada pode ser feito, e simplesmente retornamos uma lista vazia.

Se nSfor aplicado a uma lista com conteúdo, o processamento real poderá ser feito. Para isso, precisamos de uma segunda função, aqui em uma wherecláusula -, para usar a recursão, pois o nosso nSnão controla um elemento com o qual comparar.
Nós chamamos essa função g. Ele funciona comparando seu primeiro argumento com o cabeçalho da lista que foi dado e descartando o cabeçalho se eles corresponderem e se chamando de cauda com o antigo primeiro argumento. Caso contrário, ele anexa a cabeça à cauda, ​​passando por ela mesma com a cabeça como o novo primeiro argumento.
Para usar g, damos a ele a cabeça do argumento nSe a cauda como seus dois argumentos.

nSagora é do tipo Eq a => [a] -> [a], pegando uma lista e retornando uma lista. Exige que possamos verificar a igualdade entre os elementos, pois isso é feito na definição da função.

Em seguida, compomos as funções nSe transposeusamos o (.)operador.
Compor funções significa o seguinte: (f . g) x = f (g (x)).

Em nosso exemplo, transposegira uma tabela em 90 °, nSremove todos os elementos iguais seqüenciais da lista; nesse caso, outras listas (é o que é uma tabela), transposegira-a para trás e nSremove novamente os elementos iguais sequenciais. Isso basicamente remove as colunas e linhas duplicadas subsequentes.

Isso é possível porque, se é possível averificar a igualdade ( instance Eq a), também [a]é.
Em resumo:instance Eq a => Eq [a]

schuelermine
fonte