微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

加工 |程序滞后 贾法玛

如何解决加工 |程序滞后 贾法玛


我是 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,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:

About 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,displayWidth,displayHeight)));
    // ADD THE NEXT LINE
    screenshot.resize(width/4,height/4);
  }
  catch (AWTException e) {
  }
  frame.setLocation(displayWidth/2,0);
}

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

10x faster!

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

玩得开心!

,

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

  1. 调整图像大小绝对是一个好的方向。请记住,您还可以跳过多个像素,而不是增加每个像素。 (如果您正确处理像素索引,您可以在不调用 resize 的情况下获得类似的调整大小效果,尽管这不会为您节省大量 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,displayHeight)));
  frame.setLocation(displayWidth/2,0);
}

第 3 点示例: VisualVM CPU profiler

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

,

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

迭代和线程

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

这使我们免于对相同像素进行两次迭代并避免不必要的转换(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 个非整数指数大大增加了计算量。解决方案是更快的数学...

贾法玛

Jafama 是一个内置的 java.math 替代品——只需导入库并将任何 Math.__() 调用替换为 FastMath.__() 以获得免费加速(您可以更进一步将 Jafama 的 E-15 精度与准确度更低但速度更快的基于 LUT 的专用类进行交换)。

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


您甚至可能会发现,对于这样的项目,仅转换为 XYZ 就足以克服 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,width,height/2);
  Compute thread2 = new Compute(0,height/2,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);
  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,rawScreenshot.getWidth(),rawScreenshot.getHeight(),ssPixels,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;
  }
}

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

enter image description here

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。