บางครั้งฉันต้องการ Resizer Screenshot Lossless


44

บางครั้งฉันต้องเขียนเอกสารมากกว่าแค่แสดงความคิดเห็นในรหัส และบางครั้งคำอธิบายเหล่านั้นต้องการภาพหน้าจอ บางครั้งเงื่อนไขในการรับสกรีนช็อตนั้นแปลกมากฉันจึงขอให้ผู้พัฒนาถ่ายภาพหน้าจอให้ฉัน บางครั้งภาพหน้าจอไม่ตรงกับข้อกำหนดของฉันและฉันต้องปรับขนาดเพื่อให้มันดูดี

อย่างที่คุณเห็นสถานการณ์ที่จำเป็นสำหรับเวทมนตร์ "Lossless Screenshot Resizer" นั้นไม่น่าเป็นไปได้ อย่างไรก็ตามสำหรับฉันดูเหมือนว่าฉันต้องการมันทุกวัน แต่มันยังไม่มี

ฉันเคยเห็นคุณที่นี่ใน PCG แก้ปริศนากราฟิคที่ยอดเยี่ยมมาก่อนดังนั้นฉันคิดว่าเกมนี้น่าเบื่อสำหรับคุณ ...

สเปค

  • โปรแกรมจะจับภาพหน้าจอของหน้าต่างเดียวเป็นอินพุต
  • ภาพหน้าจอไม่ได้ใช้เอฟเฟกต์ของแก้วหรือสิ่งที่คล้ายกัน (ดังนั้นคุณไม่จำเป็นต้องจัดการกับสิ่งพื้นหลังใด ๆ
  • รูปแบบไฟล์อินพุตคือ PNG (หรือรูปแบบที่ไม่มีการสูญเสียอื่น ๆ เพื่อให้คุณไม่ต้องจัดการกับไฟล์บีบอัด)
  • รูปแบบไฟล์เอาต์พุตเหมือนกับรูปแบบไฟล์อินพุต
  • โปรแกรมสร้างภาพหน้าจอที่มีขนาดแตกต่างกันเป็นเอาต์พุต ความต้องการขั้นต่ำลดขนาดลง
  • ผู้ใช้จะต้องระบุขนาดผลลัพธ์ที่คาดหวัง หากคุณสามารถให้คำแนะนำเกี่ยวกับขนาดต่ำสุดที่โปรแกรมของคุณสามารถผลิตจากอินพุตที่กำหนดนั่นเป็นประโยชน์
  • ภาพหน้าจอของเอาต์พุตจะต้องมีข้อมูลน้อยกว่าหากตีความโดยมนุษย์ คุณจะไม่ลบเนื้อหาข้อความหรือรูปภาพ แต่คุณจะลบพื้นที่ที่มีพื้นหลังเท่านั้น ดูตัวอย่างด้านล่าง
  • หากไม่สามารถรับขนาดที่คาดหวังได้โปรแกรมควรระบุว่าไม่ใช่ความผิดพลาดหรือลบข้อมูลโดยไม่ต้องแจ้งให้ทราบล่วงหน้า
  • หากโปรแกรมระบุส่วนที่จะถูกลบเนื่องจากเหตุผลในการยืนยันควรเพิ่มความนิยม
  • โปรแกรมอาจต้องการอินพุตของผู้ใช้อื่นเช่นเพื่อระบุจุดเริ่มต้นสำหรับการปรับให้เหมาะสม

กฎระเบียบ

นี่คือการประกวดความนิยม คำตอบที่ได้คะแนนโหวตมากที่สุดเมื่อวันที่ 2015-03-08 ได้รับการตอบรับ

ตัวอย่าง

ภาพหน้าจอของ Windows XP ขนาดดั้งเดิม: 1003x685 พิกเซล

ภาพหน้าจอ XP ขนาดใหญ่

พื้นที่ตัวอย่าง (สีแดง: แนวตั้ง, สีเหลือง: แนวนอน) ที่สามารถลบได้โดยไม่สูญเสียข้อมูลใด ๆ (ข้อความหรือรูปภาพ) โปรดทราบว่าแถบสีแดงไม่ต่อเนื่องกัน ตัวอย่างนี้ไม่ได้ระบุพิกเซลที่เป็นไปได้ทั้งหมดที่อาจถูกลบออก

ตัวบ่งชี้การลบภาพหน้าจอ XP

ปรับขนาดแบบไม่สูญเสีย: 783x424 พิกเซล

ภาพหน้าจอ XP ขนาดเล็ก

ภาพหน้าจอของ Windows 10 ขนาดดั้งเดิม: 999x593 พิกเซล

ภาพหน้าจอขนาดใหญ่ของ Windows 10

พื้นที่ตัวอย่างที่สามารถลบได้

ระบุการลบภาพหน้าจอของ Windows 10

ภาพหน้าจอที่ปรับขนาดแบบ Losslessly: 689x320 พิกเซล

โปรดทราบว่ามันก็โอเคที่ข้อความชื่อ ("ดาวน์โหลด") และ "โฟลเดอร์นี้ว่างเปล่า" จะไม่อยู่กึ่งกลางอีกต่อไป แน่นอนว่ามันจะดีกว่าถ้าอยู่กึ่งกลางและหากโซลูชันของคุณให้สิ่งนั้นมันก็ควรจะเป็นที่นิยมมากขึ้น

ภาพหน้าจอของ Windows 10 เล็ก


3
เตือนฉันเกี่ยวกับคุณสมบัติ "การปรับขนาดเนื้อหาที่รับรู้ " ของ Photoshop
ตลอดไป

รูปแบบคืออินพุต เราสามารถเลือกรูปแบบภาพมาตรฐานได้ไหม?
HEGX64

@ThomasW พูดว่า "ฉันคิดว่าอันนี้น่าเบื่อ" ไม่จริง. นี่คือความโหดร้าย
Logic Knight อัศวิน

1
คำถามนี้ไม่ได้รับความสนใจเพียงพอคำตอบแรกถูกยกระดับขึ้นเพราะเป็นคำตอบเดียวเป็นเวลานาน จำนวนโหวตอยู่ในขณะนี้ไม่เพียงพอที่จะเป็นตัวแทนของความนิยมของคำตอบที่แตกต่างกัน คำถามคือเราจะให้ผู้คนลงคะแนนได้อย่างไร แม้ฉันจะลงคะแนนในคำตอบ
Rolf ツ

1
@Rolf ツ: ฉันได้เริ่มต้นเงินรางวัลมูลค่า 2/3 ของชื่อเสียงที่ฉันได้รับจากคำถามนี้จนถึงตอนนี้ ฉันหวังว่ามันยุติธรรมพอ
โทมัสเวลเลอร์

คำตอบ:


29

หลาม

ฟังก์ชั่นdelrowsจะลบทั้งหมดยกเว้นแถวที่ซ้ำกันหนึ่งแถวและส่งคืนอิมเมจที่ถูกนำไปใช้การใช้สองครั้งจะเป็นการลบคอลัมน์และสลับกลับไปมา นอกจากนี้thresholdควบคุมจำนวนพิกเซลที่สามารถแตกต่างกันสำหรับสองบรรทัดที่จะยังคงพิจารณาเหมือนกัน

from scipy import misc
from pylab import *

im7 = misc.imread('win7.png')
im8 = misc.imread('win8.png')

def delrows(im, threshold=0):
    d = diff(im, axis=0)
    mask = where(sum((d!=0), axis=(1,2))>threshold)
    return transpose(im[mask], (1,0,2))

imsave('crop7.png', delrows(delrows(im7)))
imsave('crop8.png', delrows(delrows(im8)))

ป้อนคำอธิบายรูปภาพที่นี่
ป้อนคำอธิบายรูปภาพที่นี่

การเปรียบเทียบตัวเปรียบเทียบในmaskจาก>เป็น<=แทนจะเอาท์พุทพื้นที่ที่ถูกลบซึ่งส่วนใหญ่เป็นพื้นที่ว่างเปล่าแทน

ป้อนคำอธิบายรูปภาพที่นี่ ป้อนคำอธิบายรูปภาพที่นี่

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

ป้อนคำอธิบายรูปภาพที่นี่
ป้อนคำอธิบายรูปภาพที่นี่


ว้าวแม้จะเล่นกอล์ฟ ... (ฉันหวังว่าคุณจะทราบว่านี่เป็นการประกวดความนิยม)
Thomas Weller

คุณอยากลบคะแนนกอล์ฟหรือไม่ นี่อาจทำให้ผู้คนคิดว่านี่คือรหัสกอล์ฟ ขอขอบคุณ.
โทมัสเวลเลอร์

1
@ThomasW ลบคะแนนและย้ายไปด้านล่างออกจากสายตา
DenDenDo

15

Java: ลอง lossless และ fallback เพื่อรับรู้เนื้อหา

(ผลลัพธ์แบบไม่สูญเสียที่ดีที่สุดจนถึงตอนนี้!)

ภาพหน้าจอของ XP ไม่สูญเสียขนาดที่ต้องการ

เมื่อฉันดูคำถามนี้ครั้งแรกฉันคิดว่านี่ไม่ใช่ปริศนาหรือความท้าทายมีเพียงบางคนที่ต้องการโปรแกรมและเป็นรหัส;) แต่ในธรรมชาติของฉันคือการแก้ปัญหาการมองเห็นดังนั้นฉันจึงไม่สามารถหยุดความท้าทายนี้ได้ !

ฉันคิดวิธีต่อไปนี้และการรวมกันของอัลกอริทึม

ในรหัสหลอกดูเหมือนว่านี้:

function crop(image, desired) {
    int sizeChange = 1;
    while(sizeChange != 0 and image.width > desired){

        Look for a repeating and connected set of lines (top to bottom) with a minimum of x lines
        Remove all the lines except for one
        sizeChange = image.width - newImage.width
        image = newImage;
    }
    if(image.width > desired){
        while(image.width > 2 and image.width > desired){
           Create a "pixel energy" map of the image
           Find the path from the top of the image to the bottom which "costs" the least amount of "energy"
           Remove the lowest cost path from the image
           image = newImage;
        }
    }
}

int desiredWidth = ?
int desiredHeight = ?
Image image = input;

crop(image, desiredWidth);
rotate(image, 90);
crop(image, desiredWidth);
rotate(image, -90);

เทคนิคที่ใช้:

  • ความเข้มระดับสีเทา
  • การขยาย
  • ค้นหาและลบคอลัมน์ที่เท่ากัน
  • ตะเข็บแกะสลัก
  • การตรวจจับขอบ Sobel
  • กำหนดเกณฑ์ขั้นต่ำ

โปรแกรม

โปรแกรมสามารถครอบตัดภาพหน้าจอแบบไม่สูญเสียได้ แต่มีตัวเลือกในการย้อนกลับไปสู่การปลูกพืชที่รับรู้เนื้อหาซึ่งไม่สูญเสีย 100% ข้อโต้แย้งของโปรแกรมสามารถปรับแต่งเพื่อให้ได้ผลลัพธ์ที่ดีกว่า

หมายเหตุ: โปรแกรมสามารถปรับปรุงได้หลายวิธี (ฉันไม่มีเวลาว่าง!)

ข้อโต้แย้ง

File name = file
Desired width = number > 0
Desired height = number > 0
Min slice width = number > 1
Compare threshold = number > 0
Use content aware = boolean
Max content aware cycles = number >= 0

รหัส

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JOptionPane;

/**
 * @author Rolf Smit
 * Share and adapt as you like, but don't forget to credit the author!
 */
public class MagicWindowCropper {

    public static void main(String[] args) {
        if(args.length != 7){
            throw new IllegalArgumentException("At least 7 arguments are required: (file, desiredWidth, desiredHeight, minSliceSize, sliceThreshold, forceRemove, maxForceRemove)!");
        }

        File file = new File(args[0]);

        int minSliceSize = Integer.parseInt(args[3]); //4;
        int desiredWidth = Integer.parseInt(args[1]); //400;
        int desiredHeight = Integer.parseInt(args[2]); //400;

        boolean forceRemove = Boolean.parseBoolean(args[5]); //true
        int maxForceRemove = Integer.parseInt(args[6]); //40

        MagicWindowCropper.MATCH_THRESHOLD = Integer.parseInt(args[4]); //3;

        try {

            BufferedImage result = ImageIO.read(file);

            System.out.println("Horizontal cropping");

            //Horizontal crop
            result = doDuplicateColumnsMagic(result, minSliceSize, desiredWidth);
            if (result.getWidth() != desiredWidth && forceRemove) {
                result = doSeamCarvingMagic(result, maxForceRemove, desiredWidth);
            }

            result = getRotatedBufferedImage(result, false);


            System.out.println("Vertical cropping");

            //Vertical crop
            result = doDuplicateColumnsMagic(result, minSliceSize, desiredHeight);
            if (result.getWidth() != desiredHeight && forceRemove) {
                result = doSeamCarvingMagic(result, maxForceRemove, desiredHeight);
            }

            result = getRotatedBufferedImage(result, true);

            showBufferedImage("Result", result);

            ImageIO.write(result, "png", getNewFileName(file));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static BufferedImage doSeamCarvingMagic(BufferedImage inputImage, int max, int desired) {
        System.out.println("Seam Carving magic:");

        int maxChange = Math.min(inputImage.getWidth() - desired, max);

        BufferedImage last = inputImage;
        int total = 0, change;
        do {
            int[][] energy = getPixelEnergyImage(last);
            BufferedImage out = removeLowestSeam(energy, last);

            change = last.getWidth() - out.getWidth();
            total += change;
            System.out.println("Carves removed: " + total);
            last = out;
        } while (change != 0 && total < maxChange);

        return last;
    }

    private static BufferedImage doDuplicateColumnsMagic(BufferedImage inputImage, int minSliceWidth, int desired) {
        System.out.println("Duplicate columns magic:");

        int maxChange = inputImage.getWidth() - desired;

        BufferedImage last = inputImage;
        int total = 0, change;
        do {
            BufferedImage out = removeDuplicateColumn(last, minSliceWidth, desired);

            change = last.getWidth() - out.getWidth();
            total += change;
            System.out.println("Columns removed: " + total);
            last = out;
        } while (change != 0 && total < maxChange);
        return last;
    }


    /*
     * Duplicate column methods
     */

    private static BufferedImage removeDuplicateColumn(BufferedImage inputImage, int minSliceWidth, int desiredWidth) {
        if (inputImage.getWidth() <= minSliceWidth) {
            throw new IllegalStateException("The image width is smaller than the minSliceWidth! What on earth are you trying to do?!");
        }

        int[] stamp = null;
        int sliceStart = -1, sliceEnd = -1;
        for (int x = 0; x < inputImage.getWidth() - minSliceWidth + 1; x++) {
            stamp = getHorizontalSliceStamp(inputImage, x, minSliceWidth);
            if (stamp != null) {
                sliceStart = x;
                sliceEnd = x + minSliceWidth - 1;
                break;
            }
        }

        if (stamp == null) {
            return inputImage;
        }

        BufferedImage out = deepCopyImage(inputImage);

        for (int x = sliceEnd + 1; x < inputImage.getWidth(); x++) {
            int[] row = getHorizontalSliceStamp(inputImage, x, 1);
            if (equalsRows(stamp, row)) {
                sliceEnd = x;
            } else {
                break;
            }
        }

        //Remove policy
        int canRemove = sliceEnd - (sliceStart + 1) + 1;
        int mayRemove = inputImage.getWidth() - desiredWidth;

        int dif = mayRemove - canRemove;
        if (dif < 0) {
            sliceEnd += dif;
        }

        int mustRemove = sliceEnd - (sliceStart + 1) + 1;
        if (mustRemove <= 0) {
            return out;
        }

        out = removeHorizontalRegion(out, sliceStart + 1, sliceEnd);
        out = removeLeft(out, out.getWidth() - mustRemove);
        return out;
    }

    private static BufferedImage removeHorizontalRegion(BufferedImage image, int startX, int endX) {
        int width = endX - startX + 1;

        if (endX + 1 > image.getWidth()) {
            endX = image.getWidth() - 1;
        }
        if (endX < startX) {
            throw new IllegalStateException("Invalid removal parameters! Wow this error message is genius!");
        }

        BufferedImage out = deepCopyImage(image);

        for (int x = endX + 1; x < image.getWidth(); x++) {
            for (int y = 0; y < image.getHeight(); y++) {
                out.setRGB(x - width, y, image.getRGB(x, y));
                out.setRGB(x, y, 0xFF000000);
            }
        }
        return out;
    }

    private static int[] getHorizontalSliceStamp(BufferedImage inputImage, int startX, int sliceWidth) {
        int[] initial = new int[inputImage.getHeight()];
        for (int y = 0; y < inputImage.getHeight(); y++) {
            initial[y] = inputImage.getRGB(startX, y);
        }
        if (sliceWidth == 1) {
            return initial;
        }
        for (int s = 1; s < sliceWidth; s++) {
            int[] row = new int[inputImage.getHeight()];
            for (int y = 0; y < inputImage.getHeight(); y++) {
                row[y] = inputImage.getRGB(startX + s, y);
            }

            if (!equalsRows(initial, row)) {
                return null;
            }
        }
        return initial;
    }

    private static int MATCH_THRESHOLD = 3;

    private static boolean equalsRows(int[] left, int[] right) {
        for (int i = 0; i < left.length; i++) {

            int rl = (left[i]) & 0xFF;
            int gl = (left[i] >> 8) & 0xFF;
            int bl = (left[i] >> 16) & 0xFF;

            int rr = (right[i]) & 0xFF;
            int gr = (right[i] >> 8) & 0xFF;
            int br = (right[i] >> 16) & 0xFF;

            if (Math.abs(rl - rr) > MATCH_THRESHOLD
                    || Math.abs(gl - gr) > MATCH_THRESHOLD
                    || Math.abs(bl - br) > MATCH_THRESHOLD) {
                return false;
            }
        }
        return true;
    }


    /*
     * Seam carving methods
     */

    private static BufferedImage removeLowestSeam(int[][] input, BufferedImage image) {
        int lowestValue = Integer.MAX_VALUE; //Integer overflow possible when image height grows!
        int lowestValueX = -1;

        // Here be dragons
        for (int x = 1; x < input.length - 1; x++) {
            int seamX = x;
            int value = input[x][0];
            for (int y = 1; y < input[x].length; y++) {
                if (seamX < 1) {
                    int top = input[seamX][y];
                    int right = input[seamX + 1][y];
                    if (top <= right) {
                        value += top;
                    } else {
                        seamX++;
                        value += right;
                    }
                } else if (seamX > input.length - 2) {
                    int top = input[seamX][y];
                    int left = input[seamX - 1][y];
                    if (top <= left) {
                        value += top;
                    } else {
                        seamX--;
                        value += left;
                    }
                } else {
                    int left = input[seamX - 1][y];
                    int top = input[seamX][y];
                    int right = input[seamX + 1][y];

                    if (top <= left && top <= right) {
                        value += top;
                    } else if (left <= top && left <= right) {
                        seamX--;
                        value += left;
                    } else {
                        seamX++;
                        value += right;
                    }
                }
            }
            if (value < lowestValue) {
                lowestValue = value;
                lowestValueX = x;
            }
        }

        BufferedImage out = deepCopyImage(image);

        int seamX = lowestValueX;
        shiftRow(out, seamX, 0);
        for (int y = 1; y < input[seamX].length; y++) {
            if (seamX < 1) {
                int top = input[seamX][y];
                int right = input[seamX + 1][y];
                if (top <= right) {
                    shiftRow(out, seamX, y);
                } else {
                    seamX++;
                    shiftRow(out, seamX, y);
                }
            } else if (seamX > input.length - 2) {
                int top = input[seamX][y];
                int left = input[seamX - 1][y];
                if (top <= left) {
                    shiftRow(out, seamX, y);
                } else {
                    seamX--;
                    shiftRow(out, seamX, y);
                }
            } else {
                int left = input[seamX - 1][y];
                int top = input[seamX][y];
                int right = input[seamX + 1][y];

                if (top <= left && top <= right) {
                    shiftRow(out, seamX, y);
                } else if (left <= top && left <= right) {
                    seamX--;
                    shiftRow(out, seamX, y);
                } else {
                    seamX++;
                    shiftRow(out, seamX, y);
                }
            }
        }

        return removeLeft(out, out.getWidth() - 1);
    }

    private static void shiftRow(BufferedImage image, int startX, int y) {
        for (int x = startX; x < image.getWidth() - 1; x++) {
            image.setRGB(x, y, image.getRGB(x + 1, y));
        }
    }

    private static int[][] getPixelEnergyImage(BufferedImage image) {

        // Convert Image to gray scale using the luminosity method and add extra
        // edges for the Sobel filter
        int[][] grayScale = new int[image.getWidth() + 2][image.getHeight() + 2];
        for (int x = 0; x < image.getWidth(); x++) {
            for (int y = 0; y < image.getHeight(); y++) {
                int rgb = image.getRGB(x, y);
                int r = (rgb >> 16) & 0xFF;
                int g = (rgb >> 8) & 0xFF;
                int b = (rgb & 0xFF);
                int luminosity = (int) (0.21 * r + 0.72 * g + 0.07 * b);
                grayScale[x + 1][y + 1] = luminosity;
            }
        }

        // Sobel edge detection
        final double[] kernelHorizontalEdges = new double[] { 1, 2, 1, 0, 0, 0, -1, -2, -1 };
        final double[] kernelVerticalEdges = new double[] { 1, 0, -1, 2, 0, -2, 1, 0, -1 };

        int[][] energyImage = new int[image.getWidth()][image.getHeight()];

        for (int x = 1; x < image.getWidth() + 1; x++) {
            for (int y = 1; y < image.getHeight() + 1; y++) {

                int k = 0;
                double horizontal = 0;
                for (int ky = -1; ky < 2; ky++) {
                    for (int kx = -1; kx < 2; kx++) {
                        horizontal += ((double) grayScale[x + kx][y + ky] * kernelHorizontalEdges[k]);
                        k++;
                    }
                }
                double vertical = 0;
                k = 0;
                for (int ky = -1; ky < 2; ky++) {
                    for (int kx = -1; kx < 2; kx++) {
                        vertical += ((double) grayScale[x + kx][y + ky] * kernelVerticalEdges[k]);
                        k++;
                    }
                }

                if (Math.sqrt(horizontal * horizontal + vertical * vertical) > 127) {
                    energyImage[x - 1][y - 1] = 255;
                } else {
                    energyImage[x - 1][y - 1] = 0;
                }
            }
        }

        //Dilate the edge detected image a few times for better seaming results
        //Current value is just 1...
        for (int i = 0; i < 1; i++) {
            dilateImage(energyImage);
        }
        return energyImage;
    }

    private static void dilateImage(int[][] image) {
        for (int x = 0; x < image.length; x++) {
            for (int y = 0; y < image[x].length; y++) {
                if (image[x][y] == 255) {
                    if (x > 0 && image[x - 1][y] == 0) {
                        image[x - 1][y] = 2; //Note: 2 is just a placeholder value
                    }
                    if (y > 0 && image[x][y - 1] == 0) {
                        image[x][y - 1] = 2;
                    }
                    if (x + 1 < image.length && image[x + 1][y] == 0) {
                        image[x + 1][y] = 2;
                    }
                    if (y + 1 < image[x].length && image[x][y + 1] == 0) {
                        image[x][y + 1] = 2;
                    }
                }
            }
        }
        for (int x = 0; x < image.length; x++) {
            for (int y = 0; y < image[x].length; y++) {
                if (image[x][y] == 2) {
                    image[x][y] = 255;
                }
            }
        }
    }

    /*
     * Utilities
     */

    private static void showBufferedImage(String windowTitle, BufferedImage image) {
        JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(image)), windowTitle, JOptionPane.PLAIN_MESSAGE, null);
    }

    private static BufferedImage deepCopyImage(BufferedImage input) {
        ColorModel cm = input.getColorModel();
        return new BufferedImage(cm, input.copyData(null), cm.isAlphaPremultiplied(), null);
    }

    private static final BufferedImage getRotatedBufferedImage(BufferedImage img, boolean back) {
        double oldW = img.getWidth(), oldH = img.getHeight();
        double newW = img.getHeight(), newH = img.getWidth();

        BufferedImage out = new BufferedImage((int) newW, (int) newH, img.getType());
        Graphics2D g = out.createGraphics();
        g.translate((newW - oldW) / 2.0, (newH - oldH) / 2.0);
        g.rotate(Math.toRadians(back ? -90 : 90), oldW / 2.0, oldH / 2.0);
        g.drawRenderedImage(img, null);
        g.dispose();
        return out;
    }

    private static BufferedImage removeLeft(BufferedImage image, int startX) {
        int removeWidth = image.getWidth() - startX;

        BufferedImage out = new BufferedImage(image.getWidth() - removeWidth,
                image.getHeight(), image.getType());

        for (int x = 0; x < startX; x++) {
            for (int y = 0; y < out.getHeight(); y++) {
                out.setRGB(x, y, image.getRGB(x, y));
            }
        }
        return out;
    }

    private static File getNewFileName(File in) {
        String name = in.getName();
        int i = name.lastIndexOf(".");
        if (i != -1) {
            String ext = name.substring(i);
            String n = name.substring(0, i);
            return new File(in.getParentFile(), n + "-cropped" + ext);
        } else {
            return new File(in.getParentFile(), name + "-cropped");
        }
    }
}

ผล


ภาพหน้าจอ XP แบบไม่สูญเสียขนาดที่ต้องการ (การบีบอัดข้อมูลแบบไม่สูญเสียสูงสุด)

อาร์กิวเมนต์: "image.png" 1 1 5 10 เท็จ 0

ผลลัพธ์: 836 x 323

ภาพหน้าจอของ XP ไม่สูญเสียขนาดที่ต้องการ


ภาพหน้าจอ XP เป็น 800x600

อาร์กิวเมนต์: "image.png" 800 600 6 10 จริง 60

ผลลัพธ์: 800 x 600

อัลกอริธึมแบบ lossless ลบเส้นแนวนอนประมาณ 155 เส้นกว่าอัลกอริทึมจะย้อนกลับไปยังการลบแบบรับรู้เนื้อหาเพื่อให้เห็นบางสิ่งประดิษฐ์

สกรีนช็อต Xp เป็น 800x600


ภาพหน้าจอของ Windows 10 ถึง 700x300

อาร์กิวเมนต์: "image.png" 700 300 6 10 จริง 60

ผลลัพธ์: 700 x 300

อัลกอริธึมแบบ lossless จะลบเส้นแนวนอน 270 เส้นกว่าอัลกอริทึมจะย้อนกลับไปยังการลบแบบรับรู้เนื้อหาซึ่งจะลบอีก 29 เส้นแนวตั้งใช้อัลกอริธึมแบบ lossless เท่านั้น

ภาพหน้าจอของ Windows 10 ถึง 700x300


ภาพหน้าจอของ Windows 10 รับรู้เนื้อหาถึง 400x200 (ทดสอบ)

อาร์กิวเมนต์: "image.png" 400 200 5 10 จริง 600

ผลลัพธ์: 400 x 200

นี่คือการทดสอบเพื่อดูว่าภาพที่ได้จะดูแลการใช้คุณสมบัติการรับรู้เนื้อหาอย่างรุนแรงอย่างไร ผลลัพธ์เสียหายอย่างมาก แต่ไม่สามารถจดจำได้

ภาพหน้าจอของ Windows 10 รับรู้เนื้อหาถึง 400x200 (ทดสอบ)



เอาต์พุตแรกไม่ได้ถูกตัดแต่งอย่างสมบูรณ์ ฉันสามารถตัดปลายได้มากจากที่ถูกต้อง
เครื่องมือเพิ่มประสิทธิภาพ

นั่นเพราะการขัดแย้ง (จากโปรแกรมของฉัน) บอกว่ามันไม่ควรจะเพิ่มประสิทธิภาพของมันเพิ่มเติมใด ๆ กว่า 800 พิกเซล :)
Rolfツ

เนื่องจาก popcon นี้คุณน่าจะแสดงผลลัพธ์ที่ดีที่สุด :)
เครื่องมือเพิ่มประสิทธิภาพ

โปรแกรมของฉันเริ่มต้นเช่นเดียวกับคำตอบอื่น ๆ แต่ก็มีฟังก์ชั่นการรับรู้เนื้อหาเพื่อลดขนาดลง นอกจากนี้ยังมีตัวเลือกในการครอบตัดตามความกว้างและความสูงที่ต้องการ (ดูคำถาม)
Rolf ツ

3

C # อัลกอริทึมเหมือนฉันจะทำด้วยตนเอง

นี่เป็นโปรแกรมประมวลผลภาพครั้งแรกของฉันและใช้เวลาสักครู่ในการปรับใช้กับทุกLockBitsสิ่งนั้นแต่ฉันต้องการให้มันเร็ว (ใช้Parallel.For) เพื่อรับข้อเสนอแนะเกือบจะทันที

โดยทั่วไปอัลกอริทึมของฉันขึ้นอยู่กับการสังเกตว่าฉันลบพิกเซลออกจากหน้าจอด้วยตนเองได้อย่างไร:

  • ฉันเริ่มต้นจากขอบขวาเนื่องจากมีโอกาสสูงกว่าที่มีพิกเซลที่ไม่ได้ใช้งาน
  • ฉันกำหนดเกณฑ์สำหรับการตรวจจับขอบเพื่อจับปุ่มระบบอย่างถูกต้อง สำหรับภาพหน้าจอของ Windows 10 ขีด จำกัด ของ 48 พิกเซลทำงานได้ดี
  • หลังจากตรวจพบขอบ (ทำเครื่องหมายด้วยสีแดงด้านล่าง) ฉันกำลังมองหาพิกเซลที่มีสีเดียวกัน ฉันใช้จำนวนพิกเซลขั้นต่ำที่พบและนำไปใช้กับทุกแถว (ทำเครื่องหมายสีม่วง)
  • จากนั้นฉันก็เริ่มต้นใหม่อีกครั้งด้วยการตรวจจับขอบ (ทำเครื่องหมายสีแดง), พิกเซลที่มีสีเดียวกัน (ทำเครื่องหมายสีน้ำเงิน, จากนั้นสีเขียว, จากนั้นสีเหลือง) และอื่น ๆ

ในขณะนี้ฉันทำในแนวนอนเท่านั้น ผลลัพธ์ในแนวตั้งสามารถใช้อัลกอริธึมเดียวกันและทำงานกับภาพที่หมุนได้ 90 °ดังนั้นในทางทฤษฎีแล้วมันเป็นไปได้

ผล

นี่คือภาพหน้าจอของแอปพลิเคชันของฉันที่ตรวจพบภูมิภาค:

Resizer หน้าจอแบบไม่สูญเสีย

และนี่คือผลลัพธ์สำหรับหน้าจอ Windows 10 และขีด จำกัด 48 พิกเซล ผลลัพธ์มีความกว้าง 681 พิกเซล น่าเสียดายที่มันไม่สมบูรณ์ (ดู "ค้นหาดาวน์โหลด" และแถบคอลัมน์แนวตั้งบางส่วน)

ผลลัพธ์ Windows 10 ขีด จำกัด 48 พิกเซล

และอีกอันที่มีขีด จำกัด 64 พิกเซล (กว้าง 567 พิกเซล) สิ่งนี้ดูดียิ่งขึ้น

ผลลัพธ์ Windows 10 ขีด จำกัด 64 พิกเซล

ผลโดยรวมใช้การหมุนเพื่อครอบตัดจากด้านล่างทั้งหมดเช่นกัน (567x304 พิกเซล)

ผลลัพธ์ Windows 10, ขีด จำกัด 64 พิกเซล, หมุนได้

สำหรับ Windows XP ฉันต้องเปลี่ยนรหัสเล็กน้อยเนื่องจากพิกเซลไม่เท่ากัน ฉันใช้เกณฑ์ความคล้ายคลึงกันที่ 8 (ความแตกต่างในค่า RGB) หมายเหตุสิ่งประดิษฐ์บางอย่างในคอลัมน์

โปรแกรมลดขนาดหน้าจอแบบ Lossless พร้อมโหลดภาพหน้าจอของ Windows XP

ผลลัพธ์ Windows XP

รหัส

ความพยายามครั้งแรกของฉันในการประมวลผลภาพ ดูไม่ดีเลยใช่ไหม นี่เป็นเพียงรายการอัลกอริธึมหลักไม่ใช่ UI และไม่ใช่การหมุน 90 °

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;

namespace LosslessScreenshotResizer.BL
{
    internal class PixelAreaSearcher
    {
        private readonly Bitmap _originalImage;

        private readonly int _edgeThreshold;
        readonly Color _edgeColor = Color.FromArgb(128, 255, 0, 0);
        readonly Color[] _iterationIndicatorColors =
        {
            Color.FromArgb(128, 0, 0, 255), 
            Color.FromArgb(128, 0, 255, 255), 
            Color.FromArgb(128, 0, 255, 0),
            Color.FromArgb(128, 255, 255, 0)
        };

        public PixelAreaSearcher(Bitmap originalImage, int edgeThreshold)
        {
            _originalImage = originalImage;
            _edgeThreshold = edgeThreshold;

            // cache width and height. Also need to do that because of some GDI exceptions during LockBits
            _imageWidth = _originalImage.Width;
            _imageHeight = _originalImage.Height;            
        }

        public Bitmap SearchHorizontal()
        {
            return Search();
        }

        /// <summary>
        /// Find areas of pixels to keep and to remove. You can get that information via <see cref="PixelAreas"/>.
        /// The result of this operation is a bitmap of the original picture with an overlay of the areas found.
        /// </summary>
        /// <returns></returns>
        private unsafe Bitmap Search()
        {
            // FastBitmap is a wrapper around Bitmap with LockBits enabled for fast operation.
            var input = new FastBitmap(_originalImage);
            // transparent overlay
            var overlay = new FastBitmap(_originalImage.Width, _originalImage.Height);

            _pixelAreas = new List<PixelArea>(); // save the raw data for later so that the image can be cropped
            int startCoordinate = _imageWidth - 1; // start at the right edge
            int iteration = 0; // remember the iteration to apply different colors
            int minimum;
            do
            {
                var indicatorColor = GetIterationColor(iteration);

                // Detect the edge which is not removable
                var edgeStartCoordinates = new PixelArea(_imageHeight) {AreaType = AreaType.Keep};
                Parallel.For(0, _imageHeight, y =>
                {
                    edgeStartCoordinates[y] = DetectEdge(input, y, overlay, _edgeColor, startCoordinate);
                }
                    );
                _pixelAreas.Add(edgeStartCoordinates);

                // Calculate how many pixels can theoretically be removed per line
                var removable = new PixelArea(_imageHeight) {AreaType = AreaType.Dummy};
                Parallel.For(0, _imageHeight, y =>
                {
                    removable[y] = CountRemovablePixels(input, y, edgeStartCoordinates[y]);
                }
                    );

                // Calculate the practical limit
                // We can only remove the same amount of pixels per line, otherwise we get a non-rectangular image
                minimum = removable.Minimum;
                Debug.WriteLine("Can remove {0} pixels", minimum);

                // Apply the practical limit: calculate the start coordinates of removable areas
                var removeStartCoordinates = new PixelArea(_imageHeight) { AreaType = AreaType.Remove };
                removeStartCoordinates.Width = minimum;
                for (int y = 0; y < _imageHeight; y++) removeStartCoordinates[y] = edgeStartCoordinates[y] - minimum;
                _pixelAreas.Add(removeStartCoordinates);

                // Paint the practical limit onto the overlay for demo purposes
                Parallel.For(0, _imageHeight, y =>
                {
                    PaintRemovableArea(y, overlay, indicatorColor, minimum, removeStartCoordinates[y]);
                }
                    );

                // Move the left edge before starting over
                startCoordinate = removeStartCoordinates.Minimum;
                var remaining = new PixelArea(_imageHeight) { AreaType = AreaType.Keep };
                for (int y = 0; y < _imageHeight; y++) remaining[y] = startCoordinate;
                _pixelAreas.Add(remaining);

                iteration++;
            } while (minimum > 1);


            input.GetBitmap(); // TODO HACK: release Lockbits on the original image 
            return overlay.GetBitmap();
        }

        private Color GetIterationColor(int iteration)
        {
            return _iterationIndicatorColors[iteration%_iterationIndicatorColors.Count()];
        }

        /// <summary>
        /// Find a minimum number of contiguous pixels from the right side of the image. Everything behind that is an edge.
        /// </summary>
        /// <param name="input">Input image to get pixel data from</param>
        /// <param name="y">The row to be analyzed</param>
        /// <param name="output">Output overlay image to draw the edge on</param>
        /// <param name="edgeColor">Color for drawing the edge</param>
        /// <param name="startCoordinate">Start coordinate, defining the maximum X</param>
        /// <returns>X coordinate where the edge starts</returns>
        private int DetectEdge(FastBitmap input, int y, FastBitmap output, Color edgeColor, int startCoordinate)
        {
            var repeatCount = 0;
            var lastColor = Color.DodgerBlue;
            int x;

            for (x = startCoordinate; x >= 0; x--)
            {
                var currentColor = input.GetPixel(x, y);
                if (almostEquals(lastColor,currentColor))
                {
                    repeatCount++;
                }
                else
                {
                    lastColor = currentColor;
                    repeatCount = 0;
                    for (int i = x; i < startCoordinate; i++)
                    {
                        output.SetPixel(i,y,edgeColor);
                    }
                }

                if (repeatCount > _edgeThreshold)
                {
                    return x + _edgeThreshold;
                }
            }
            return repeatCount;
        }

        /// <summary>
        /// Counts the number of contiguous pixels in a row, starting on the right and going to the left
        /// </summary>
        /// <param name="input">Input image to get pixels from</param>
        /// <param name="y">The current row</param>
        /// <param name="startingCoordinate">X coordinate to start from</param>
        /// <returns>Number of equal pixels found</returns>
        private int CountRemovablePixels(FastBitmap input, int y, int startingCoordinate)
        {
            var lastColor = input.GetPixel(startingCoordinate, y);
            for (int x=startingCoordinate; x >= 0; x--)
            {
                var currentColor = input.GetPixel(x, y);
                if (!almostEquals(currentColor,lastColor)) 
                {
                    return startingCoordinate-x; 
                }
            }
            return startingCoordinate;
        }

        /// <summary>
        /// Calculates color equality.
        /// Workaround for Windows XP screenshots which do not have 100% equal pixels.
        /// </summary>
        /// <returns>True if the RBG value is similar (maximum R+G+B difference is 8)</returns>
        private bool almostEquals(Color c1, Color c2)
        {
            int r = c1.R;
            int g = c1.G;
            int b = c1.B;
            int diff = (Math.Abs(r - c2.R) + Math.Abs(g - c2.G) + Math.Abs(b - c2.B));
            return (diff < 8) ;
        }

        /// <summary>
        /// Paint pixels that can be removed, starting at the X coordinate and painting to the right
        /// </summary>
        /// <param name="y">The current row</param>
        /// <param name="output">Overlay output image to draw on</param>
        /// <param name="removableColor">Color to use for drawing</param>
        /// <param name="width">Number of pixels that can be removed</param>
        /// <param name="start">Starting coordinate to begin drawing</param>
        private void PaintRemovableArea(int y, FastBitmap output, Color removableColor, int width, int start)
        {
            for(int i=start;i<start+width;i++)
            {
                output.SetPixel(i, y, removableColor);
            }
        }

        private readonly int _imageHeight;
        private readonly int _imageWidth;
        private List<PixelArea> _pixelAreas;

        public List<PixelArea> PixelAreas
        {
            get { return _pixelAreas; }
        }
    }
}

1
+1 วิธีที่น่าสนใจฉันชอบมัน! มันจะสนุกถ้าอัลกอริทึมบางอย่างที่โพสต์ไว้ที่นี่เช่นของฉันและของคุณจะถูกรวมเข้าด้วยกันเพื่อให้ได้ผลลัพธ์ที่ดีที่สุด แก้ไข: C # เป็นสัตว์ประหลาดที่จะอ่านฉันไม่แน่ใจเสมอว่ามีบางสิ่งเป็นฟิลด์หรือฟังก์ชัน / ทะเยอทะยานด้วยตรรกะ
Rolf ツ

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หมายถึงองค์ประกอบ aนำหน้าไปยังรายการประเภทของaผลในรายการ นี่คือโครงสร้างพื้นฐานของรายการ []หมายถึงรายการที่ว่างเปล่า

หมายเหตุ: a :: bวิธีเป็นประเภทa bตัวอย่างเช่นถ้าa :: kแล้ว(a : []) :: [k]ซึ่งหมายถึงรายการที่มีสิ่งที่ประเภท[x] ที่นี้หมายถึงว่าตัวเองโดยไม่ขัดแย้งใด ๆ หมายถึงฟังก์ชั่นจากบางสิ่งบางอย่างบางสิ่งบางอย่างx
(:):: a -> [a] -> [a]->

import Data.Listเพียงแค่ได้รับการทำงานบางอย่างบางคนอื่น ๆ ไม่ได้สำหรับเราและช่วยให้เราใช้ฟังก์ชั่นของพวกเขาโดยไม่ต้องเขียนใหม่ให้พวกเขา

nubSequential :: Eq a => [a] -> [a]ครั้งแรกกำหนดฟังก์ชัน
ฟังก์ชั่นนี้จะลบองค์ประกอบที่ตามมาของรายการที่เหมือนกัน
ดังนั้นnubSequential [1, 2, 2, 3] === [1, 2, 3]. ขณะนี้เราจะย่อฟังก์ชั่นนี้เป็นnS

หากnSนำไปใช้กับรายการที่ว่างเปล่าไม่มีสิ่งใดที่สามารถทำได้และเรากลับรายการเปล่า

หากnSนำไปใช้กับรายการที่มีเนื้อหาการประมวลผลจริงก็สามารถทำได้ สำหรับสิ่งนี้เราต้องการฟังก์ชั่นที่สองที่นี่ในwhere-clause เพื่อใช้การเรียกซ้ำเนื่องจากเราnSไม่ได้ติดตามองค์ประกอบเพื่อเปรียบเทียบ
เราตั้งชื่อฟังก์ชั่นgนี้ มันทำงานได้โดยการเปรียบเทียบอาร์กิวเมนต์แรกกับส่วนหัวของรายการที่ได้รับและยกเลิกหัวถ้าพวกเขาจับคู่และเรียกตัวเองบนหางกับอาร์กิวเมนต์แรกเก่า หากพวกเขาทำไม่ได้มันจะต่อท้ายหัวท้ายหางผ่านตัวเองพร้อมกับหัวเป็นอาร์กิวเมนต์แรกใหม่
หากต้องการใช้gเราให้หัวของอาร์กิวเมนต์nSและส่วนท้ายเป็นอาร์กิวเมนต์สองตัว

nSตอนนี้เป็นประเภทEq a => [a] -> [a]นำรายการและส่งคืนรายการ มันต้องการให้เราสามารถตรวจสอบความเท่าเทียมกันระหว่างองค์ประกอบที่ทำในการกำหนดฟังก์ชั่น

จากนั้นเราเขียนฟังก์ชั่นnSและการtransposeใช้(.)โอเปอเรเตอร์ ฟังก์ชั่นการเขียนหมายถึงต่อไปนี้:
(f . g) x = f (g (x))

ในตัวอย่างของเราtransposeหมุนตาราง 90 ° nSลบองค์ประกอบที่เท่ากันตามลำดับทั้งหมดของรายการในกรณีนี้รายการอื่น ๆ (นั่นคือสิ่งที่ตาราง) transposeหมุนมันกลับมาแล้วnSลบองค์ประกอบที่เท่ากันตามลำดับ นี่คือการลบแถวที่ซ้ำกันตามลำดับคอลัมน์

สิ่งนี้เป็นไปได้เพราะถ้าaตรวจสอบได้สำหรับความเสมอภาค ( instance Eq a) แล้ว[a]ก็เช่นกัน
ในระยะสั้น:instance Eq a => Eq [a]

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.