Segmentace obrazu a měření rozměrů

Základní znalostí při zpracování obrazu je nejenom ideálně nasnímat obraz, ale hlavně ze získaného obrazu dostat použitelné informace. Extrakce objektů z obrazu (oddělení objektů od pozadí) se nazývá segmentace. Nejčastěji se využívá metod prahování, hranové detekce nebo kontur (viz link).

Když už se podaří získat z obrazu objekty, je dále potřeba změřit jejich rozměry - a to nejen v pixelech, ale hlavně reálné rozměry v cm nebo dokonce v menších jednotkách.

Na cvičení se bude využívat umělý obrázek s objekty, jejichž rozměry bude třeba automatizovaně získat.

Import knihoven a konfigurace

%run ./library.ipynb

Pomocné funkce

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

Seznam funkcí pro přehlednost:

Úkol 1

Úkol 2


Úkol 1

Úkol je zaměřen na segmentace obrazů, získání kontur objektů a měření poměru rozměrů v cm a pixelech.

1) Nasnímejte papír se vzory a uložte jej do složky data. Obrázek poté načtěte a zobrazte. Nakonec nadefinujte proměné s šířkou a výškou referečního objektu.

Obrázek nastavte tak, aby byl ideálně záběr pouze na papír. HINT: Je možné pomoci si oříznutím obrázku (crop).

img_patterns = load_image('data/0.bmp') ###
img_patterns = crop(img_patterns, 200, 100, 1600, 1000)
# snížení velikosti obrázku pro rychlejší výpočet v interact slideru
img_patterns = cv2.resize(img_patterns, dsize = None, fx=0.5, fy=0.5, interpolation = cv2.INTER_AREA)
plot_images(img_patterns) ###
No description has been provided for this image
ref_width_real = 40 ###
ref_height_real = 80 ###

2) Převeďte obrázek do šedotónu a segmentací získejte masku referenčního obdélníku.

img_patterns_gray = to_gray(img_patterns) ###
plot_images(img_patterns_gray) ###
No description has been provided for this image

Vyberte vhodnou funkci a experimentujte se sliderem (políčko s číselnými hodnotami je editovatelné).

@interact(threshold_range=create_slider(min=0, max=255, description='Threshold range:',))
def seg(threshold_range):
    mask = segmentation_two_thresholds(img_patterns_gray, threshold_range[0], threshold_range[1]) ###
    plot_images(mask)
interactive(children=(IntRangeSlider(value=(0, 255), continuous_update=False, description='Threshold range:\xa…

Optimální hodnoty zaneste do funkce:

mask = segmentation_two_thresholds(img_patterns_gray, 0, 114) ###

3) BONUS: šedotónový obrázek je jen 2D numpy array, proveďte stejnou segmentaci pomocí numpy operací. (1b)

Poznámka 1: for cyklus není numpy operace.

Poznámka 2: bonusové body je možné získat až po vypracování obou notebooků.

def segment_gray_np(img, l_bound, u_bound):
    mask = (img > l_bound) & (img < u_bound) ###
    return mask
mask2 = segment_gray_np(img_patterns_gray, 0, 114)
plot_images(mask)
No description has been provided for this image

4) Převeďte obrázek do do vhodné barevné soustavy, získejte masku referenčního obdélníku.

img_patterns_color = to_hsv(img_patterns) ###
@interact(h_range=create_slider(min=0, max=360, description='Hue:'),
          s_range=create_slider(min=0, max=255, description='Saturation:'),
          v_range=create_slider(min=0, max=255, description='Value:'))
def _(h_range, s_range, v_range):

    lower_bound = (to_intensity(h_range[0]), s_range[0], v_range[0])
    upper_bound = (to_intensity(h_range[1]), s_range[1], v_range[1])
    
    mask = segmentation_two_thresholds(img_patterns_color,lower_bound,upper_bound) ###
    plot_images(mask, apply_mask(img_patterns, mask))
interactive(children=(IntRangeSlider(value=(0, 360), continuous_update=False, description='Hue:\xa0\xa0\xa0\xa…

Optimální hodnoty zaneste do funkcí:

lower_bound = (to_intensity(104), 91, 0)
upper_bound = (to_intensity(156), 255, 255)

mask = segmentation_two_thresholds(img_patterns_color, lower_bound, upper_bound) ###

5) Naleznete konturu která pokrývá nevětší plochu, otestujte aproximace kontury na obdélník.

Výsledná maska by měla ideálně obsahovat pouze referenční obdelník, v obrazu se ale obecně může vyskytovat i šum. Nyní je nutné získat pozici referenčního obdelníku v obraze masky. Nejjednoduším způsobem je vyhledání kontur (obrysů) v obraze pomocí find_contours(). Vyhledání kontur funguje jen na binárním černobílém obraze, proto jsme nejdříve museli využít segmentation_two_thresholds() k získání masky.

Konturu si lze představit jako křivku spojující několik bodů kolem obrysu souvislého objektu. Funkce find_contours příjímá navíc ještě čtyři parametry: nejmenší a největší přípustnou plochu kontury v pixelech, indikátor, zda má být kontura vyplněná a contour approximation method, která je podrobněji popsána zde.

Vzhledem k šumu, který se na každém snímku vyskytuje, prakticky nikdy nenajdeme konturu pouze jednu. Je tedy nutné následně provést filtrování. V našem případě si vystačíme s výběrem kontury, která má největší plochu.

Posledním krokem je validace našeho postupu a vizualizace nalezené kontury pomocí funkce cv2.drawContours().

_, num_contours, contours  = find_contours(mask, external=False) ###
print(f'Found {num_contours} contours.')

# pick only the contour with the biggest area
contour_biggest = max(contours, key=cv2.contourArea)
print(f'Biggest contour area: {cv2.contourArea(contour_biggest)}, coordinates:\n {contour_biggest.reshape((-1, 2)).tolist()}')
contour_drawn = cv2.drawContours(img_patterns.copy(), [contour_biggest], -1, color=(255, 0, 0), thickness=5)
plot_images(contour_drawn)
Found 11 contours.
Biggest contour area: 19946.0, coordinates:
 [[141, 143], [140, 144], [107, 144], [106, 145], [103, 145], [102, 144], [100, 144], [99, 145], [82, 145], [81, 146], [80, 146], [79, 145], [78, 146], [77, 145], [73, 145], [72, 146], [71, 145], [66, 145], [65, 146], [64, 146], [63, 145], [62, 146], [61, 145], [59, 145], [58, 146], [56, 146], [55, 145], [54, 146], [53, 145], [52, 146], [48, 146], [47, 147], [46, 146], [46, 145], [46, 257], [47, 257], [48, 258], [47, 259], [46, 259], [46, 262], [47, 263], [47, 264], [46, 265], [46, 266], [47, 267], [47, 269], [46, 270], [47, 271], [46, 272], [47, 273], [47, 305], [48, 306], [48, 308], [47, 309], [48, 310], [48, 330], [47, 331], [47, 333], [48, 333], [49, 334], [48, 335], [47, 335], [47, 347], [48, 348], [47, 347], [47, 345], [48, 344], [49, 345], [49, 346], [50, 347], [50, 348], [51, 347], [52, 347], [53, 348], [54, 347], [55, 348], [56, 347], [57, 348], [58, 348], [59, 347], [61, 347], [62, 348], [63, 348], [64, 347], [65, 348], [66, 347], [67, 348], [68, 348], [67, 347], [68, 346], [69, 346], [70, 347], [71, 347], [72, 348], [74, 348], [75, 347], [92, 347], [93, 346], [94, 346], [95, 347], [96, 347], [97, 346], [104, 346], [105, 347], [106, 346], [112, 346], [113, 345], [114, 346], [124, 346], [125, 345], [126, 346], [127, 346], [128, 345], [129, 346], [129, 347], [130, 347], [131, 346], [132, 346], [133, 347], [136, 347], [137, 346], [138, 347], [139, 347], [141, 345], [142, 346], [143, 345], [144, 346], [144, 347], [147, 347], [146, 346], [147, 345], [147, 344], [146, 343], [146, 342], [147, 341], [147, 340], [146, 339], [146, 322], [145, 321], [146, 320], [146, 303], [145, 302], [146, 301], [145, 300], [146, 299], [146, 291], [145, 290], [146, 289], [145, 288], [146, 287], [146, 284], [145, 283], [146, 282], [146, 281], [145, 280], [145, 279], [146, 278], [146, 277], [145, 276], [146, 275], [146, 274], [145, 273], [145, 272], [146, 271], [146, 270], [145, 269], [146, 268], [145, 267], [145, 263], [146, 262], [146, 259], [145, 258], [145, 257], [146, 256], [145, 255], [145, 240], [146, 239], [145, 238], [145, 237], [146, 236], [145, 235], [145, 224], [146, 223], [145, 222], [145, 215], [146, 214], [145, 213], [145, 205], [144, 204], [145, 203], [145, 184], [144, 183], [145, 182], [144, 181], [145, 180], [145, 172], [144, 171], [145, 170], [145, 146], [144, 145], [144, 144], [143, 143], [142, 144]]
No description has been provided for this image

Z výsledných souřadnic vidíme, že kontura netvoří přesný obdelník, proto ji musíme obdelníkem aproximovat. To lze pomocí funkce cv2.minAreaRect. Návratovou hodnotou této funkce je tuple - (střed obdélníku (x,y), (výška, šířka), úhel rotace obdélníku). Jedna otázka zní, co je výška a co je šířka? Musíte to vždycky kontrolovat okem.

Pomocná funkce cv2.boxPoints() převádí nalezený obdelník z formátu (střed obdélníku (x,y), (šířka, výška), úhel rotace obdélníku) na 4 rohové body obdélníku ve formátu (x, y). To se může hodit např. k vizualizaci (lze použít jako vstup pro cv2.drawContours()). Pozor, pořadí vrácených bodů není zaručeno.

rect = cv2.minAreaRect(contour_biggest) ###
print(f'Rect tuple: {rect}')
print()
print(f'(cx, cy)={rect[0]}')
print(f'(height, width)={rect[1]}')
print(f'angle={rect[2]}')
print()
print(f'Rect points: {cv2.boxPoints(rect).tolist()}')

box_points_drawn = img_patterns.copy()
for p in cv2.boxPoints(rect):
    cv2.circle(box_points_drawn,(int(p[0]),int(p[1])), 10, (0,0,255), -1)
plot_images(box_points_drawn)
Rect tuple: ((96.02055358886719, 245.59112548828125), (204.4617919921875, 100.4571533203125), 89.54528045654297)

(cx, cy)=(96.02055358886719, 245.59112548828125)
(height, width)=(204.4617919921875, 100.4571533203125)
angle=89.54528045654297

Rect points: [[44.98222351074219, 143.76206970214844], [145.4362335205078, 142.96482849121094], [147.0588836669922, 347.420166015625], [46.60487365722656, 348.2174072265625]]
No description has been provided for this image

6) Nalezenou šířku referenčního obdelníku v pixelech můžeme konečně využít k získání poměru skutečné šířky obdélníku v reálných jednotkách a pixelové šířky obdélníku v obraze. Tento poměr budeme následně potřebovat k výpočtu rozměrů ostatních neznámých objektů.

ref_width_image, ref_height_image = rect[1][1], rect[1][0] ###
real_image_ratio = (ref_width_real / ref_width_image)### 
print(f'Ratio between real width and image width: {real_image_ratio}')
Ratio between real width and image width: 4.032513427734375

7) Zkontrolujte si, že přepočtené hodnoty odpovídají reálné velikosti referenčního obdélníku.

print(f'real size: {(ref_width_real, ref_height_real)}')
print(f'recalculated size: {(ref_width_image*real_image_ratio, ref_height_image*real_image_ratio)}')
real size: (40, 80)
recalculated size: (650.4465817943216, 237.09641903908923)

ref_height_image / ref_height_real
2.555772399902344
ref_width_image / ref_width_real
2.5114288330078125

Úkol 2

Úkol 1 máme za sebou a už víme poměr mezi rozměrem v cm a pixelech, tím pádem už můžeme naměřit rozměry ostatních objektů na obrázku s neznámými rozměry.

@interact(h_range=create_slider(min=0, max=360, description='Hue:'),
          s_range=create_slider(min=0, max=255, description='Saturation:'),
          v_range=create_slider(min=0, max=255, description='Value:'))
def _(h_range, s_range, v_range):

    lower_bound = (to_intensity(h_range[0]), s_range[0], v_range[0])
    upper_bound = (to_intensity(h_range[1]), s_range[1], v_range[1])
    
    mask = segmentation_two_thresholds(img_patterns_color,lower_bound,upper_bound) ###
    plot_images(mask, apply_mask(img_patterns, mask))
interactive(children=(IntRangeSlider(value=(0, 360), continuous_update=False, description='Hue:\xa0\xa0\xa0\xa…
lower_bound = (to_intensity(0), 105, 0)
upper_bound = (to_intensity(360), 255, 255)

mask = segmentation_two_thresholds(img_patterns_color, lower_bound, upper_bound) ###

1) Libovolným způsobem získejte masky objektů s neznámými skutečnými rozměry.

patterns_mask = mask ###
plot_images(patterns_mask)
No description has been provided for this image

2) Nalezněte v nové masce snímku kontury stejně jako v případě referenčního objektu a následně proveďte filtrování kontur podle jejich obsahu. Prahovou hodnotu obsahu v pixelech (threshold) je nutno zvolit experimentálně.

_, num_contours, contours = find_contours(patterns_mask, external=False)
print(f'Found {num_contours} contours.')

# Filter out noise
threshold = 300 ###
contours =  [c for c in contours if cv2.contourArea(c) > threshold]
print(f'After filtering, {len(contours)} contours remained.')

contour_drawn = cv2.drawContours(img_patterns.copy(), contours, -1, color=(255, 0, 0 ), thickness=10)
plot_images(contour_drawn)

# Sort contours by area. Just for better debugging.
contours.sort(key=cv2.contourArea, reverse=True)
Found 30 contours.
After filtering, 5 contours remained.
No description has been provided for this image

3) Nalezněte skutečné rozměry objektu

Nyní potřebujeme pro jednotlivé kontury zjistit jejich skutečné rozměry, které chceme vizualizovat do výsledného obrázku.

Pro každou konturu tedy získáme jeji obdélníkovou aproximaci pomocí cv2.minAreaRect. Následně můžeme vypočítat skutečnou šířku a výšku objektu, díky předešlému vypočítanému poměru mezi skutečnou šířkou a pixelovou šířkou. Posledním krokem je volání funkce draw_real_sizes(), která se do vstupního obrázku pokusí vykreslit rozměry nalezeného objektu.

Poznámka: pokud funkce draw_real_sizes vyhazuje chybu, změňte crop obrázku, jelikož funkce nemá dost místa, aby na okrajích vypsala text.

# create a copy of original image
sizes_drawn = img_patterns.copy()
im2re = ref_width_image / ref_width_real
re2im = 1 / im2re

for c in contours:
    rect = cv2.minAreaRect(c) ###
    shape_width, shape_height = rect[1][1], rect[1][0] ### 
    real_width = re2im * shape_width ###
    real_height = re2im * shape_height ###
    print(rect[1], real_width, real_height)
    
    cv2.drawContours(sizes_drawn, [c], -1, color=(255, 0, 0 ), thickness=5)
#     def draw_real_sizes(img, rect, width_text, height_text, lbl_size_scale=2, lbl_color=(0, 0, 255), lbl_thickness=8):
    sizes_drawn = draw_real_sizes(
        sizes_drawn, ###
        rect, ###
        real_height, ###
        real_width, ###
        lbl_size_scale=.7,
        lbl_color=(0, 0, 255),
        lbl_thickness=1
    ) ###

plot_images(sizes_drawn)
(204.22195434570312, 143.8769073486328) 57.28886499097752 81.3170381981785
(100.99998474121094, 204.99996948242188) 81.62682803832577 40.216144456798446
(127.42440795898438, 101.12496948242188) 40.26591084458864 50.737813584139886
(205.48236083984375, 52.75676727294922) 21.006674200584484 81.81890648828293
(58.79618835449219, 161.300537109375) 64.22660080564314 23.411449124789627
No description has been provided for this image