有时我需要一个无损的截屏大小调整器


44

有时我需要编写更多的文档,而不仅仅是代码中的注释。有时,这些解释需要屏幕截图。有时,获得这样的屏幕截图的条件太奇怪了,以至于我要求开发人员为我拍摄屏幕截图。有时屏幕截图不符合我的规格,因此我必须调整其大小以使其看起来不错。

如您所见,需要魔术“ Lossless Screenshot Resizer”的可能性很小。无论如何,对我来说似乎每天都需要它。但是它还不存在。

我以前在PCG上见过您解决过很棒的图形难题,所以我想这对您来说很无聊...

规格

  • 该程序将单个窗口的屏幕截图作为输入
  • 屏幕截图不使用玻璃效果或类似效果(因此您不需要处理任何会发光的背景材料)
  • 输入文件格式为PNG(或其他任何无损格式,因此您不必处理压缩工件)
  • 输出文件格式与输入文件格式相同
  • 该程序将创建不同大小的屏幕截图作为输出。最低要求正在缩小。
  • 用户应指定预期的输出大小。如果您可以提供有关程序可以根据给定输入产生的最小大小的提示,那将很有帮助。
  • 如果人为解释,则输出屏幕截图的信息一定不能少。您不应删除文本或图像内容,而应仅删除具有背景的区域。请参阅下面的示例。
  • 如果无法获得预期的大小,程序应指出这一点,而不仅仅是崩溃或删除信息,而无需另行通知。
  • 如果程序指示出于验证原因将删除的区域,则应该增加其受欢迎程度。
  • 该程序可能需要一些其他用户输入,例如,标识优化的起点。

规则

这是一次人气竞赛。在2015-03-08票数最多的答案被接受。

例子

Windows XP屏幕截图。原始大小:1003x685像素。

XP屏幕截图大

可以删除示例区域(红色:垂直,黄色:水平),而不会丢失任何信息(文本或图像)。请注意,红色条形不是连续的。此示例并未指出所有可能被删除的像素。

XP屏幕截图删除指标

无损调整大小:783x424像素。

XP截图小

Windows 10屏幕截图。原始尺寸:999x593像素。

Windows 10屏幕截图大

可以删除的示例区域。

指示删除Windows 10屏幕截图

无损调整大小的屏幕截图:689x320像素。

请注意,标题文本(“下载”)和“此文件夹为空”也可以不再居中。当然,如果居中,那会更好,而且,如果您的解决方案能够做到这一点,它应该会越来越受欢迎。

Windows 10屏幕截图小


3
让我想起了Photoshop的“ 内容感知缩放 ”功能。
agtoever

输入什么格式。我们可以选择任何标准图像格式吗?
HEGX64

@ThomasW说:“我想这很无聊”。不对。这是恶魔般的。
逻辑骑士

1
这个问题没有引起足够的重视,第一个答案已经被推荐,因为这是很长一段时间以来唯一的答案。目前,票数还不足以代表不同答案的受欢迎程度。问题是我们如何才能使更多的人投票?甚至我都对答案投票。
罗尔夫(Rolf)2015年

1
@Rolfツ:到目前为止,我已经开始了悬赏,其价值是我从这个问题中获得的声誉的2/3。我希望这是公平的。
Thomas Weller

Answers:


29

蟒蛇

该函数将delrows删除除一个重复行以外的所有行,并返回转置后的图像,对其应用两次也将删除列并将其转回。另外threshold控制两条线可以有多少个像素可以不同,仍然认为相同

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

在此处输入图片说明
在此处输入图片说明

将比较器mask从翻转>到,<=将输出大部分是空白的已删除区域。

在此处输入图片说明 在此处输入图片说明

打高尔夫球(因为为什么不这样做),
而不是比较每个像素,而是只查看总和,因此,副作用还可以将屏幕截图转换为灰度,并且在保存总和排列方面遇到麻烦,例如Win8地址栏中的向下箭头屏幕截图

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

在此处输入图片说明
在此处输入图片说明


哇,甚至打高尔夫球……(我希望您知道这是一场人气比赛)
Thomas Weller

您介意消除高尔夫球成绩吗?这可能会使人们认为这是代码高尔夫。谢谢。
Thomas Weller

1
@ThomasW。删除分数并将其移到底部,看不见。
DenDenDo

15

Java:尝试无损并回退到内容感知

(迄今为止最好的无损结果!)

XP屏幕截图无损失,没有所需的大小

当我初次看这个问题时,我认为这不是一个难题或挑战,只是一个迫切需要程序和代码的人;)但是解决视觉问题是我的天性,因此我无法阻止自己尝试这个挑战!

我想出了以下方法和算法的组合。

用伪代码看起来像这样:

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

二手技术:

  • 强度灰度
  • 扩张
  • 相等的列搜索和删除
  • 接缝雕刻
  • Sobel边缘检测
  • 门槛

该程序

该程序可以无损地裁剪屏幕截图,但可以选择回退到内容感知裁剪,而并非100%无损。可以对程序的参数进行调整以获得更好的结果。

注意:该程序可以通过多种方式进行改进(我没有太多的空闲时间!)

争论

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

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

结果


XP屏幕截图无损失,没有所需的大小(最大无损压缩)

参数: “ image.png” 1 1 5 10 false 0

结果: 836 x 323

XP屏幕截图无损失,没有所需的大小


XP屏幕截图到800x600

参数: “ image.png” 800 600 6 10正确60

结果: 800 x 600

无损算法去除了大约155条水平线,而算法又退回到了内容感知的去除方法,因此可以看到一些伪像。

Xp屏幕截图到800x600


Windows 10屏幕截图到700x300

参数: “ image.png” 700 300 6 10是60

结果: 700 x 300

无损算法去除了270条水平线,而该算法又退回到了内容感知去除,后者又去除了29条。垂直方向仅使用了无损算法。

Windows 10屏幕截图到700x300


Windows 10屏幕快照内容感知到400x200(测试)

参数: “ image.png” 400200 5 10 true 600

结果: 400 x 200

这是一项测试,旨在查看在严重使用内容感知功能后生成的图像的外观。结果严重受损,但并非无法识别。

Windows 10屏幕快照内容感知到400x200(测试)



第一个输出未完全修剪。我能从右边截断太多
Optimizer

这是因为(我的程序的)自变量说它不应对其进行优化,除非超过800像素:)
Rolf 1515Feb

由于这个popcon,您可能应该显示最佳结果:)
Optimizer

我的程序的初始名称与其他答案相同,但是它还具有内容识别功能,甚至可以进一步缩小比例。它还可以选择裁剪到所需的宽度和高度(请参阅问题)。
罗尔夫(Rolf)2015年

3

C#,像我将手动执行的算法

这是我的第一个图像处理程序,花了一些时间来实现所有类似的LockBits东西。但是我希望它能快速(使用Parallel.For)来获得几乎即时的反馈。

基本上,我的算法基于对如何从屏幕截图中手动删除像素的观察:

  • 我从右边开始,因为出现未使用像素的机会更高。
  • 我定义了边缘检测的阈值,以便正确捕获系统按钮。对于Windows 10屏幕截图,阈值48像素效果很好。
  • 检测到边缘后(下面以红色标记),我正在寻找相同颜色的像素。我将找到的最小像素数应用于所有行(标记为紫色)。
  • 然后,我重新开始进行边缘检测(标记为红色),相同颜色的像素(标记为蓝色,绿色,然后黄色),依此类推

目前,我仅水平执行此操作。垂直结果可以使用相同的算法并对90°旋转的图像进行操作,因此从理论上讲是可能的。

结果

这是我的应用程序的屏幕截图,其中包含检测到的区域:

无损截图调整大小

这是Windows 10屏幕截图和48像素阈值的结果。输出为681像素宽。不幸的是,它并不完美(请参阅“搜索下载”和一些垂直的竖条)。

Windows 10结果,阈值48像素

另一个具有64像素阈值(567像素宽)。看起来更好。

Windows 10结果,阈值64像素

还将结果从所有底部应用到作物的旋转效果(567x304像素)。

Windows 10结果,阈值64像素,已旋转

对于Windows XP,由于像素不完全相等,因此需要稍微更改代码。我将相似度阈值设置为8(RGB值的差异)。注意列中的一些工件。

装有Windows XP截图的无损截图Resizer

Windows XP结果

好吧,我第一次尝试图像处理。看起来不是很好,不是吗?这仅列出了核心算法,没有列出UI,也没有列出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有趣的方法,我喜欢!如果将此处发布的某些算法(例如我的算法和您的算法)组合在一起以实现最佳结果,那将很有趣。编辑:C#是一个难以理解的怪物,我并不总是确定某个东西是字段还是带有逻辑的函数/获取器。
罗尔夫(Rolf)2015年

1

Haskell,使用天真的删除重复的顺序行

不幸的是,此模块仅提供具有非常通用类型的函数Eq a => [[a]] -> [[a]],因为我不知道如何在Haskell中编辑图像文件,但是,我敢肯定可以将PNG图像转换为[[Color]]值,并且我想instance Eq Color是容易定义。

有问题的功能是resizeL

码:

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

说明:

注意: a : b表示在类型list的前缀的元素 ,产生一个list。这是列表的基本构造。表示空白列表。aa[]

注意: a :: b手段a是类型b。例如,如果a :: k,则(a : []) :: [k],其中[x]表示包含type的列表x
这表示(:)本身没有任何参数:: a -> [a] -> [a]。该->表示从某事某物的功能。

import Data.List简单地得到了一些工作的一些其他人的为我们所做的,让我们用自己的功能,而无需重写它们。

首先,定义一个函数nubSequential :: Eq a => [a] -> [a]
此函数删除列表中相同的后续元素。
因此,nubSequential [1, 2, 2, 3] === [1, 2, 3]现在,我们将该功能缩写为nS

如果nS将应用于空列表,则无法执行任何操作,我们简单地返回一个空列表。

如果nS将其应用于具有内容的列表,则可以进行实际处理。为此,我们需要第二个函数(在- where子句中)使用递归,因为我们nS没有跟踪要比较的元素。
我们命名此功能g。它的工作原理是将其第一个参数与给出的列表的开头进行比较,如果匹配则丢弃该开头,并使用旧的第一个参数在结尾处进行调用。如果没有,它将头部附加到尾部,并以头部作为新的第一个参数通过头部。
要使用g,我们给它的参数的头部nS和尾部作为其两个参数。

nS现在为类型Eq a => [a] -> [a],接受列表并返回列表。它要求我们可以检查元素之间的相等性,因为这是在函数定义中完成的。

然后,我们组合函数nStranspose使用(.)运算符。
组成函数的含义如下:(f . g) x = f (g (x))

在我们的示例中,transpose将表格旋转90°,nS删除列表中所有顺序相等的元素,在本例中为其他列表(这就是表格),transpose向后旋转并nS再次移除顺序相等的元素。这实质上是删除后续重复的行和列。

这是可能的,因为如果a可以检查是否等于(instance Eq a),那么[a]也可以。
简而言之:instance Eq a => Eq [a]

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.