2013-03-12 21 views
7

Opracowuję rutynę autofocusa do pozycjonowania w skali mikrometrycznej, więc muszę znaleźć bardzo małe różnice w ostrości/rozmyciu między obrazami. Na szczęście, wzór obraz będzie zawsze taki sam (to 256x256 Centrum uprawach pierwotnych 2 obrazów MP):Procedura autofokusa wykrywająca bardzo małe różnice w rozmyciu

0 µm off50 µm off

  Perfect focus   |   50 µm off 

Znalezienie lepiej ostry obraz z dwóch poprzednich nie jest problemem , Chyba większość algorytmów to zrobi. Ale naprawdę trzeba porównać obrazy o dużo mniejszej różnicy w ostrości, jak te poniżej:

5 µm off10 µm off

  5 µm off   |   10 µm off 

alternatywnego do podchodząc bliżej i bliżej do optymalnej ostrości jest znalezienie dwóch obrazów, które mają taka sama ilość rozmycia po przeciwnych stronach płaszczyzny ustawiania ostrości. Na przykład można zapisać obraz z -50 μm, a następnie spróbować znaleźć obraz o wielkości około 50 μm, gdy rozmycie jest równe. Powiedzmy, że obraz został znaleziony przy +58 μm, wtedy płaszczyzna ostrości powinna być ustawiona na +4 μm.

Jakieś pomysły na odpowiedni algorytm?

+1

Co dzieje się podczas próby podstawowe algorytmy? Czy jest za mało sygnału do szumu? –

+0

Po prostu założyłem, że podstawowy algorytm nie wykryłby żadnej ważnej różnicy między obrazami 0, 5 i 10 μm. Ale wypróbowałem tylko kilka i osiągnąłem całkiem obiecujące wyniki. Zdobędę więcej obrazów w odstępie 1 μm i zobaczę, czy wyniki wciąż są satifying. – Anlo

Odpowiedz

10

Co zaskakujące, wiele całkiem prostych algorytmów autofokusa rzeczywiście radziło sobie całkiem dobrze z tym problemem. Zaimplementowałem 11 z 16 algorytmów opisanych w artykule Dynamic evaluation of autofocusing for automated microscopic analysis of blood smear and pap smear autorstwa Liu, Wang & Sun. Ponieważ miałem problemy ze znalezieniem zaleceń dotyczących ustawiania wartości progowych, dodałem także kilka wariantów bez progów. Dodałem również prostą, ale sprytną sugestię znalezioną tutaj na SO: porównaj rozmiar pliku skompresowanych obrazów JPEG (większy rozmiar = więcej szczegółów = lepsza ostrość).

Moja autofocus rutyna robi następujące:

  • Zapisz 21 zdjęć w odstępie 2 um ogniskowa, całkowity zakres ± 20 um.
  • Obliczanie wartości ostrości każdego obrazu.
  • Dopasuj wynik do wielomianu drugiego stopnia.
  • Znajdź pozycję, która daje maksymalną wartość wielomianu.

Wszystkie algorytmy z wyjątkiem zakresu histogramu dały dobre wyniki. Niektóre algorytmy są nieznacznie zmodyfikowane, na przykład używają różnicy jasności w obu kierunkach X &. Musiałem również zmienić znak algorytmów StdevBasedCorrelation, Entropy, ThresholdedContent, ImagePower i ThresholdedImagePower, aby uzyskać maksimum zamiast minimalnego w pozycji ogniskowania. Algorytmy oczekują 24-bitowego obrazu w skali szarości, w którym R = G = B. Jeśli zostanie użyty na kolorowym obrazie, tylko niebieski kanał zostanie obliczony (łatwo skorygowany oczywiście).

optymalnej wartości progowych zostało znalezione przez uruchomienie algorytmów z wartościami progowymi 0, 8, 16, 24 itd aż do 255, a wybór najlepszej wartości dla:

  • pozycji Stabilny ostrości
  • Large negatywnej X² współczynnik wynikający w wąskim piku przy pozycji ostrości
  • niską resztkową sumę kwadratów z wielomianu dopasowanie

To ciekawe, że ThresholdedSq uaredGradient i ThresholdedBrennerGradient algorytmy mają prawie płaską linię pozycji ostrości, współczynnik x² i resztkową sumę kwadratów. Są bardzo niewrażliwe na zmiany wartości progowej.

Implementacja algorytmów:

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; 
    } 
} 

Aby móc wykreślić i porównać wartości ostrości różnych algorytmów zostały przeskalowane do wartości pomiędzy 0 a 1 (scaled = (value - min)/(max - min)).

Działka wszystkich algorytmów zakresie ± 20 mikrometrów:

±20 µm

0 µm20 µm

   0 µm    |    20 µm 

Wszystko wygląda całkiem podobny do zakresu ± 50 mikrometrów:

±50 µm

0 µm50 µm

   0 µm    |    50 µm 

Podczas korzystania z zakresem ± 500 urn robi się bardziej interesująca. Cztery algorytmy wykazują więcej kształtu wielomianu czwartego stopnia, a pozostałe zaczynają bardziej przypominać funkcje Gaussa. Ponadto algorytm Histogram Range zaczyna działać lepiej niż w przypadku mniejszych zakresów.

±500 µm

0 µm500 µm

   0 µm    |    500 µm 

Ogólnie jestem bardzo pod wrażeniem wydajności i spójności tych prostych algorytmów. Gołym okiem trudno jest stwierdzić, że nawet obraz o średnicy 50 μm jest nieostry, ale algorytmy nie mają problemu z porównywaniem obrazów w odległości kilku mikronów.

+0

Witam, wiem, że to stare pytanie, ale pracuję nad BARDZO podobnym problemem, więc miałem nadzieję, że mógłbyś rozwinąć ten punkt "Dopasuj wynik do wielomianu drugiego stopnia". – NindzAI

+0

Cześć NindzAl, zobacz moją dodatkową odpowiedź (http://stackoverflow.com/a/17758994/306074) – Anlo

+0

@Aloo Wiem, że to bardzo stare pytanie, ale może spróbuj również bardzo prosty algorytm oparty na rozmiarze JPEG używany w NASA Curiosity Mars Rover, zobacz moją odpowiedź http://stackoverflow.com/a/32951113/15485 –

3

Dodatkowa odpowiedź do komentarza NindzAl dotyczącego oryginalnej odpowiedzi:

używam biblioteki Extreme Optimization dopasować wartości ostrości do drugiego stopnia wielomianu. Odległość maksymalnej ostrości jest następnie wyodrębniana za pomocą pierwszej pochodnej wielomianu.

Biblioteka Extreme Optimization kosztuje 999 USD za jedną licencję dev, ale jestem pewien, że istnieją biblioteki matematyczne z otwartym dostępem do kodu źródłowego, które równie dobrze mogą wykonać dopasowanie.

// 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(); 
0

chodzi o wolnej biblioteki Math.Net będzie działać w tym celu