Terkadang saya membutuhkan Resizer Screenshot Lossless

44

Terkadang saya perlu menulis lebih banyak dokumentasi dari sekadar komentar dalam kode. Dan terkadang, penjelasan itu membutuhkan tangkapan layar. Terkadang kondisi untuk mendapatkan tangkapan layar seperti itu sangat aneh sehingga saya meminta pengembang untuk mengambil tangkapan layar untuk saya. Terkadang tangkapan layar tidak sesuai dengan spesifikasi saya dan saya harus mengubah ukurannya agar terlihat bagus.

Seperti yang Anda lihat, keadaan untuk kebutuhan akan sihir "Lossless Screenshot Resizer" sangat tidak mungkin. Bagaimanapun, bagi saya sepertinya saya membutuhkannya setiap hari. Tapi itu belum ada.

Saya pernah melihat Anda di sini di PCG memecahkan teka-teki grafis yang mengagumkan sebelumnya, jadi saya kira ini agak membosankan untuk Anda ...

Spesifikasi

  • Program ini mengambil tangkapan layar dari satu jendela sebagai input
  • Tangkapan layar tidak menggunakan efek kaca atau sejenisnya (jadi Anda tidak perlu berurusan dengan hal-hal latar belakang yang menyinari)
  • Format file input adalah PNG (atau format lossless lainnya sehingga Anda tidak harus berurusan dengan artefak kompresi)
  • Format file output sama dengan format file input
  • Program ini membuat tangkapan layar dengan ukuran berbeda sebagai output. Persyaratan minimum menyusut ukurannya.
  • Pengguna harus menentukan ukuran output yang diharapkan. Jika Anda dapat memberikan petunjuk tentang ukuran minimum yang dapat dihasilkan oleh program Anda dari input yang diberikan, itu sangat membantu.
  • Tangkapan layar keluaran tidak boleh kurang informasi jika ditafsirkan oleh manusia. Anda tidak akan menghapus konten teks atau gambar, tetapi Anda harus menghapus area dengan latar belakang saja. Lihat contoh di bawah ini.
  • Jika tidak mungkin untuk mendapatkan ukuran yang diharapkan, program harus mengindikasikan hal itu dan tidak sekadar crash atau menghapus informasi tanpa pemberitahuan lebih lanjut.
  • Jika program menunjukkan area yang akan dihapus karena alasan verifikasi, itu akan meningkatkan popularitasnya.
  • Program mungkin memerlukan beberapa input pengguna lain, misalnya untuk mengidentifikasi titik awal untuk optimasi.

Aturan

Ini adalah kontes popularitas. Jawaban dengan suara terbanyak pada 2015-03-08 diterima.

Contohnya

Tangkapan layar Windows XP. Ukuran asli: 1003x685 piksel.

Tangkapan layar XP besar

Contoh area (merah: vertikal, kuning: horizontal) yang dapat dihapus tanpa kehilangan informasi (teks atau gambar). Perhatikan bahwa bilah merah tidak bersebelahan. Contoh ini tidak menunjukkan semua kemungkinan piksel yang berpotensi dihapus.

Indikator penghapusan tangkapan layar XP

Ubah ukuran lossless: 783x424 piksel.

Tangkapan layar XP kecil

Tangkapan layar Windows 10. Ukuran asli: 999x593 piksel.

Tangkapan layar Windows 10 besar

Contoh area yang bisa dihapus.

Penghapusan tangkapan layar Windows 10 diindikasikan

Tangkapan layar yang hilang ukurannya: 689x320 piksel.

Perhatikan bahwa boleh saja teks judul ("Unduhan") dan "Folder ini kosong" tidak lagi berada di tengah. Tentu saja, akan lebih baik jika dipusatkan, dan jika solusi Anda menyatakannya, itu akan menjadi lebih populer.

Tangkapan layar Windows 10 kecil

Thomas Weller
sumber
3
Mengingatkan saya pada fitur " content aware scaling " Photoshop .
agtoever
Apa format inputnya. Bisakah kita memilih format gambar standar?
HEGX64
@ Thomas mengatakan, "Saya kira yang ini agak membosankan". Tidak benar. Ini jahat.
Logic Knight
1
Pertanyaan ini tidak mendapat perhatian yang cukup, jawaban pertama telah terangkat karena itu adalah satu-satunya jawaban untuk waktu yang lama. Jumlah suara saat ini tidak cukup untuk mewakili popularitas jawaban yang berbeda. Pertanyaannya adalah bagaimana kita bisa mendapatkan lebih banyak orang untuk memilih? Bahkan saya memilih jawaban.
Rolf ツ
1
@Rolf ツ: Saya telah memulai hadiah senilai 2/3 dari reputasi yang saya dapatkan dari pertanyaan ini sejauh ini. Saya harap itu cukup adil.
Thomas Weller

Jawaban:

29

Python

fungsi ini delrowsmenghapus semua kecuali satu baris duplikat dan mengembalikan gambar yang ditransposisikan, menerapkannya dua kali juga menghapus kolom dan mentransposnya kembali. Selain itu thresholdmengontrol berapa banyak piksel yang dapat berbeda untuk dua baris yang masih dianggap sama

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

masukkan deskripsi gambar di sini
masukkan deskripsi gambar di sini

Membalik pembanding maskdari dari >ke <=sebaliknya akan menampilkan area yang dihapus yang sebagian besar ruang kosong.

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

golf (karena mengapa tidak)
Alih-alih membandingkan setiap piksel, ia hanya melihat jumlah, sebagai efek samping ini juga mengubah tangkapan layar menjadi skala abu-abu dan memiliki masalah dengan permutasi penjumlahan jumlah, seperti panah bawah di bilah alamat Win8 tangkapan layar

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

masukkan deskripsi gambar di sini
masukkan deskripsi gambar di sini

DenDenDo
sumber
Wow, bahkan bermain golf ... (Saya harap Anda sadar bahwa ini adalah kontes popularitas)
Thomas Weller
maukah Anda menghapus skor golf? Ini mungkin membuat orang berpikir ini adalah kode golf. Terima kasih.
Thomas Weller
1
@ Thomas. menghapus skor dan memindahkannya ke bawah, tidak terlihat.
DenDenDo
15

Java: Coba lossless dan mundur ke sadar konten

(Hasil lossless terbaik sejauh ini!)

Tangkapan layar XP lossless tanpa ukuran yang diinginkan

Ketika saya pertama kali melihat pertanyaan ini, saya pikir ini bukan teka-teki atau tantangan, hanya seseorang yang sangat membutuhkan program dan kode itu;) Tetapi sudah menjadi sifat saya untuk menyelesaikan masalah penglihatan sehingga saya tidak dapat menghentikan diri saya untuk mencoba tantangan ini. !

Saya datang dengan pendekatan dan kombinasi algoritma berikut.

Dalam pseudo-code tampilannya seperti ini:

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

Teknik yang digunakan:

  • Skala abu-abu intensitas
  • Pelebaran
  • Cari kolom yang sama dan hapus
  • Ukiran jahitan
  • Deteksi tepi sobel
  • Ambang batas

Program

Program ini dapat memotong tangkapan layar lossless tetapi memiliki opsi untuk mundur ke pemotongan konten-sadar yang tidak 100% lossless. Argumen dari program ini dapat disesuaikan untuk mencapai hasil yang lebih baik.

Catatan: Program ini dapat ditingkatkan dengan banyak cara (saya tidak punya banyak waktu luang!)

Argumen

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

Kode

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

Hasil


Tangkapan layar XP lossless tanpa ukuran yang diinginkan (Max lossless compression)

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

Hasil: 836 x 323

Tangkapan layar XP lossless tanpa ukuran yang diinginkan


Tangkapan layar XP hingga 800x600

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

Hasil: 800 x 600

Algoritma lossless menghilangkan sekitar 155 garis horizontal daripada algoritma kembali ke penghapusan konten-sadar sehingga beberapa artefak dapat dilihat.

Tangkapan layar Xp hingga 800x600


Tangkapan layar Windows 10 hingga 700x300

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

Hasil: 700 x 300

Algoritma lossless menghilangkan 270 garis horizontal daripada algoritma kembali ke penghapusan konten-sadar yang menghilangkan 29 lainnya. Vertikal hanya algoritma lossless yang digunakan.

Tangkapan layar Windows 10 hingga 700x300


Screenshot Windows 10 menyadari konten hingga 400x200 (uji)

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

Hasil: 400 x 200

Ini adalah tes untuk melihat bagaimana gambar yang dihasilkan akan terlihat setelah penggunaan fitur sadar konten yang parah. Hasilnya sangat rusak tetapi tidak bisa dikenali.

Screenshot Windows 10 menyadari konten hingga 400x200 (uji)


Rolf ツ
sumber
Output pertama tidak sepenuhnya dipangkas. Begitu banyak yang dapat saya potong dari kanan
Pengoptimal
Itu karena argumen (dari program saya) mengatakan bahwa itu seharusnya tidak mengoptimalkannya lebih jauh dari 800 piksel :)
Rolf ツ
Sejak popcon ini, Anda mungkin harus menunjukkan hasil terbaik :)
Pengoptimal
Program saya melakukan inisialisasi yang sama dengan jawaban lainnya tetapi juga memiliki fungsi yang sadar konten untuk penskalaan lebih jauh. Ia juga memiliki opsi untuk memotong ke lebar dan tinggi yang diinginkan (lihat pertanyaan).
Rolf ツ
3

C #, algoritma seperti saya akan melakukannya secara manual

Ini adalah program pemrosesan gambar pertama saya dan butuh beberapa saat untuk menerapkan dengan semua itu LockBitsdll. Tapi saya ingin cepat (menggunakan Parallel.For) untuk mendapatkan umpan balik yang hampir instan.

Pada dasarnya algoritma saya didasarkan pada pengamatan tentang cara menghapus piksel secara manual dari tangkapan layar:

  • Saya mulai dari tepi kanan, karena kemungkinan lebih tinggi bahwa piksel yang tidak digunakan ada di sana.
  • Saya menetapkan ambang batas untuk deteksi tepi untuk menangkap tombol sistem dengan benar. Untuk tangkapan layar Windows 10, ambang batas 48 piksel berfungsi dengan baik.
  • Setelah tepi terdeteksi (ditandai dengan warna merah di bawah), saya mencari piksel dengan warna yang sama. Saya mengambil jumlah piksel minimum yang ditemukan dan menerapkannya ke semua baris (ditandai violet).
  • Lalu saya mulai lagi dari deteksi tepi (ditandai merah), piksel dengan warna yang sama (ditandai biru, lalu hijau, lalu kuning) dan sebagainya

Saat ini saya melakukannya secara horizontal saja. Hasil vertikal dapat menggunakan algoritma yang sama dan beroperasi pada gambar yang diputar 90 °, jadi secara teori itu mungkin.

Hasil

Ini adalah tangkapan layar aplikasi saya dengan wilayah yang terdeteksi:

Resizer Screenshot Lossless

Dan ini adalah hasil tangkapan layar Windows 10 dan ambang 48 piksel. Outputnya adalah lebar 681 piksel. Sayangnya itu tidak sempurna (lihat "Cari Unduhan" dan beberapa bilah kolom vertikal).

Hasil Windows 10, ambang 48 piksel

Dan satu lagi dengan ambang 64 piksel (lebar 567 piksel). Ini terlihat lebih baik.

Hasil Windows 10, ambang 64 piksel

Hasil keseluruhan menerapkan rotasi untuk memotong dari semua bagian bawah juga (567x304 piksel).

Hasil Windows 10, ambang 64 piksel, diputar

Untuk Windows XP, saya perlu mengubah kode sedikit karena pikselnya tidak persis sama. Saya menerapkan ambang kesamaan 8 (perbedaan dalam nilai RGB). Perhatikan beberapa artefak di kolom.

Lossless Screenshot Resizer dengan tangkapan layar Windows XP dimuat

Hasil Windows XP

Kode

Nah, upaya pertama saya pada pemrosesan gambar. Tidak terlihat sangat bagus, bukan? Ini hanya mencantumkan algoritma inti, bukan UI dan bukan rotasi 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
sumber
1
+1 Pendekatan yang menarik, saya menyukainya! Akan menyenangkan jika beberapa algoritma yang diposting di sini, seperti milik saya dan milik Anda, akan digabungkan untuk mencapai hasil yang optimal. Sunting: C # adalah monster yang harus dibaca, saya tidak selalu yakin apakah sesuatu adalah bidang atau fungsi / pengambil dengan logika.
Rolf ツ
1

Haskell, menggunakan penghapusan naif dari garis sekuensial duplikat

Sayangnya, modul ini hanya menyediakan fungsi dengan tipe yang sangat umum Eq a => [[a]] -> [[a]], karena saya tidak tahu cara mengedit file gambar di Haskell, namun, saya yakin itu mungkin untuk mengubah gambar PNG ke [[Color]]nilai dan saya membayangkan instance Eq Colormenjadi mudah didefinisikan.

Fungsi yang dimaksud adalah resizeL.

Kode:

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

Penjelasan:

Catatan: a : b berarti elemen yang a diawali dengan daftar jenisa , menghasilkan daftar. Ini adalah konstruksi daftar yang mendasar. []menunjukkan daftar kosong.

Catatan: a :: b sarana aadalah tipe b. Misalnya, jika a :: k, kemudian (a : []) :: [k], di mana [x]menunjukkan daftar yang berisi hal-hal tipe x.
Ini berarti bahwa (:)itu sendiri, tanpa argumen :: a -> [a] -> [a],. The ->menunjukkan fungsi dari sesuatu untuk sesuatu.

The import Data.Listhanya mendapat beberapa pekerjaan beberapa orang lain lakukan untuk kita dan memungkinkan kita menggunakan fungsi mereka tanpa menulis ulang mereka.

Pertama, tentukan suatu fungsi nubSequential :: Eq a => [a] -> [a].
Fungsi ini menghilangkan elemen berikutnya dari daftar yang identik.
Jadi, nubSequential [1, 2, 2, 3] === [1, 2, 3]. Kami sekarang akan menyingkat fungsi ini sebagai nS.

Jika nSditerapkan ke daftar kosong, tidak ada yang bisa dilakukan, dan kami dengan mudah mengembalikan daftar kosong.

Jika nSditerapkan ke daftar dengan konten, maka pemrosesan sebenarnya dapat dilakukan. Untuk ini, kita membutuhkan fungsi kedua, di sini dalam where-klik, untuk menggunakan rekursi, karena kita nStidak melacak elemen untuk dibandingkan.
Kami beri nama fungsi ini g. Ini bekerja dengan membandingkan argumen pertama dengan kepala daftar yang telah diberikan, dan membuang kepala jika mereka cocok dan menyebut dirinya sendiri dengan argumen pertama yang lama. Jika tidak, itu menambahkan kepala ke ekor, melewati dirinya sendiri dengan kepala sebagai argumen pertama yang baru.
Untuk menggunakannya g, kami memberikannya kepala argumen nSdan ekor sebagai dua argumennya.

nSsekarang bertipe Eq a => [a] -> [a], mengambil daftar dan mengembalikan daftar. Ini mensyaratkan bahwa kita dapat memeriksa kesetaraan antara elemen karena hal ini dilakukan dalam definisi fungsi.

Kemudian, kami menyusun fungsi nSdan transposemenggunakan (.)operator.
Menyusun fungsi berarti berikut: (f . g) x = f (g (x)).

Dalam contoh kami, transposememutar tabel 90 °, nSmenghapus semua elemen yang sama berurutan dari daftar, dalam hal ini daftar lain (itulah tabel), transposememutarnya kembali dan nSlagi menghilangkan elemen sama berurutan. Ini pada dasarnya menghapus duplikat baris berikutnya kolom.

Ini dimungkinkan karena jika adapat diperiksa kesetaraan ( instance Eq a), maka [a]juga.
Pendeknya:instance Eq a => Eq [a]

schuelermine
sumber