C# 自动对焦例行程序检测非常小的模糊差异

C# 自动对焦例行程序检测非常小的模糊差异,c#,image,algorithm,image-processing,aforge,C#,Image,Algorithm,Image Processing,Aforge,我正在开发一个自动对焦程序,用于在千分尺上定位,因此我需要找到图像之间在对焦/模糊方面的非常小的差异。幸运的是,图像模式始终相同(这些是原始2 MP图像的256x256中心裁剪): 找到以上两种图像的更好聚焦不是问题,我想大多数算法都可以。但我确实需要比较焦距差异小得多的图像,如以下图像: 另一种方法是在焦距平面的对侧找到两个模糊程度相同的图像,以逐步接近最佳焦距。例如,可以保存-50µm范围内的图像,然后尝试在模糊度相等的+50µm范围内查找图像。假设图像位于+58µm处,则焦平面应位于+

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

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

另一种方法是在焦距平面的对侧找到两个模糊程度相同的图像,以逐步接近最佳焦距。例如,可以保存-50µm范围内的图像,然后尝试在模糊度相等的+50µm范围内查找图像。假设图像位于+58µm处,则焦平面应位于+4µm处


有什么合适的算法吗?

令人惊讶的是,许多非常简单的自动对焦算法实际上在这个问题上表现得非常好。我实现了Liu、Wang和Sun在论文中概述的16种算法中的11种。因为我很难找到设置阈值的建议,所以我还添加了一些没有阈值的变体。我还添加了一个简单但聪明的建议,可以在这里找到:比较JPEG压缩图像的文件大小(更大的大小=更多的细节=更好的焦点)

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

  • 以2µm焦距的间隔保存21幅图像,总范围为±20µm
  • 计算每个图像的焦点值
  • 将结果拟合为二次多项式
  • 找到给出多项式最大值的位置
除直方图范围外,所有算法都给出了良好的结果。有些算法稍作修改,例如,它们使用X和Y方向的亮度差。我还必须改变StdevBasedCorrelation、熵、ThresholdeContent、ImagePower和ThresholdeImagePower算法的符号,以便在焦点位置获得最大值而不是最小值。这些算法需要24位灰度图像,其中R=G=B。如果用于彩色图像,则只计算蓝色通道(当然很容易校正)

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

  • 稳定焦点位置
  • 较大的负x²系数导致焦点位置的窄峰
  • 多项式拟合的低残差平方和
有趣的是,ThresholdedSquaredGradent和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;
  }
}
在±50µm的范围内,情况看起来非常相似:

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


总的来说,这些简单算法的性能和一致性给我留下了深刻的印象。用肉眼很难判断50µm图像是否失焦,但算法在比较仅相隔几微米的图像时没有问题。

对于NindzAl对原始答案的评论,有一个额外的答案:

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

Extreme Optimization library的单个开发许可证的成本为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();

至于免费的库,Math.Net将用于此目的

当您尝试基本算法时会发生什么?信噪比太小了吗?我只是假设基本算法检测不到0、5和10µm图像之间的任何有效差异。但是我刚才试了一些,实际上得到了很有希望的结果。我将获取更多相距仅1µm的图像,看看结果是否仍然令人满意。嗨,我知道这是一个老问题,但我正在研究一个非常类似的问题,所以我希望你能详细说明这一点“将结果拟合为二次多项式”。嗨,NindzAl,请查看我的额外答案()@安洛我知道这是一个非常古老的问题,但也许你也可以尝试NASA好奇号火星探测车使用的非常简单的基于JPEG大小的算法,看我的答案是的,JPEG大小是我测试的算法之一。它的表现相当不错。
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 µm             |             20 µm
              0 µm             |             50 µm
              0 µm             |             500 µm
// 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();