ロスレススクリーンショットリサイズが必要な場合があります


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
この質問は十分な注目を集めていません。最初の回答は長い間唯一の回答だったため、支持されました。現時点では、投票数はさまざまな回答の人気を表すには不十分です。問題は、どうすればもっと多くの人に投票してもらうかということです。私も答えに投票しました。
ロルフツ

1
@Rolfツ:私はこれまでこの質問から得た評判の2/3に相当する賞金を始めました。これで十分だと思います。
トーマスウェラー

回答:


29

Python

この関数はdelrows、1行を除くすべての重複行を削除し、転置画像を返します。2回適用すると、列も削除され、転置されます。さらにthreshold、2つのラインが同じと見なされるためにピクセル数が異なる場合があることを制御します

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

ここに画像の説明を入力してください
ここに画像の説明を入力してください

maskfromからコンパレータを反転>する<=と、代わりに、ほとんどが空白の削除された領域が出力されます。

ここに画像の説明を入力してください ここに画像の説明を入力してください

Golfed(理由はありません)
各ピクセルを比較する代わりに合計のみを見るため、これは副作用としてスクリーンショットをグレースケールに変換し、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')

ここに画像の説明を入力してください
ここに画像の説明を入力してください


うわー、ゴルフも...(これは人気コンテストであることを知っていたと思います)
トーマスウェラー

ゴルフのスコアを削除してもよろしいですか?これにより、人々はこれがコードゴルフであると考えるようになるかもしれません。ありがとうございました。
トーマスウェラー

1
しゅう スコアを削除し、見えないように底に移動しました。
-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);

使用されたテクニック:

  • 強度グレースケール
  • 膨張
  • 等しい列の検索と削除
  • シームカービング
  • ソーベルエッジ検出
  • しきい値処理

プログラム

このプログラムはスクリーンショットをロスレスでトリミングできますが、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" 800600 6 10 true 60

結果: 800 x 600

ロスレスアルゴリズムは、約155の水平線を削除しますが、一部のアーティファクトが見られるため、アルゴリズムはコンテンツ認識の削除にフォールバックします。

800x600へのXPスクリーンショット


700x300へのWindows 10スクリーンショット

引数: "image.png" 700 300 6 10 true 60

結果: 700 x 300

ロスレスアルゴリズムは270本の水平線を削除しますが、アルゴリズムはコンテンツに対応した削除にフォールバックし、29本を削除します。垂直ロスレスアルゴリズムのみが使用されます。

700x300へのWindows 10スクリーンショット


400x200(テスト)に対応したWindows 10スクリーンショット

引数: "image.png" 400 200 5 10 true 600

結果: 400 x 200

これは、結果認識された画像がコンテンツ認識機能の厳しい使用後にどのように見えるかを確認するためのテストでした。結果はひどく破損していますが、認識できません。

400x200(テスト)に対応したWindows 10スクリーンショット



最初の出力は完全にトリミングされていません。私は右から切り捨てることができます
オプティマイザー

(私のプログラムの)引数が800ピクセルを超えて最適化すべきでないと言っているからです:)
ロルフツ

このポプコン以来、おそらく最高の結果を表示する必要があります:)
オプティマイザー

私のプログラムは、他の答えと同じように初期設定を行いますが、さらにダウンスケーリングするためのコンテンツ認識機能も備えています。また、希望の幅と高さに切り抜くオプションもあります(質問を参照)。
ロルフツ

3

C#、私が手動で行うようなアルゴリズム

これは私の最初の画像処理プログラムであり、そのLockBitsようなものすべてを実装するのに時間がかかりましたParallel.For

基本的に、私のアルゴリズムは、スクリーンショットから手動でピクセルを削除する方法に関する観察に基づいています。

  • 未使用のピクセルが存在する可能性が高いため、右端から始めています。
  • システムボタンを正しくキャプチャするために、エッジ検出のしきい値を定義します。Windows 10スクリーンショットの場合、48ピクセルのしきい値が適切に機能します。
  • エッジが検出された後(下の赤色でマーク)、同じ色のピクセルを探しています。見つかった最小ピクセル数を取得し、それをすべての行に適用します(紫色でマークされています)。
  • 次に、エッジ検出(赤でマーク)、同じ色のピクセル(青でマーク、次に緑、次に黄色)などでやり直します。

現時点では、水平方向のみにしています。垂直方向の結果は同じアルゴリズムを使用し、90°回転した画像で動作するため、理論的には可能です。

結果

これは、検出された領域を含むアプリケーションのスクリーンショットです。

ロスレススクリーンショットリサイズ

これは、Windows 10スクリーンショットと48ピクセルのしきい値の結果です。出力の幅は681ピクセルです。残念ながら、それは完全ではありません(「ダウンロードの検索」およびいくつかの垂直列バーを参照)。

Windows 10の結果、48ピクセルのしきい値

もう1つは、しきい値が64ピクセル(幅567ピクセル)です。これはさらに良く見えます。

Windows 10の結果、64ピクセルのしきい値

すべての下からのトリミングにも回転を適用した全体的な結果(567x304ピクセル)。

Windows 10の結果、64ピクセルのしきい値、回転

Windows XPでは、ピクセルが正確に等しくないため、コードを少し変更する必要がありました。8(RGB値の差)の類似性しきい値を適用しています。列内のいくつかのアーティファクトに注意してください。

Windows XPスクリーンショットがロードされたLossless Screenshot 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#は読むべき怪物です。何かがフィールドなのか、ロジックを備えた関数/ゲッターなのかは常にわかりません。
ロルフツ

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、のタイプのリストの前にある要素を 意味し、リストになります。これはリストの基本的な構成です。空のリストを示します。aa[]

注: a :: b平均aのタイプはbです。たとえば、if a :: k、then (a : []) :: [k]、where [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、再帰を使用するための2番目の関数(ここでは-clause)が必要nSです。
この関数に名前を付けますg。最初の引数を与えられたリストの先頭と比較し、一致する場合は先頭を破棄し、古い最初の引数で末尾で自分自身を呼び出すことで機能します。そうでない場合、ヘッドをテールに追加し、新しい最初の引数としてヘッドを使用して自身を通過させます。
を使用するgにはnS、2つの引数として引数の先頭と末尾を指定します。

nS現在はtype Eq a => [a] -> [a]で、リストを取得してリストを返します。これは関数定義で行われるため、要素間の同等性をチェックできることが必要です。

次に、関数を構成し、演算子nStranspose使用し(.)ます。
関数の構成とは、次のことを意味します(f . g) x = f (g (x))

この例でtransposeは、テーブルを90度回転しnS、リストのすべての連続する等しい要素、この場合は他のリスト(テーブルと同じ)を削除しtranspose、元に戻し、nS再び連続する等しい要素を削除します。これは、本質的に後続の重複する行と列を削除することです。

if aが等式(instance Eq a)でチェック可能である場合、それも可能であるため、これが可能[a]です。
要するに:instance Eq a => Eq [a]

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.