Detekce defektů pomocí Fourierovy transformace

Na dnešním cvičení bude úkolem vytvořit algoritmus na kontrolu kvality kartonu (tzv. vlnité lepenky). Algoritmus by měl být schopný detekovat trhliny, díry a jiné defekty. Jelikož kontrolovaný karton je vyráběn jako tzv. nekonečný pás, je potřeba využít řádkovou kameru.

No description has been provided for this image

Výhodou materiálu je, že má opakující se strukturu. Tím pádem lze k detekci vytvořit algoritmus využívající Fourierovu transformaci (rozklad obrazu na opakující se obrazové frekvence). Na našem konkrétním kartonu se defekty vyskytují v podobě promáčklin na některých vlnách. Tím byl narušen jejich pravidelný vzor a tato nedokonalost je patrná ve frekvenčním spektru obrazu. Úkolem je tyto defekty v obrázku najít za pomoci Fourierovy transformace a vyznačit.

Obrázek kartonu byl nasnímán řádkovou kamerou. Kamera byla umístěna staticky nad pásem, karton se pod ní pohyboval pomocí důmyslného dopravníku.

Řádkové kamery

Řádková kamera neboli řádkový skener je speciální druh kamery, která snímá pouze 1 řádek. Díky tomu je schopna dosáhnout obrovských frekvencí snímání (až ticíce řádků za 1 sekundu). V labu máme kamery Basler Racer o velikosti řádku 6k, 8k a 12k pixelů. Řádkové kamery jsou díky svým specifickým vlastnostem vynikající pro řadu úloh, typickým použitím je snímání nekonečného pásu ve výrobních linkách.

No description has been provided for this image

Z podstaty skeneru však vyplývá, že se objekt pod kamerou musí pohybovat. V případě, že se objekt nebude pohybovat, vytvoří kamera obrázek, který bude mít všechny řádky totožné (pouze ten 1, který kamera snímá).

Tím, že je kamera schopná dosahovat neskutečných frekvencí snímání dále umožňuje získávat obrazová data v neskutečně velkém rozlišení. Na druhou stranu nás tím nutí nastavovat nízkou dobu expozice a tedy je zapotřebí mnohem více světla.

Import knihoven a konfigurace

import os
import io

import cv2
import numpy as np
import matplotlib.pyplot as plt
import pytesseract

from improutils import *

%matplotlib inline
np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})

Pomocné funkce

Z funkcí je potřeba vybírat ty vhodné pro splnění úkolu.

Následující funkce jsou nové a umožňují nám provést Fourierovu transformaci a související operace.


Úkol

1) Načtěte a zobrazte vstupní data nasnímaného kartonu.

Pozor, obrázek je potřeba načíst jako černobílý, tzn. pouze s jedním kanálem. Lze toho docílit vícero způsoby: buď lze použít klasickou funkci load_image a vybrat libovolný kanál nebo obrázek následně převést na šedotónový.

cardboard_texture = load_image('data/cardboard.bmp') ### načtení dat
cardboard_texture = to_gray(cardboard_texture) ### převod do šedotónu
plot_images(cardboard_texture)
No description has been provided for this image

2) Vyberte si z obrazu rozumně velký čtvercový výřez s nějakou vadou na kartonu.

Velikost obrazu zvolte 500 × 500.

x = 800
y = 1250
cardboard_texture_cropped = crop(cardboard_texture, x, y, x+500, y+500) ### zvolte správně ořez

# Kontrola rozměrů obrazu
h,w = cardboard_texture_cropped.shape
if h != w:
    print('Obrázek není čtverec! [h × w] = ' + str(h) + ' × ' + str(w) + '!')

plot_images(cardboard_texture_cropped)
No description has been provided for this image

3) Vyfiltrujte v rozumné míře šum mediánovým filtrem vhodné velikosti.

Pro připomenutí, mediánový filtr zachovává hrany v obraze (tzn. nechová se jako filtr dolní propusti). Proto je vhodné jeho využití společně s Fourierovou transformací. V případě využití filtrů dolní propusti (blur, gauss) by byly vyšší obrazové frekvence zbytečně odstraněny.

cardboard_texture_filtered = filtration_median(cardboard_texture_cropped, 25) ### filtrace o vhodné velikosti
plot_images(cardboard_texture_filtered)
No description has been provided for this image

4) Zvyšte konstrast obrazu použitím min-max normalizace.

Normalizace obrazu převede jeho aktuální rozsah hodnot na zvolené hodnoty. V případě zvyšování konstrastu jsou zvolenými hodnotami minimální resp. maximální hodnoty pro osmibitový obraz (tedy 0 - 255).

cardboard_normalized = normalize(cardboard_texture_filtered) ###
plot_images(cardboard_normalized)
No description has been provided for this image

5) Zobrazte si frekvenční spektrum obrázku.

Aplikujte Fourierovu transformaci na upravený obrázek a získané spektrum si zobrazte.

Poznámka: Frekvenční spektrum obrazu je tvořené komplexními čísly (viz Předzpracování obrazu - Filtrace v prostorové a frekvenční oblasti. Pro jeho správné funkční zobrazení se využívá následujících pár vylepšení:

  • Zobrazení velikosti těchto komplexních čísel.
  • Zobrazení obrázku spektra v logaritmickém měřítku (z důvodu neporovnatelně velké velikosti stejnosměrné složky = nulové frekvence oproti zbylým frekvencím).
  • Zobrazení spektra středově souměrného než je vypočtené pomocí přeházení kvadrantů (tzv. shift).
mag_spec, spectrum_shift = apply_fft(cardboard_normalized) ### aplikujte Fourierovu transformaci
plot_images(cardboard_normalized, mag_spec)
No description has been provided for this image

6) Nalezněte souřadnice zajímavých frekvencí ve frekvenčním spektru.

S vizualizací frekvenčního spektra lze zacházet jako s klasickým obrázkem. Jedná se přeci jen o osmibitový obraz s hodnotami 0 - 255.

Segmentujte obraz tak, aby v něm zbylo pouze 7 pixelů (neboli hodnot zajímavých obrazových frekvencí). Hodnotu amplitudy obrazové frekvence rezprezentuje jas v daném pixelu. Nejvyšší hodnota jasu v obrazu je samozřejmě jeho stejnosměrná složka.

# Segmentace
threshold = 212 ### zvolte správný práh
spectrum_intresting = segmentation_one_threshold(mag_spec, threshold) ### zvolte správnou metodu segmentace

# Nalezení indexů
idx = cv2.findNonZero(spectrum_intresting)
print(len(idx))
if len(idx) != 7:
    print('Pozor nalezených frekvencí není 7!')

# Výpis hodnot
print(idx)
7
[[[250 223]]

 [[250 241]]

 [[251 241]]

 [[250 250]]

 [[249 259]]

 [[250 259]]

 [[250 277]]]

7) Experimentujte s frekvenčními filtry.

Vytvořte několik filtrů (4) pro představu toho, co filtrace spektra dělá. Vždy si zobrazte kombinaci filtr + filtrované spektrum + obrázek s vyfiltrovaným spektrem. Filtrace obrazových frekvencí se provádí nad získaným spektrem. Ve spektru ukazují vysoké hodnoty na zajímavé frekvence vyskytující se v obraze.

Vytvořte 4 takovéto filtry:

  1. Filtr pouze stejnosměrné složky obrazu.
  2. Filtr dolní propust v ose x i ose y.
  3. Filtr dolní propust pouze v ose y obsahující zajímavé hodnoty frekvenčního spektra kartonu.
  4. Selektivní filtr v ose y pouze 7 (6+1) zajímavých hodnot frekvencí ze spektra kartonu.

Vhodné filtry mohou vypadat následovně:

Hint k práci s funkcí create_filter_mask():

  • Vstupem je vždy list hodnot, např. [1,2,3,4,5,6,7].
  • Pokud chcete jako vstup použít range(), je třeba ho zapsat jako *range(1:7).
  • Pokud chcete kombinovat obojí, lze to např. takto: [1, *range(3-5), 7].
  • V listu nemusí být pouze unikátní čísla (nespadne to), ale není jediný důvod, proč by tam mělo být nějaké číslo víckrát :)
cardboard_normalized.shape
(500, 500)
mid = cardboard_normalized.shape[0] // 2

# Filtr 1 pouze stejnosměrná složka
filter_mask_1 = create_filter_mask(cardboard_normalized.shape, [], [mid]) ### doplňte jak funkci, tak hodnoty
spec_mask_filt_1 = filter_mag_spec(mag_spec, filter_mask_1) ### doplňte funkci
cardboard_filt_1 = inverse_fft(spectrum_shift, filter_mask_1) ### doplňte funkci

# Filtr 2 dolní propust v x i y
filter_mask_2 = create_filter_mask(cardboard_normalized.shape, [mid], [mid]) ### doplňte jak funkci, tak hodnoty
spec_mask_filt_2 = filter_mag_spec(mag_spec, filter_mask_2) ### doplňte funkci
cardboard_filt_2 = inverse_fft(spectrum_shift, filter_mask_2) ### doplňte funkci

w = 30
ll = range(mid-w, mid+w, 1)

# Filtr 3 dolní propust v y se zajímavými hodnotami
filter_mask_3 = create_filter_mask(cardboard_normalized.shape, [ll], [mid]) ### doplňte jak funkci, tak hodnoty
spec_mask_filt_3 = filter_mag_spec(mag_spec, filter_mask_3) ### doplňte jak funkci, tak hodnoty
cardboard_filt_3 = inverse_fft(spectrum_shift, filter_mask_3) ### doplňte funkci

# Filtr 4 selektivní pouze pro 7 zajímavých hodnot v obraze
filter_mask_4 = create_filter_mask(cardboard_normalized.shape, [[i[0] for i in idx]], [mid]) ### doplňte jak funkci, tak hodnoty
spec_mask_filt_4 = filter_mag_spec(mag_spec, filter_mask_4) ### doplňte funkci
cardboard_filt_4 = inverse_fft(spectrum_shift, filter_mask_4) ### doplňte funkci

# Zobrazení
plot_images(
    filter_mask_1, spec_mask_filt_1, cardboard_filt_1,
    filter_mask_2, spec_mask_filt_2, cardboard_filt_2,
    filter_mask_3, spec_mask_filt_3, cardboard_filt_3,
    filter_mask_4, spec_mask_filt_4, cardboard_filt_4,
    normalize=True
)
No description has been provided for this image

8) Zjistěte index správné obrazové frekvence (ind), která odpovídá struktuře (vzoru) materiálu kartonu.

Je potřeba se zamyslet nad strukturou materiálu.

  • Jeho "vlnitost" neboli opakování vzoru je pouze v ose y obrazu, v ose x jsou hodnoty vždy stejné.
  • Jaká je perioda opakování obrazového vzoru (T_vzor)?
  • Jaká je maximální vzorkovací frekvence, kterou obraz umožňuje pojmout (f_max)?
  • Zajímavých frekvencí je 7, ale jen 1 odpovídá přesně vzoru.
  • Stejnosměrná složka to NENÍ, i když se to tak může zdát.

Vysoké frekvence (zde odpovídají vadám a šumu) a nízké frekvence odpovídají struktuře materiálu. To jsou ty, které jsou blíže ke středu obrázku spektra (našich 7 vybraných).

Vhodné vzorce: $$ f_{vzor} = \frac{1}{T_{vzor}} \; \mathrm{[pix}^{-1}] $$

$$ f_{max} = \frac{1}{T_{max}} \; \mathrm{[pix}^{-1}] $$

$$ pix_{spect} = f_{vzor} \cdot T_{max} \; \mathrm{[pix]} $$

Hodnota pix_spect označuje hledanou vzdálenost v pixelech bodu obrazové frekvence od stejnosměrné složky spektra (středu) v obrazovém frekvenčím spektru.

# Změřte si periodu vzoru
# ... uložte si obrázek na disk 
# ... otevřete si ho v nějakém editoru, ve kterém jste schopni určit souřadnice dvou pixelů
save_image(cardboard_normalized, 'data/cardboard_normalized.png')
True
# Podpůrné indexy a hodnoty
indexes = idx[:, 0, 1]
idx_middle = math.floor(len(indexes)/2)

# Hodnota pixelu indexu stejnosměrné složky
ind_ss = indexes[idx_middle]
indexes = np.delete(indexes, idx_middle)

# Maximální perioda v obraze T_max (rozlišení obrazu) [pix]
T_max = 500 ### získaná hodnota

# Perioda opakování struktury (vzoru) [pix}
T_vzor = 58 ### změřená hodnota

# Frekvence vzoru [pix^-1]
f_vzor = 1 / T_vzor ### vzorec

# Hledaná hodnota frekvence se pozná podle vzdálenosti od středu spektra [pix]
pix_spect = f_vzor * T_max ### vzorec

# Hledání ideální frekvence ze zjištěných možných
diffs = []
for i in indexes:
    pix_from_center = abs(i - ind_ss)
    
    # Nemá smysl odečítat střed sám od sebe
    if pix_from_center != 0:
        diff = abs(pix_spect - pix_from_center)
        diffs.append(diff)

# Převod na numpy array kvůli vyhledání více možností než 1
diffs = np.array(diffs)
    
# Nalezení správných indexů obrazové frekvence
ind = indexes[np.where(diffs == diffs.min())]
    
# Výpis výsledků
print('Správné obrazové frekvence leží na hodnotách pixelů na ose y: ' + str(ind))
print('Spektrum je totiž souměrné podle středu.')
Správné obrazové frekvence leží na hodnotách pixelů na ose y: [241 241 259 259]
Spektrum je totiž souměrné podle středu.

9) Vytvořte finální filtr a odstraňte z obrazu opakující se vzor.

Opět vytvořte filtr pro frekvenční spektrum, který tentokrát ideálně odstraní z obrazu POUZE opakující se vzor. Filtr použijte na původní frekvenční spektrum a aplikujte inverzní fourierovu transformaci.

Pozor: v předchozích případech se takový vzor hledal, teď je nutné ho odstranit!

# Vytvořte vzor pro odstranění opakujícího se vzoru
filter_mask_final = create_filter_mask(cardboard_normalized.shape, [241, 259], [mid]) ### doplňte jak funkci, tak hodnoty
filter_mask_final = negative(filter_mask_final)

# Funguje filtr podle zadání?

# Vytvořte filtrované spektrum a výsledný filtrovaný obraz
spec_mask_filt_final = filter_mag_spec(mag_spec, filter_mask_final) ### doplňte funkci
cardboard_filt_final = inverse_fft(spectrum_shift, filter_mask_final).astype(np.uint8) ### doplňte funkci

plot_images(spec_mask_filt_final, cardboard_filt_final, normalize=True)
No description has been provided for this image

10) Detekujte defekty materiálu. Vhodně vizualizujte.

Segmentujte defekty pomocí vhodného prahování. Cílem je najít pouze největší defekty. Drobnosti lze ignorovat.

Zakreslete nalezené defekty do původního obrázku.

# Pomocná metoda pro vizualizaci
def vizualize(image, image_binary):
    _, _, contours = find_contours(image_binary, 50, fill=False)

    result = to_3_channels(image)
    cv2.drawContours(result, contours, -1, (0, 0, 255), 2)
    return result
defects_bin = segmentation_one_threshold(cardboard_filt_final, 50) ### vhodná metoda segmentace
# defects_bin = filtration_median(defects_bin, 25)
defects_bin = ~cv2.morphologyEx(~defects_bin, cv2.MORPH_OPEN, np.ones((3, 11), np.uint8), iterations=25)

# Vizualizace výsledků
defects_viz = vizualize(cardboard_texture_cropped, defects_bin) 
plot_images(defects_bin, defects_viz)
No description has been provided for this image