加工 |程序滞后

Processing | Program is lagging


我是 Processing 的新手,我需要制作一个程序,捕获主显示器,在第二个屏幕上显示平均颜色,并使用函数获得的另一种颜色(感知主色)制作螺旋线。
问题是程序太慢了(滞后,1FPS)。我想是因为每次截图的时候有太多事情要做,但是我不知道如何让它更快。

可能还有很多其他问题,但最主要的是。
非常感谢!

代码如下:

import java.awt.Robot;
import java.awt.AWTException;
import java.awt.Rectangle;
import java.awt.color.ColorSpace;

PImage screenshot; 

float a = 0;
int blockSize = 20;

int avg_c;
int per_c;


void setup() {
  fullScreen(2); // 1920x1080
  noStroke();
  frame.removeNotify();
}

void draw() { 
  screenshot();
  avg_c = extractColorFromImage(screenshot);
  per_c = extractAverageColorFromImage(screenshot);
  background(avg_c); // Average color
  spiral();
}


void screenshot() {
  try{
    Robot robot_Screenshot = new Robot();
    screenshot = new PImage(robot_Screenshot.createScreenCapture
    (new Rectangle(0, 0, displayWidth, displayHeight)));
  }
  catch (AWTException e){ }
  frame.setLocation(displayWidth/2, 0);
}

void spiral() {
  fill (per_c); 
  for (int i = blockSize; i < width; i += blockSize*2)
  {
    ellipse(i, height/2+sin(a+i)*100, blockSize+cos(a+i)*5, blockSize+cos(a+i)*5);    
    a += 0.001;
  }
}


color extractColorFromImage(PImage screenshot) { // Get average color
    screenshot.loadPixels(); 
    int r = 0, g = 0, b = 0; 
    for (int i = 0; i < screenshot.pixels.length; i++) { 
        color c = screenshot.pixels[i]; 
        r += c>>16&0xFF; 
        g += c>>8&0xFF; 
        b += c&0xFF;
    } 
    r /= screenshot.pixels.length; g /= screenshot.pixels.length; b /= screenshot.pixels.length;
    return color(r, g, b);
}

color extractAverageColorFromImage(PImage screenshot) { // Get lab average color (perceptual)
  float[] average = new float[3];
  CIELab lab = new CIELab();

  int numPixels = screenshot.pixels.length;
  for (int i = 0; i < numPixels; i++) {
    color rgb = screenshot.pixels[i];

    float[] labValues = lab.fromRGB(new float[]{red(rgb),green(rgb),blue(rgb)});

    average[0] += labValues[0];
    average[1] += labValues[1];
    average[2] += labValues[2];
  }

  average[0] /= numPixels;
  average[1] /= numPixels;
  average[2] /= numPixels;

  float[] rgb = lab.toRGB(average);

  return color(rgb[0] * 255,rgb[1] * 255, rgb[2] * 255);
}


public class CIELab extends ColorSpace {

    @Override
    public float[] fromCIEXYZ(float[] colorvalue) {
        double l = f(colorvalue[1]);
        double L = 116.0 * l - 16.0;
        double a = 500.0 * (f(colorvalue[0]) - l);
        double b = 200.0 * (l - f(colorvalue[2]));
        return new float[] {(float) L, (float) a, (float) b};
    }

    @Override
    public float[] fromRGB(float[] rgbvalue) {
        float[] xyz = CIEXYZ.fromRGB(rgbvalue);
        return fromCIEXYZ(xyz);
    }

    @Override
    public float getMaxValue(int component) {
        return 128f;
    }

    @Override
    public float getMinValue(int component) {
        return (component == 0)? 0f: -128f;
    }    

    @Override
    public String getName(int idx) {
        return String.valueOf("Lab".charAt(idx));
    }

    @Override
    public float[] toCIEXYZ(float[] colorvalue) {
        double i = (colorvalue[0] + 16.0) * (1.0 / 116.0);
        double X = fInv(i + colorvalue[1] * (1.0 / 500.0));
        double Y = fInv(i);
        double Z = fInv(i - colorvalue[2] * (1.0 / 200.0));
        return new float[] {(float) X, (float) Y, (float) Z};
    }

    @Override
    public float[] toRGB(float[] colorvalue) {
        float[] xyz = toCIEXYZ(colorvalue);
        return CIEXYZ.toRGB(xyz);
    }

    CIELab() {
        super(ColorSpace.TYPE_Lab, 3);
    }

    private double f(double x) {
        if (x > 216.0 / 24389.0) {
            return Math.cbrt(x);
        } else {
            return (841.0 / 108.0) * x + N;
        }
    }

    private double fInv(double x) {
        if (x > 6.0 / 29.0) {
            return x*x*x;
        } else {
            return (108.0 / 841.0) * (x - N);
        }
    }


    private final ColorSpace CIEXYZ =
        ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);

    private final double N = 4.0 / 29.0;
}

是的,在我的机器上大约 1 FPS:

优化代码可能真的很难,所以我没有阅读所有东西寻找可以改进的东西,而是从测试你失去这么多处理能力的地方开始。答案是在这一行:

per_c = extractAverageColorFromImage(screenshot);

extractAverageColorFromImage方法写得很好,但是低估了它要做的工作量。屏幕尺寸与屏幕像素数之间存在二次关系,因此屏幕越大情况越差。而且这种方法一直在处理屏幕截图的每个像素,每个屏幕截图几次。

对于平均颜色来说,这需要大量工作。现在,如果有办法减少一些角落……也许是更小的屏幕,或者更小的屏幕截图……哦!有!让我们调整屏幕截图的大小。毕竟,我们不需要为了平均而深入到单个像素这样的细节。在 screenshot 方法中,添加这一行:

void screenshot() {
  try {
    Robot robot_Screenshot = new Robot();
    screenshot = new PImage(robot_Screenshot.createScreenCapture(new Rectangle(0, 0, displayWidth, displayHeight)));
    // ADD THE NEXT LINE
    screenshot.resize(width/4, height/4);
  }
  catch (AWTException e) {
  }
  frame.setLocation(displayWidth/2, 0);
}

我将工作量除以 4,但我鼓励您调整这个数字,直到您以最快的速度获得令人满意的结果。这只是一个概念证明:

如您所见,调整屏幕截图大小并将其缩小 4 倍可使我的速度提高 10 倍。这不是奇迹,但好多了,我看不出最终结果有什么不同——但关于那部分,你必须使用自己的判断,因为你是知道你的项目是关于什么的人.希望对您有所帮助!

玩得开心!

很遗憾,我无法像 laancelot (+1) 那样提供详细的答案,但希望我能提供一些提示:

  1. 调整图片大小绝对是个不错的方向。请记住,您也可以跳过一些像素而不是递增每个像素。 (如果你正确处理像素索引,你可以在不调用调整大小的情况下获得类似的调整大小效果,尽管这不会为你节省很多 CPU 时间)
  2. 不要每秒多次创建新的 Robot 实例。在设置中创建一次并重新使用它。 (这是一个更好的习惯)
  3. 使用 CPU 探查器,例如 VisualVM 中的探查器,看看到底什么是慢的,旨在首先优化最慢的东西。

点 1 示例:

for (int i = 0; i < numPixels; i+= 100)

点 2 示例:

Robot robot_Screenshot;
...
void setup() {
  fullScreen(2); // 1920x1080
  noStroke();
  frame.removeNotify();
  try{
    robot_Screenshot = new Robot();
  }catch(AWTException e){
    println("error setting up screenshot Robot instance");
    e.printStackTrace();
  }
}
...
void screenshot() {
  screenshot = new PImage(robot_Screenshot.createScreenCapture
    (new Rectangle(0, 0, displayWidth, displayHeight)));
  frame.setLocation(displayWidth/2, 0);
}

第 3 点示例:

注意最慢的位实际上是 AWT 的 fromRGBMath.cbrt() 我建议寻找另一种 RGB -> XYZ -> L*a*b* 转换方法,它更简单(主要是函数,较少 类,具有 AWT 或其他依赖项)并且希望更快。

有很多事情可以做,甚至超出了已经提到的范围。

迭代与线程

截取屏幕截图后,立即迭代缓冲图像的每 1/N 个像素(可能每 4 或 8 个)。然后,在此迭代期间,计算每个像素的 LAB 值(因为每个像素通道都直接可用),同时增加每个 RGB 通道的 运行ning 总数。

这避免了我们对相同像素进行两次迭代,并避免了不必要的转换(BufferedImagePImage;然后从 PImage 像素合成然后分解像素通道)。

同样,我们避免了 Processing 的昂贵 resize() 调用(如另一个答案中所建议的),这不是我们想要在每一帧调用的东西(即使它确实加快了程序速度,但它不是一种有效的方法).

现在,在迭代更改之上,我们可以将迭代包装在 Callable 中,以轻松地 运行 同时跨多个系统线程的工作负载(毕竟,像素迭代是 令人尴尬的并行);下面的示例使用 2 个线程执行此操作,每个线程截屏并处理显示器像素的一半。

优化RGB→XYZ→LAB转换

我们不太关心向后转换,因为每帧只为一个值完成

看起来您已经自己实现了 XYZ→LAB,并且正在使用来自 java.awt.color 的 RGB→XYZ 转换器。

正如已经确定的那样,前向转换 XYZ→LAB 使用 cbrt() 作为瓶颈。我还想象 RGB→XYZ 实现对 Math.Pow(x, 2.4) 进行了 3 次调用——每个像素 3 个非整数指数大大增加了计算量。解决方案是更快的数学...

贾法玛

Jafamajava.math 的替代品——只需导入库并将任何 Math.__() 调用替换为 FastMath.__() 即可免费加速(您可以去通过将 Jafama 的 E-15 精度与更不准确但更快的基于 LUT 的专用 类).

进行交易,更进一步

所以至少,将 Math.cbrt() 换成 FastMath.cbrt()。然后考虑自己实现 RGB→XYZ (example),再次使用 Jafama 代替 java.math.


您甚至可能会发现,对于这样的项目,仅转换为 XYZ 颜色就足够了 space 可以用来克服众所周知的 RGB 弱点(从而避免 XYZ→LAB 转换).

缓存 LAB 计算

除非大多数像素在帧发生变化,然后考虑缓存每个像素的LAB值,只有当像素有变化时才重新计算它当前之前的帧之间发生变化。这里的权衡是检查每个像素与其先前值的开销,以及积极检查将节省多少计算。鉴于 LAB 计算的成本要高得多,这里非常值得。下面的示例使用了这种技术。

屏幕截图

无论程序的其余部分优化得多么好,一个相当大的瓶颈是 AWT 机器人的 createScreenCapture()。在足够大的显示器上,它很难超过 30FPS。我无法提供任何确切的建议,但值得看看 Java.

中的其他屏幕捕获方法

通过迭代更改和线程重新编写代码

此代码实现了上面讨论的减去对 LAB 计算的任何更改。

float a = 0;
int blockSize = 20;

int avg_c;
int per_c;

java.util.concurrent.ExecutorService threadPool = java.util.concurrent.Executors.newFixedThreadPool(4);

List<java.util.concurrent.Callable<Boolean>> taskList;

float[] averageLAB;
int totalR = 0, totalG = 0, totalB = 0; 

CIELab lab = new CIELab();

final int pixelStride = 8; // look at every 8th pixel


void setup() {
  size(800, 800, FX2D);
  noStroke();
  frame.removeNotify();

  taskList = new ArrayList<java.util.concurrent.Callable<Boolean>>();

  Compute thread1 = new Compute(0, 0, width, height/2);
  Compute thread2 = new Compute(0, height/2, width, height/2);
  taskList.add(thread1);
  taskList.add(thread2);
}

void draw() { 

  totalR = 0; // re init
  totalG = 0; // re init
  totalB = 0; // re init 
  averageLAB = new float[3]; // re init

  final int numPixels = (width*height)/pixelStride;

  try {
    threadPool.invokeAll(taskList); // run threads now and block until completion of all
  }
  catch (Exception e) {
    e.printStackTrace();
  }

  // calculate average LAB
  averageLAB[0]/=numPixels;
  averageLAB[1]/=numPixels;
  averageLAB[2]/=numPixels;

  final float[] rgb = lab.toRGB(averageLAB);
  per_c = color(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255);

  // calculate average RGB
  totalR/=numPixels;
  totalG/=numPixels;
  totalB/=numPixels;

  avg_c = color(totalR, totalG, totalB);

  background(avg_c); // Average color
  spiral();
  fill(255, 0, 0);
  text(frameRate, 10, 20);
}

class Compute implements java.util.concurrent.Callable<Boolean> {

  private final Rectangle screenRegion;
  private Robot robot_Screenshot;
  private final int[] previousRGB;
  private float[][] previousLAB;

  Compute(int x, int y, int w, int h) {

    screenRegion = new Rectangle(x, y, w, h);

    previousRGB = new int[w*h];
    previousLAB = new float[w*h][3];

    try {
      robot_Screenshot = new Robot();
    } 
    catch (AWTException e1) {
      e1.printStackTrace();
    }
  }

  @Override
    public Boolean call() {

    BufferedImage rawScreenshot = robot_Screenshot.createScreenCapture(screenRegion);  

    int[] ssPixels = new int[rawScreenshot.getWidth()*rawScreenshot.getHeight()]; // screenshot pixels

    rawScreenshot.getRGB(0, 0, rawScreenshot.getWidth(), rawScreenshot.getHeight(), ssPixels, 0, rawScreenshot.getWidth()); // copy buffer to int[] array

    for (int pixel = 0; pixel < ssPixels.length; pixel+=pixelStride) {

      // get invididual colour channels
      final int pixelColor = ssPixels[pixel];
      final int R = pixelColor >> 16 & 0xFF;
      final int G = pixelColor >> 8 & 0xFF;
      final int B = pixelColor & 0xFF;

      if (pixelColor != previousRGB[pixel]) { // if pixel has changed recalculate LAB value
        float[] labValues = lab.fromRGB(new float[]{R/255f, G/255f, B/255f}); // note that I've fixed this; beforehand you were missing the /255, so it was always white.
        previousLAB[pixel] = labValues;
      }

      averageLAB[0] += previousLAB[pixel][0];
      averageLAB[1] += previousLAB[pixel][1];
      averageLAB[2] += previousLAB[pixel][2];

      totalR+=R;
      totalG+=G;
      totalB+=B;

      previousRGB[pixel] = pixelColor; // cache last result
    }
    return true;
  }
}

800x800px;像素步幅 = 4;相当静态的屏幕背景