【问题标题】:Autofocus routine detecting very small differences in blur自动对焦例程检测非常小的模糊差异
【发布时间】:2013-02-28 03:31:22
【问题描述】:

我正在开发用于在微米尺度上定位的自动对焦例程,因此我需要找到图像之间焦点/模糊方面非常小的差异。幸运的是,图像模式始终相同(这些是原始 2 MP 图像的 256x256 中心裁剪):

         Perfect focus         |           50 µm off

找到以上两个更好聚焦的图像不是问题,我想大多数算法都可以。但我确实需要比较焦点差异较小的图像,如下所示:

           5 µm off            |           10 µm off

越来越接近最佳焦点的另一种方法是找到两个在焦点平面的相对两侧具有相同模糊量的图像。例如,可以保存 -50 µm 的图像,然后尝试在 +50 µm 附近找到模糊相等的图像。假设图像是在 +58 µm 处找到的,那么焦平面应该位于 +4 µm 处。

对于合适的算法有什么想法吗?

【问题讨论】:

  • 尝试基本算法时会发生什么?是不是信噪比太低了?
  • 好吧,我只是假设基本算法不会检测到 0、5 和 10 µm 图像之间的任何有效差异。但我刚才尝试了一些,实际上得到了相当有希望的结果。我将获取更多相距仅 1 µm 的图像,看看结果是否仍然令人满意。

标签: c# image algorithm image-processing aforge


【解决方案1】:

令人惊讶的是,许多非常简单的自动对焦算法实际上在这个问题上表现得相当好。我实现了 Liu、Wang 和 Sun 的论文 Dynamic evaluation of autofocusing for automated microscopic analysis of blood smear and pap smear 中概述的 16 种算法中的 11 种。由于我无法找到设置阈值的建议,因此我还添加了一些没有阈值的变体。我还在 SO 上添加了一个简单但聪明的建议:比较 JPEG 压缩图像的文件大小(更大的大小 = 更多的细节 = 更好的焦点)。

我的自动对焦例程执行以下操作:

  • 以 2 µm 焦距的间隔保存 21 张图像,总范围 ±20 µm。
  • 计算每个图像的焦点值。
  • 将结果拟合到二次多项式。
  • 找到给出多项式最大值的位置。

除直方图范围外的所有算法都给出了良好的结果。一些算法稍作修改,例如它们使用 X 和 Y 方向的亮度差异。我还必须更改 StdevBasedCorrelation、Entropy、ThresholdedContent、ImagePower 和 ThresholdedImagePower 算法的符号,以在焦点位置获得最大值而不是最小值。该算法需要一个 24 位灰度图像,其中 R = G = B。如果用于彩色图像,则只会计算蓝色通道(当然很容易校正)。

通过运行阈值 0、8、16、24 等直至 255 的算法并为以下各项选择最佳值,找到了最佳阈值:

  • 稳定的焦点位置
  • 大的负 x² 系数导致焦点位置的窄峰
  • 多项式拟合的低残差平方和

有趣的是,ThresholdedSquaredGradient 和 ThresholdedBrennerGradient 算法的焦点位置、x² 系数和残差平方和具有几乎平坦的直线。它们对阈值的变化非常不敏感。

算法的实现:

public unsafe List<Result> CalculateFocusValues(string filename)
{
  using(Bitmap bmp = new Bitmap(filename))
  {
    int width  = bmp.Width;
    int height = bmp.Height;
    int bpp = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
    BitmapData data = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, bmp.PixelFormat);

    long sum = 0, squaredSum = 0;
    int[] histogram = new int[256];

    const int absoluteGradientThreshold = 148;
    long absoluteGradientSum = 0;
    long thresholdedAbsoluteGradientSum = 0;

    const int squaredGradientThreshold = 64;
    long squaredGradientSum = 0;
    long thresholdedSquaredGradientSum = 0;

    const int brennerGradientThreshold = 184;
    long brennerGradientSum = 0;
    long thresholdedBrennerGradientSum = 0;

    long autocorrelationSum1 = 0;
    long autocorrelationSum2 = 0;

    const int contentThreshold = 35;
    long thresholdedContentSum = 0;

    const int pixelCountThreshold = 76;
    long thresholdedPixelCountSum = 0;

    const int imagePowerThreshold = 40;
    long imagePowerSum = 0;
    long thresholdedImagePowerSum = 0;

    for(int row = 0; row < height - 1; row++)
    {
      for(int col = 0; col < width - 1; col++)
      {
        int current = *((byte *) (data.Scan0 + (row + 0) * data.Stride + (col + 0) * bpp));
        int col1    = *((byte *) (data.Scan0 + (row + 0) * data.Stride + (col + 1) * bpp));
        int row1    = *((byte *) (data.Scan0 + (row + 1) * data.Stride + (col + 0) * bpp));

        int squared = current * current;
        sum += current;
        squaredSum += squared;
        histogram[current]++;

        int colDiff1 = col1 - current;
        int rowDiff1 = row1 - current;

        int absoluteGradient = Math.Abs(colDiff1) + Math.Abs(rowDiff1);
        absoluteGradientSum += absoluteGradient;
        if(absoluteGradient >= absoluteGradientThreshold)
          thresholdedAbsoluteGradientSum += absoluteGradient;

        int squaredGradient = colDiff1 * colDiff1 + rowDiff1 * rowDiff1;
        squaredGradientSum += squaredGradient;
        if(squaredGradient >= squaredGradientThreshold)
          thresholdedSquaredGradientSum += squaredGradient;

        if(row < bmp.Height - 2 && col < bmp.Width - 2)
        {
          int col2    = *((byte *) (data.Scan0 + (row + 0) * data.Stride + (col + 2) * bpp));
          int row2    = *((byte *) (data.Scan0 + (row + 2) * data.Stride + (col + 0) * bpp));

          int colDiff2 = col2 - current;
          int rowDiff2 = row2 - current;
          int brennerGradient = colDiff2 * colDiff2 + rowDiff2 * rowDiff2;
          brennerGradientSum += brennerGradient;
          if(brennerGradient >= brennerGradientThreshold)
            thresholdedBrennerGradientSum += brennerGradient;

          autocorrelationSum1 += current * col1 + current * row1;
          autocorrelationSum2 += current * col2 + current * row2;
        }

        if(current >= contentThreshold)
          thresholdedContentSum += current;
        if(current <= pixelCountThreshold)
          thresholdedPixelCountSum++;

        imagePowerSum += squared;
        if(current >= imagePowerThreshold)
          thresholdedImagePowerSum += current * current;
      }
    }
    bmp.UnlockBits(data);

    int pixels = width * height;
    double mean = (double) sum / pixels;
    double meanDeviationSquared = (double) squaredSum / pixels;

    int rangeMin = 0;
    while(histogram[rangeMin] == 0)
      rangeMin++;
    int rangeMax = histogram.Length - 1;
    while(histogram[rangeMax] == 0)
      rangeMax--;

    double entropy = 0.0;
    double log2 = Math.Log(2);
    for(int i = rangeMin; i <= rangeMax; i++)
    {
      if(histogram[i] > 0)
      {
        double p = (double) histogram[i] / pixels;
        entropy -= p * Math.Log(p) / log2;
      }
    }

    return new List<Result>()
    {
      new Result("AbsoluteGradient",            absoluteGradientSum),
      new Result("ThresholdedAbsoluteGradient", thresholdedAbsoluteGradientSum),
      new Result("SquaredGradient",             squaredGradientSum),
      new Result("ThresholdedSquaredGradient",  thresholdedSquaredGradientSum),
      new Result("BrennerGradient",             brennerGradientSum),
      new Result("ThresholdedBrennerGradient",  thresholdedBrennerGradientSum),
      new Result("Variance",                    meanDeviationSquared - mean * mean),
      new Result("Autocorrelation",             autocorrelationSum1 - autocorrelationSum2),
      new Result("StdevBasedCorrelation",       -(autocorrelationSum1 - pixels * mean * mean)),
      new Result("Range",                       rangeMax - rangeMin),
      new Result("Entropy",                     -entropy),
      new Result("ThresholdedContent",          -thresholdedContentSum),
      new Result("ThresholdedPixelCount",       thresholdedPixelCountSum),
      new Result("ImagePower",                  -imagePowerSum),
      new Result("ThresholdedImagePower",       -thresholdedImagePowerSum),
      new Result("JpegSize",                    new FileInfo(filename).Length),
    };
  }
}

public class Result
{
  public string Algorithm { get; private set; }
  public double Value     { get; private set; }

  public Result(string algorithm, double value)
  {
    Algorithm = algorithm;
    Value     = value;
  }
}

为了能够绘制和比较不同算法的焦点值,它们被缩放到 0 和 1 之间的值 (scaled = (value - min)/(max - min))。

±20 µm 范围内所有算法的绘图:

              0 µm             |             20 µm

在 ±50 µm 的范围内,情况看起来非常相似:

              0 µm             |             50 µm

当使用 ±500 µm 的范围时,事情会变得更有趣。四种算法表现出更多的四次多项式形状,而其他算法开始看起来更像高斯函数。此外,直方图范围算法的性能开始优于较小范围。

              0 µm             |             500 µm

总的来说,我对这些简单算法的性能和一致性印象深刻。用肉眼,即使是 50 µm 的图像也很难判断是否失焦,但算法可以轻松比较相距仅几微米的图像。

【讨论】:

  • 嗨,我知道这是一个老问题,但我正在研究一个非常相似的问题,所以我希望你能详细说明这一点“将结果拟合到二次多项式。”
  • 您好 NindzAl,请查看我的额外答案 (stackoverflow.com/a/17758994/306074)
  • @Anlo 我知道这是一个非常古老的问题,但也许您也可以尝试 NASA 好奇号火星探测器中使用的非常简单的基于 JPEG 大小的算法,请参阅我的回答 stackoverflow.com/a/32951113/15485
  • 是的,Jpeg 大小是我测试的算法之一。它的表现相当不错。
【解决方案2】:

对 NindzAl 对原始答案的评论的额外回答:

我使用Extreme Optimization 库将锐度值拟合到二次多项式。然后使用多项式的一阶导数提取最大锐度的距离。

Extreme Optimization 库的单个开发许可证成本为 999 美元,但我确信有一些开源数学库也可以执行拟合。

// Distances (in µm) where the images were saved
double[] distance = new double[]
{
  -50,
  -40,
  -30,
  -20,
  -10,
    0,
  +10,
  +20,
  +30,
  +40,
  +50,
};

// Sharpness value of each image, as returned by CalculateFocusValues()
double[] sharpness = new double[]
{
  3960.9,
  4065.5,
  4173.0,
  4256.1,
  4317.6,
  4345.4,
  4339.9,
  4301.4,
  4230.0,
  4131.1,
  4035.0,
};

// Fit data to y = ax² + bx + c (second degree polynomial) using the Extreme Optimization library
const int SecondDegreePolynomial = 2;
Extreme.Mathematics.Curves.LinearCurveFitter fitter = new Extreme.Mathematics.Curves.LinearCurveFitter();
fitter.Curve = new Extreme.Mathematics.Curves.Polynomial(SecondDegreePolynomial);
fitter.XValues = new Extreme.Mathematics.LinearAlgebra.GeneralVector(distance,  true);
fitter.YValues = new Extreme.Mathematics.LinearAlgebra.GeneralVector(sharpness, true);
fitter.Fit();

// Find distance of maximum sharpness using the first derivative of the polynomial
// Using the sample data above, the focus point is located at distance +2.979 µm
double focusPoint = fitter.Curve.GetDerivative().FindRoots().First();

【讨论】:

    【解决方案3】:

    至于免费库,Math.Net 将为此目的而工作

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-09-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多