Bazen Kayıpsız Ekran Resizer'e ihtiyacım var


44

Bazen koddaki yorumlardan daha fazla belge yazmam gerekir. Ve bazen, bu açıklamaların ekran görüntülerine ihtiyacı var. Bazen böyle bir ekran görüntüsü almanın koşulları o kadar garip ki bir geliştiriciden benim için ekran görüntüsü almasını istiyorum. Bazen ekran görüntüsü belirtimlerime uymuyor ve düzgün görünmesi için yeniden boyutlandırmam gerekiyor.

Gördüğünüz gibi, sihir "Kayıpsız Ekran Görüntüsü Düzenleyici" için bir ihtiyaç şartı çok düşük. Neyse, benim için her gün ihtiyacım var gibi görünüyor. Ancak henüz mevcut değil.

Seni PCG'de daha önce harika grafik bulmacalar çözerken gördüm , sanırım bu senin için sıkıcı ...

Şartname

  • Program, giriş olarak tek bir pencerenin ekran görüntüsünü alır.
  • Ekran görüntüsü, cam efektlerinden veya benzerlerinden faydalanmıyor (bu nedenle, üzerinde parlayan arka planlarla uğraşmanıza gerek yok)
  • Giriş dosyası formatı PNG'dir (veya herhangi bir kayıpsız formattır, bu nedenle sıkıştırma eserleri ile uğraşmanıza gerek kalmaz)
  • Çıkış dosyası formatı giriş dosyası formatı ile aynıdır
  • Program, çıktı olarak farklı boyutta bir ekran görüntüsü oluşturur. Minimum gereksinim boyutunda küçülüyor.
  • Kullanıcı beklenen çıktı boyutunu belirtmelidir. Programınızın verilen girişten elde edebileceği asgari boyut hakkında ipuçları verebilirseniz, bu yararlı olacaktır.
  • Çıktı ekran görüntüsü bir insan tarafından yorumlanırsa daha az bilgiye sahip olmamalıdır. Metin veya resim içeriğini kaldırmazsınız, ancak yalnızca arka plana sahip alanları kaldırırsınız. Aşağıdaki örneklere bakınız.
  • Beklenen büyüklüğü elde etmek mümkün değilse, program bunu belirtmeli ve yalnızca önceden haber vermeksizin bilgileri kilitlememeli veya kaldırmamalıdır.
  • Program doğrulama nedenleriyle kaldırılacak alanları belirtirse, popülaritesi artacaktır.
  • Program, başka bir kullanıcı girişine ihtiyaç duyabilir, örneğin optimizasyon için başlangıç ​​noktasını belirlemek için.

kurallar

Bu bir popülerlik yarışması. 2015-03-08 tarihinde en çok oy alan cevap kabul edildi.

Örnekler

Windows XP ekran görüntüsü. Orijinal boyut: 1003x685 piksel.

XP ekran görüntüsü büyük

Herhangi bir bilgi (metin veya resim) kaybolmadan kaldırılabilecek örnek alanlar (kırmızı: dikey, sarı: yatay). Kırmızı çubuğun bitişik olmadığını unutmayın. Bu örnek, potansiyel olarak kaldırılabilecek tüm olası pikselleri göstermez.

XP ekran görüntüsü kaldırma göstergeleri

Kayıpsız yeniden boyutlandırıldı: 783x424 piksel.

XP ekran görüntüsü küçük

Windows 10 ekran görüntüsü. Orijinal boyut: 999x593 piksel.

Windows 10 ekran görüntüsü büyük

Kaldırılabilecek örnek alanlar.

Windows 10 ekran görüntüsü kaldırma belirtilen

Kayıpsız yeniden boyutlandırılmış ekran görüntüsü: 689x320 piksel.

Başlık metninin ("İndirilenler") ve "Bu klasör boş" un artık ortalanmamasının uygun olduğunu unutmayın. Tabi eğer merkezlenmişse daha iyi olur ve çözümünüz bunu sağlarsa daha popüler hale gelmesi gerekir.

Windows 10 ekran görüntüsü küçük


3
Photoshop'un " içeriğe duyarlı ölçeklendirme " özelliğini hatırlatıyor .
agtoever

Giriş hangi formatta? Herhangi bir standart görüntü formatını seçebilir miyiz?
HEGX64

@ThomasW, "Bu biraz sıkıcı sanırım" dedi. Doğru değil. Bu şeytanidir.
Mantık Şövalyesi

1
Bu soruya yeterince dikkat edilmiyor, ilk cevap kaldırıldı, çünkü uzun zamandır tek cevap buydu. Oyların sayısı şu anda farklı cevapların popülerliğini temsil etmek için yeterli değil. Sorun şu ki, daha fazla insanı nasıl oylayabiliriz? Ben bile bir cevaba oy verdim.
Rolf ツ

1
@Rolf ツ: Bu sorudan bugüne kadar kazandığım ünün 2 / 3'ü kadar bir lütuf aldım. Umarım bu yeterince adildir.
Thomas Weller

Yanıtlar:


29

piton

işlev, delrowsyinelenen bir satır hariç tümünü siler ve aktarılan resmi döndürür, iki kez uygulayarak da sütunları siler ve geri dönüştürür. Ek olarak threshold, iki çizginin kaç piksel için aynı olabileceğini hala kontrol eder.

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

görüntü tanımını buraya girin
görüntü tanımını buraya girin

Karşılaştırıcıda çevirme maskgelen >için <=daha çok boşluk vardır çıkarıldı alanları yerine çıkış olacaktır.

görüntü tanımını buraya girin görüntü tanımını buraya girin

golfed (çünkü neden olmasın)
Her pikseli karşılaştırmak yerine sadece toplamı gösterir, yan etki olarak ekran görüntüsünü gri tonlamaya dönüştürür ve Win8'nin adres çubuğundaki aşağı ok gibi toplam tutma izinleriyle ilgili sorun yaşar ekran görüntüsü

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

görüntü tanımını buraya girin
görüntü tanımını buraya girin


Vay, hatta golf oynadı ... (Umarım bunun bir popülerlik yarışması olduğunun farkındasındır)
Thomas Weller,

golf puanını kaldırabilir misin? Bu, insanları kod golf olduğunu düşünerek bırakabilir. Teşekkür ederim.
Thomas Weller

1
@ThomasW. skoru kaldı ve görüş alanından aşağı doğru kaydırdı.
DenDenDo

15

Java: Kayıpsız ve içeriğe duyarlı geri dönüşü deneyin

(Şu ana kadar en iyi kayıpsız sonuç!)

XP ekran görüntüsü istediğiniz boyutta kayıpsız

Bu soruya ilk baktığımda, bunun bir bilmeceye ya da çaresizce programa ihtiyaç duyan birine meydan okuma olmadığını düşündüm;) !

Aşağıdaki yaklaşım ve algoritmaların kombinasyonu ile geldi.

Sahte kodda şöyle görünür:

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

Kullanılan teknikler:

  • Yoğunluk gri tonlamalı
  • genişleme
  • Eşit sütun arama ve kaldırma
  • Derz oyma
  • Sobel kenarı tespiti
  • Eşik

Program

Program, ekran görüntülerini kayıpsız kırpabilir ancak% 100 kayıpsız olmayan içeriğe duyarlı kırpmaya geri dönme seçeneğine sahiptir. Programın argümanları daha iyi sonuçlar elde etmek için düzeltilebilir.

Not: Program birçok yönden geliştirilebilir (Çok fazla boş vaktim yok!)

Argümanlar

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

kod

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

Sonuçlar


XP ekran görüntüsü istediğiniz boyutta kayıpsız (Maks kayıpsız sıkıştırma)

Bağımsız Değişkenler: "image.png" 1 1 5 10 false 0

Sonuç: 836 x 323

XP ekran görüntüsü istediğiniz boyutta kayıpsız


XP ekran görüntüsünden 800x600'e

Bağımsız Değişkenler: "image.png" 800 600 6 10 true 60

Sonuç: 800 x 600

Kayıpsız algoritma, algoritmanın içeriğe duyarlı kaldırmaya geri döndüğünden, bazı eserler görülebilmesi için yaklaşık 155 yatay çizgiyi kaldırır.

Xp ekran görüntüsü ile 800x600


Windows 10 ekran görüntüsü ila 700x300

Bağımsız Değişkenler: "image.png" 700 300 6 10 true 60

Sonuç: 700 x 300

Kayıpsız algoritma, algoritmadan daha 270 yatay çizgiyi kaldırır, başka bir 29'u kaldıran içeriğe duyarlı kaldırmaya geri döner. Dikey yalnızca kayıpsız algoritma kullanılır.

Windows 10 ekran görüntüsü ila 700x300


Windows 10 ekran görüntüsü içeriği 400x200 (farkında) olan

Bağımsız Değişkenler: "image.png" 400 200 5 10 true 600

Sonuç: 400 x 200

Bu, sonuçtaki görüntünün, içeriğe duyarlı özelliğin şiddetli bir şekilde kullanımından sonra nasıl görüneceğini görmek için yapılan bir testti. Sonuç ağır hasar görmüştür ancak tanınmamaktadır.

Windows 10 ekran görüntüsü içeriği 400x200 (farkında) olan



İlk çıktı tamamen kesilmemiş. Bana çok sağdan kesilmiş olabilir
Doktor

Çünkü benim argümanlar (programımın) 800 pikselden daha fazla optimize etmemesi gerektiğini söylüyor :)
Rolf ツ

Bu popcon'dan beri, muhtemelen en iyi sonuçları göstermelisiniz :)
Optimizer

Programım başlangıçta diğer cevapla aynı şeyi yapıyor ancak daha da aşağı ölçeklendirme için içeriğe duyarlı bir işlevi var. Ayrıca istenen genişliğe ve yüksekliğe kadar kırpma seçeneğine sahiptir (soruya bakın).
Rolf ツ

3

C #, el ile yapacağı gibi algoritma

Bu benim ilk görüntü işleme programım ve tüm LockBitsbunlarla vb. Uygulamak için biraz zaman aldı . Ama Parallel.Forneredeyse anında geri bildirim almak için hızlı (kullanarak ) olmasını istedim .

Temel olarak algoritmam, ekran görüntüsünden pikselleri manuel olarak nasıl kaldırdığım konusundaki gözlemlere dayanıyor:

  • Ben sağ kenardan başlıyorum, çünkü kullanılmayan piksellerin var olma olasılığı daha yüksek.
  • Sistem düğmelerini doğru şekilde yakalamak için kenar algılama için bir eşik tanımlarım. Windows 10 ekran görüntüsü için, 48 piksel eşik değeri iyi çalışıyor.
  • Kenar algılandıktan sonra (aşağıda kırmızı renkle işaretli), aynı renkteki pikselleri arıyorum. Bulunan minimum piksel sayısını alıp tüm satırlara uygularım (menekşe işaretli).
  • Sonra tekrar kenar tespiti (kırmızı ile işaretlenmiş), aynı renkteki piksellerle (mavi, sonra yeşil, sonra sarı ve sonra) tekrar başlıyorum.

Şu anda sadece yatay yapıyorum. Dikey sonuç aynı algoritmayı kullanabilir ve 90 ° döndürülmüş bir görüntü üzerinde çalışabilir, bu nedenle teoride mümkündür.

Sonuçlar

Bu, uygulamamın tespit edilen bölgelere sahip bir ekran görüntüsüdür:

Kayıpsız Ekran Resizer

Ve bu, Windows 10 ekran görüntüsü ve 48 piksel eşiğinin sonucudur. Çıktı 681 piksel genişliğinde. Maalesef mükemmel değil (bkz. "İndirmeleri Ara" ve dikey sütun çubuklarından bazıları).

Windows 10 sonucu, 48 piksel eşik

64 piksel eşikli (567 piksel genişliğinde) bir tane daha. Bu daha iyi görünüyor.

Windows 10 sonucu, 64 piksel eşik

Tüm alttan da kırpmak için rotasyon uygulayarak genel sonuç (567x304 piksel).

Windows 10 sonucu, 64 piksel eşik, döndürülmüş

Windows XP'de, pikseller tam olarak eşit olmadığı için kodu biraz değiştirmem gerekiyordu. Ben 8 benzerlik eşiği (RGB değerindeki fark) uygulıyorum. Sütunlardaki bazı eserleri not alın.

Windows XP ekran görüntüsü yüklü Kayıpsız Ekran Görüntüsü Resizer

Windows XP sonucu

kod

Görüntü işleme konusundaki ilk denemem. Çok iyi görünmüyor, değil mi? Bu sadece UI ve 90 ° dönüşü değil, temel algoritmayı listeler.

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 İlginç yaklaşım, beğendim! Burada yayınlanan algoritmaların bazıları benimki ve seninki gibi en iyi sonuçları elde etmek için bir araya getirildiyse eğlenceli olur. Düzenleme: C # okumak için bir canavar, her zaman bir şey bir alan ya da mantık ile bir fonksiyon / alıcı olduğunu emin değilim.
Rolf ツ

1

Haskell, yinelenen ardışık çizgilerin saf çıkarılmasını kullanarak

Ne yazık ki, bu modül sadece çok genel türüne sahip bir fonksiyon sağlar Eq a => [[a]] -> [[a]]Haskell düzenlemek görüntü dosyaları, ancak, ben bir bir PNG görüntüsü Dönüşümü mümkün olduğuna eminim nasıl hiçbir fikrim yok çünkü, [[Color]]değer ve hayal ediyorum instance Eq Colorolmak kolayca tanımlanabilir.

Söz konusu işlev şudur resizeL.

Kod:

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çıklama:

Not: Bir liste halinde sonuçlanan, listeye ekli elemana : b anlamına gelir . Bu listelerin temel yapıdır. boş listeyi gösterir. aa[]

Not: a :: b anlamına gelir atürü b. Örneğin, eğer a :: köyleyse (a : []) :: [k], nerede [x]türünü içeren bir listeyi gösterir x.
Bu (:), herhangi bir argüman olmadan , kendisi anlamına gelir :: a -> [a] -> [a]. ->Şeye şeye bir işlev gösterir.

import Data.ListBasitçe diğer bazı insanlar bizim için yaptığı bazı işler alır ve bize bunları yeniden yazmak zorunda kalmadan görevlerini kullanmanızı sağlar.

İlk önce, bir fonksiyon tanımlayın nubSequential :: Eq a => [a] -> [a].
Bu işlev, aynı olan bir listenin sonraki öğelerini kaldırır.
Öyleyse nubSequential [1, 2, 2, 3] === [1, 2, 3]. Şimdi bu işlevi şu şekilde kısaltacağız nS.

Eğer nSboş bir listeye uygulandığında, hiçbir şey basit getirisi boş bir liste yapılır ve edilebilir.

Eğer nSiçeriği ile bir listeye uygulandığında, daha sonra gerçek işleme yapılabilir. Bunun için, burada where-şifre içinde, nSkarşılaştırılacak bir elementi takip etmediğimizden , özyineyi kullanmak için ikinci bir işleve ihtiyacımız var .
Bu işlevi adlandırıyoruz g. İlk argümanını verilen listenin başıyla karşılaştırarak ve kuyruğunda eski ilk argümanla eşleşip çağırarak başını atarak çalışır. Olmazsa, kafayı kuyruğun üzerine ekler, kafayla yeni ilk argüman olarak kendi içinden geçer.
Kullanmak için g, argümanın başına nSve iki argüman olarak kuyruğu veriyoruz .

nSşimdi bir tür Eq a => [a] -> [a]listeye alıyor ve bir listeye dönüyor. İşlev tanımında olduğu gibi, elemanlar arasındaki eşitliği kontrol etmemizi gerektirir.

Daha sonra, oluşturmak fonksiyonları nSve transposekullanma (.)operatörü.
Fonksiyonlarını Oluşturma şu demektir: (f . g) x = f (g (x)).

Örneğimizde, transposebir masayı 90 ° döndürür nS, listenin tüm ardışık eşit elemanlarını kaldırır, bu durumda diğer listeler (bir tablonun ne olduğu), transposeonu geri döndürür ve nStekrar ardışık eşit elemanları kaldırır. Bu, esas olarak müteakip kopya satırları bir sütunu kaldırmaktır.

Bu mümkündür, çünkü aeşitlik ( instance Eq a) için kontrol edilebilir ise , o zaman [a]da öyledir .
Kısacası:instance Eq a => Eq [a]

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.