从屏幕捕获并多线程保存到磁盘

6
以下问题需要观察屏幕,记录一个事件(一个测量文本框变为绿色),并记录导致该事件发生的所有事件,生成导致该事件发生的“电影”。不幸的是,整个屏幕都需要被记录。到目前为止,我已经完成了识别部分。然而,我几乎每秒只能得到两帧。我想要大约25到30帧每秒
我的想法是在两个单独的线程中进行写入和读取。因为写入事件很少,可以在后台运行,所以记录事件可以占用更多时间并运行得更快。不幸的是,整个过程似乎太慢了。我想要能够将屏幕上发生事件的之前的10到20秒写入磁盘。
编辑:如果可能的话,我想尽可能保持平台无关性。
编辑2: 对于Xuggler,似乎有一个平台无关的jar文件。不幸的是,我不太清楚如何将其用于我的目的:记录导致isarecord被触发的前20秒。
以下是我迄今为止所做的事情:
package fragrecord;

import java.awt.AWTException;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;

public class Main {
    public static void main(String[] args) {
        //The numbers are just silly tune parameters. Refer to the API.
        //The important thing is, we are passing a bounded queue.
        ExecutorService consumer = new ThreadPoolExecutor(1,4,30,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(100));
        System.out.println("starting");
        //No need to bound the queue for this executor.
        //Use utility method instead of the complicated Constructor.
        ExecutorService producer = Executors.newSingleThreadExecutor();

        Runnable produce = new Produce(consumer);
        producer.submit(produce);  
        try {
            producer.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        consumer.shutdown();
        try {
            consumer.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}

class Produce implements Runnable {
    private final ExecutorService consumer;

    public Produce(ExecutorService consumer) {
        this.consumer = consumer;
    }
    boolean isarecord(BufferedImage image){
        int x=10, y = 10;
        Color c = new Color(image.getRGB(x,y));
        int red = c.getRed();
        int green = c.getGreen();
        int blue = c.getBlue();
        // Determine whether to start recording
        return false;

    }


    @Override
    public void run() {

        Robot robot = null;
        try {
            robot = new Robot();
        } catch (AWTException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //
        // Capture screen from the top left to bottom right
        //
        int i = 0;
        while(true) {

            i++;
        BufferedImage bufferedImage = robot.createScreenCapture(
                new Rectangle(new Dimension(1024, 798)));

        Runnable consume = new Consume(bufferedImage,i);
        consumer.submit(consume);
        }

    }
}

class Consume implements Runnable {
    private final BufferedImage bufferedImage;
    private final Integer picnr;
    public Consume(BufferedImage bufferedImage, Integer picnr){
        this.bufferedImage = bufferedImage;
        this.picnr = picnr;
    }

    @Override
    public void run() {
        File imageFile = new File("screenshot"+picnr+".png");
        try {
            System.out.println("screenshot"+picnr+".png");
            ImageIO.write(bufferedImage, "png", imageFile);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
5个回答

4

我尝试对您的代码进行了一些编辑,不再创建 png 文件,而是尝试创建 bmp 文件,这样可以减少数据压缩的开销时间,但以牺牲磁盘空间为代价。

结果:我不知道如何计算 fps,但这个解决方案比您的更快。 :-)


谢谢,我没想到会得到这个。我会把它加入到我的代码中。 - tarrasch

3
你最大的问题在于只能使用一个线程来创建图像。ThreadPoolExecutor并不会按你预期的方式创建线程。 根据javadoc: - 如果正在运行的线程少于corePoolSize,则执行程序总是倾向于添加新线程而不是排队。 - 如果正在运行的线程达到corePoolSize或更多,则执行程序总是倾向于将请求排队而不是添加新线程。 - 如果无法将请求排队,则会创建一个新线程,除非这将超过maximumPoolSize,在这种情况下,任务将被拒绝。
因此,它只使用一个线程,除非队列已满。此时你有100个屏幕截图在内存中,这增加了垃圾回收的工作量。如果我将核心线程数设置为4(我的笔记本电脑有4个核心)并将内存增加到1 GB,我就能够捕捉大约20 FPS左右的速度。
如果输出到磁盘受限,你可以将最后400张写入的图像存储为字节数组放在队列中,并在按钮变为“绿色”时再将其写入磁盘。但在我的测试中,这些图像将占用超过100MB的内存,因此请确保你有足够的内存。

我该如何修改我的ThreadPoolExecutor以使其正常工作?我假设我有3个核心 - 一个用于计算,另外3个用于四核机器上的其他任务。内存应该在2-3 GB左右。磁盘空间不受限制。 - tarrasch
线程池的工作是“正确的”,只是不像大多数人所期望的那样。如果将核心线程设置为3,则始终会有3个线程在工作或准备工作。在我的机器上(2.6 GHz,4核i7),仅获取快照就需要约50毫秒,因此这将限制为20 fps。其他三个核心似乎能够跟上,硬盘(SSD)也是如此。但是,在那一点上,机器正在努力工作。 - Roger Lindsjö
我现在将同时工作的进程数量设置为3个。这样获得了更好的结果。我现在会尝试测量FPS并更新此内容。也许我会设定一个奖励,因为有些人对此感兴趣。 - tarrasch

3
您需要测量robot.createScreenCapture()的执行时间。很有可能它需要超过40毫秒,这意味着使用纯Java无法实现您想要的功能。按我的经验来看,该调用可能非常缓慢。
我成功地通过一个技巧将该时间大大降低,但该技巧仅适用于Unix:启动VNC服务器(= RAM中的桌面)。 我已经修改了TightVNC的源代码,使用NIO将图像写入磁盘,使用内存映射文件。这使得我获得了大约10-20 fps。
以下是使用NIO编写图像的代码:
private File pixelFile = new File("tmp", "pixels.nio").getAbsoluteFile();
private IntBuffer intBuffer;
private FileChannel rwChannel;

private MappedByteBuffer byteBuffer;
private int[] pixels;

private void createMemoryMappedFile() {
    File dir = pixelFile.getParentFile();
    if(!dir.exists()) {
        dir.mkdirs();
    }

    try {
        rwChannel = new RandomAccessFile(pixelFile, "rw").getChannel();

        int width = ...;
        int height = ...;
        pixels = new int[width*height];

        byteBuffer = rwChannel.map(MapMode.READ_WRITE, 0, width * height * 4);
        intBuffer = byteBuffer.asIntBuffer();
    } catch(Exception e) {
        throw new RuntimeException("Error creating NIO file " + pixelFile, e);
    }
}

public void saveImage() {

     buffer.position(0);
     buffer.put(image.getRaster().getPixels(0,0,width,height,pixels));

     flushPixels();
}

private void flushPixels() {
    byteBuffer.force();
}

不幸的是,整个东西必须在Mac和Windows上运行。谢谢 :-) - tarrasch
也有适用于Mac和Windows的VNC服务器。我从未使用过它们,所以不知道它们能做什么。在Unix上,你可以在RAM中获得整个桌面;你不必共享实际的桌面,因此你可以运行服务器并仍然使用鼠标+键盘。 - Aaron Digulla

2
由于各种原因,使用执行器、重写成 NIO 等方法都不会有帮助。以下是您应该考虑的一些事项:
  1. 在Java中,图像捕获速度很慢,除非使用JNI依赖项,否则无法解决此问题。
  2. 使用JPEG而不是PNG,速度更快。
  3. 使用ImageIO进行任何图像压缩都很慢。您可以使用旧式的专有JpegEncoder类(在com.sun包中)或TurboJPEG Java库(也是JNI),但我认为这不是瓶颈。
  4. 磁盘I / O绝对不是问题(您正在写入<5mb / s,所以不用担心)。
  5. 同时写入多个图像实际上会减慢应用程序的速度,而不是加快速度(除非您有SSD)。
  6. 考虑并行化捕获/分析线程(例如,仅每20个帧执行一次分析)(*)。
  7. 我敢打赌,在将Java应用程序优化到以100% CPU运行25fps之前,您会使用本机语言为每个平台编写两次此应用程序XD
  8. 认真考虑混合解决方案;例如,使用可用工具将所有内容记录到压缩电影中,然后稍后进行分析(**)。

总之,Java在您想要做的事情上表现非常糟糕。

然而,我已经编写了自己的版本这个工具。 http://pastebin.com/5h285fQw 它的一个功能是允许记录鼠标后面的较小矩形。 在500x500的情况下,它可以轻松地达到25fps,并且图像会在后台被写入(对我来说,图像压缩和写入只需要5-10毫秒,因此它比记录速度更快)。
(*) 你没有谈论如何分析图像,但这似乎是你性能问题的主要来源。以下是一些想法:
- 只查看每第 N 帧 - 仅捕获屏幕的子部分并随时间移动该子部分(稍后重新组合图像;这会导致可怕的撕裂,但也许对您的目的无关紧要) - 捕获本身应该“不太慢”(全屏幕可能为每秒10-20帧);使用串行捕获但并行化分析
(**) 在 MacOSX 上,您可以使用内置的 QuickTime X 来有效地记录到硬盘;在 Windows 上,我听说 PlayClaw(http://www.playclaw.com/)非常好,并且也许值得购买(考虑一下在优化 Java 代码时浪费的时间 :) )

1

虽然有些离题,但是看看Xuggler吧。如果你想用Java创建视频,它会很有用。

编辑:此外,如果您不将每个图片都转储到磁盘上,而是将它们附加到字节数组中,并且很少在磁盘上转储,那么您可以优化您的图像消费者代码。

编辑2:这里有一个“无需安装”的库版本和它的maven依赖项(带有预编译的特定于平台的库的jar文件):blog.xuggle.com/2012/04/03/death-to-installersxuggle.com/xuggler/downloads


如果可能的话,我想尽可能地避免使用本地代码。我已经修改了我的问题。 - tarrasch
但是如果我找到一种有效地在Mac上部署Juggler的方法,我可能会使用它。我不想让用户费心从互联网安装东西。 - tarrasch
@tarrasch,我理解你的意思。但是据我所知,当你试图从jvm内部访问外部资源时(特别是对于大量数据,例如屏幕的完整位图),会有很多开销。 - stemm
@tarrasch,此外,如果您不将每个图像转储到磁盘,而是将它们附加到字节数组中,并且更少地将整个数组转储到磁盘上,那么您可以优化您的图像消费者代码。 - stemm
@tarrasch,看看这个例子:https://github.com/xuggle/xuggle-xuggler/blob/master/src/com/xuggle/xuggler/demos/CaptureScreenToFile.java 它看起来与你的情况非常相似。 - stemm
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接