Hloubkové senzory - měření objektů¶
Cvičení je zaměřené na práci s hloubkovými mapami vzniklých použitím tzv. hloubkových senzorů. V laboratoři jsou k dispozici hloubkové senzory založené na několika principech zisku hloubkových map. Jedná se o kamery typu Kinect, RealSense nebo Time-of-Flight (ToF). Souhrně se často označují jako RGBD kamery, kde k standardní barevné informaci RGB přibyde informace hloubková "D" (depth).
Vytvořit stereokameru lze i pomocí dvou obyčejných kamer. Podrobný návod pro zájemce je k dispozici na příklad zde.
Typy systémů¶
Microsoft Kinect | Intel RealSense | Basler ToF | |||
---|---|---|---|---|---|
Postaven na principu promítání laserového vzoru a měření jeho deformace infračervenou kamerou. | Využívá stejného principu jako Kinect, pouze má jiný vzor. | Využívá promítání světla z více LED zdrojů a měří čas jeho návratu (odrazu). | |||
Je náchylný na měření venku. Denní světlo kazí obraz. | Měl by být vhodný i pro měření venku. | Je náchylný na měření venku. Denní světlo kazí obraz. | |||
Více Kinectů vedle sebe si vzájemně kazí vzory. | Nejnovější a nejmenší senzor na trhu. | Průmyslová konstrukce pro běh 24/7. |
Využití¶
Kromě zjevného využití RGBD kamer k měření vzdálenosti objektu od senzoru (např. precizní robotická manipulace, 3D skeny, výpočet objemu součástky) jsou kamery užitečné i pro robustnější detekce a klasifikace objektů.
Příklad:
Obrázek vlevo nasnímaný RGBD kamerou představuje naměřenou hloubku pomocí barevné škály. Nelze však jednoduše určit, zda se jedná o jablko nebo pomeranč. Je potřeba použít dodatečnou barevnou informaci.
Naopak samotná 2D kamera by nebyla schopna rozpoznat rozdíl mezi plochou nálepkou ovoce a skutečným hmotným ovocem.
To umožňuje např. detekovat hloubku jablka ve stromu a zároveň jeho zralost dle barvy.
Hloubková informace v synergii s tou barevnou tak poskytuje nové souvislosti v obrazových datech a umožňuje pokročilejší analýzy, nikoliv jen měření vzdáleností.
Úkol¶
Cílem cvičení je využít hloubkovou mapu ze senzoru Basler ToF pro detekci zkoumaného objektu - zavěšené dřevěné koule mimikující jablko. Dále je potřeba vyfiltrovat dřevěnou krychli, kterou měřit nechceme a výstup vhodně vizualizovat.
Úloha je inspirovaná řešením z aplikovaného výzkumu samosběrného robota [1]:
Import knihoven a konfigurace¶
import os
import io
import builtins
import cv2
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patheffects import withStroke
from improutils import *
%matplotlib inline
np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})
1) Nasnímejte data hloubkovou kamerou Basler ToF¶
Pro práci s hloubkovou kamerou využijte software Basler ToF Viewer. Kameru připojte k PC, nasnímejte zkoumaný objekt a výstup uložte. Typ souboru při ukládání vyberte jako "Points intensity" (končí příponou points-intensity.pcd).
Dbejte na to, aby při pořizování dat byla kamera nastavena rovnoběžně s pozadím, a to bez rušivých elementů v záběru, např. okraj monitoru apod.
Usnadnit snímání Vám může nastavení "Maximum depth [mm]" v programu, který vyfiltruje vše, co je dále, než zvolený threshold.
2) Načtěte data ve formátu point cloud .pcd (text)
a z dat získejte hloubkovou mapu. Mapu zobrazte.¶
a) Zobrazte strukturu .pcd dat¶
filepath = "images/tof640-20gm-22731198-0013-points_intensity.pcd"
filepath = "images/tof640-20gm-22731198-0013-points_intensity.pcd"
with builtins.open(filepath, 'r') as f: ### soubor s pcd daty
pcd = f.readlines()
header = pcd[:11]
data = pcd[11:]
for line in header : # zobrazení hlavičky
print(line)
for line in data : # zobrazení několika bodů
print(line)
break
# .PCD v0.7 - Point Cloud Data file format VERSION 0.7 FIELDS x y z rgb SIZE 4 4 4 4 TYPE F F F F COUNT 1 1 1 1 WIDTH 640 HEIGHT 480 VIEWPOINT 0 0 0 0 -1 0 0 POINTS 307200 DATA ascii nan nan nan 0
b) Převeďte .pcd data na hloubkovou mapu¶
Informace v hlavičce .pcd souboru vám mimo jiné prozradila, že data jsou ve formátu 4 float hodnot x, y, z, rgb. Při pořizování dat byla kamera nastavena rovnoběžně s pozadím, tudíž nás budou zajímat pouze z hodnoty -> hloubková mapa.
Souřadnice z = 0
je dle výrobce na přední straně krytu kamery a od ní dále se měří vzdálenost. Pixely s nespolehlivými údaji o vzdálenosti (např. poblíž hran objektů) nebo pixely, které představují objekty v oblastech mimo ROI (vámi nastavená maximální hloubka), mají hodnotu NaN.
# Funkce pro převod .pcd dat do hloubkové mapy o velikosti height x width
def pcd_to_depth(pcd, height, width):
data = pcd
data = [float(x.split(' ')[2]) for x in data] # extrakce hloubkové hodnoty
data = np.reshape(data, (height, width))
return data
depths = pcd_to_depth(data, 480, 640) ###
c) Normalizujte a zobrazte data¶
Pro využití automatické segmentace je třeba data převézt na typ uint8. Pro převedení dat do rozsahu [0, 255] je třeba použít normalizaci, OpenCV a improutils obsahují vhodnou funkci.
Normalizovaná data zobrazte pomocí čtyř různých colormap, vhodných pro hloubková data.
def subplot_image(image, subplots=(2, 2), cmaps=['gray'], normalize=False, ticks_off=True, title_size=12):
"""
Plots the same image in multiple subplots with different color maps.
Parameters
----------
image : ndarray
Image to be shown.
subplots : tuple
Shape of the subplot grid (rows, columns).
cmaps : list
List of colormaps to be used for each subplot.
normalize : bool
If True, image will be normalized.
ticks_off : bool
If True, axis decorations will be hidden.
title_size : int
Size of the title.
Returns
-------
None
"""
rows, cols = subplots
fig, axes = plt.subplots(rows, cols, figsize=(cols * 6, rows * 6))
# Flatten axes array if it's 2D for easier iteration
axes = axes.ravel()
# Determine number of subplots and iterate
num_subplots = min(len(axes), len(cmaps))
for i in range(num_subplots):
ax = axes[i]
if ticks_off:
ax.axis('off')
# Set colormap and display the image
cmap = cmaps[i] if i < len(cmaps) else 'gray'
norm = Normalize() if normalize else NoNorm()
ax.imshow(image, cmap=plt.get_cmap(cmap), norm=norm)
ax.set_title(f'Cmap: {cmap}', fontsize=title_size)
# Hide any unused subplots if there are fewer cmaps than grid cells
for j in range(num_subplots, len(axes)):
fig.delaxes(axes[j])
plt.tight_layout()
plt.show()
c) Normalizujte a zobrazte data¶
Pro využití automatické segmentace je třeba data převézt na typ uint8. Pro převedení dat do rozsahu [0, 255] je třeba použít normalizaci, OpenCV a improutils obsahují vhodnou funkci.
Normalizovaná data zobrazte pomocí čtyř různých colormap, vhodných pro hloubková data.
depths_normalized = normalize(depths) ### normalizace dat
subplot_image(depths_normalized, subplots=(2, 2), cmaps=['gray', 'viridis', 'plasma', 'inferno']) ### zobrazení s různými colormapy
np.unique(depths_normalized)
array([ 0, 2, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 145, 147, 148, 149, 151, 158, 162, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 210, 211, 212, 215, 216, 218, 219, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255], dtype=uint8)
3) Segmentujte objekty¶
Při použití klasické segmentace není vhodné segmentovat normalizovaná data, jelikož by segmentace nebyla založena na reálných datech. Funkce pro automatické segmentování v improutils používá Otsouvu metodu, která u originálních a normalizovaných bude segmentovat stejně.
Využijte automatickou segmentaci na normalizovaných datech a manuální pro originální i normalizovaná data. Výsledné 3 masky zobrazte.
Pro volbu vhodných hodnot při manuální segmentaci originálních hodnot si můžete pomoci např. matplotlib histogramem (pozor na NaN hodnoty) či podstatou, jaká data hloubková kamera zaznamenává.
Dále pracujte jen s nejlepším segmentace.
img_seg_auto = segmentation_auto_threshold(depths_normalized) ###
img_seg_org = segmentation_two_thresholds(depths, -800, 0) ###
img_seg_norm = segmentation_two_thresholds(depths_normalized, 10, 255) ###
plot_images(depths_normalized, img_seg_auto, img_seg_org, img_seg_norm)
4) Získejte masku naměřených objektů¶
Pro měření objektů je třeba získat kontury objektů. Počet kontur by měl odpovídat počtu zavěšených objektů. Filtrací odstraníme nekruhové objekty a šum.
Kontury vyfiltrujte minimální plochou a na základě jejich kulatosti. Kulatost je definována vzorcem $\dfrac{4 * \pi * plocha}{obvod^2}$ [2]. Pro některé výpočty týkajících se vlastností kontury budete muset použít vhodné funkce z OpenCV2 či improutils.
Zobrazte novou masku na základě vyfiltrovaných kontur. Pro hledání kontur využijte jednu ze 3 vytvořených masek.
# contour_drawn, count, contours = find_contours(img_seg_norm, min_area=800, max_area=2000, fill=True, external=True) ###
contour_drawn, count, contours = find_contours(img_seg_norm, min_area=800, max_area=20000, fill=True, external=True) ###
print(f"Počet nalezených kontur: {count}")
plot_images(contour_drawn)
Počet nalezených kontur: 4
import math
filtered_image = np.zeros_like(contour_drawn) # prázdný obraz pro zápis přijatých kontur
passed_contours = []
# Filtrování kontur na základě jejich kulatosti
for i, contour in enumerate(contours):
area = cv2.contourArea(contour) # obsah kontury
# r = math.sqrt(area / math.pi)
# perimeter = 2 * math.pi * r # obvod kontury
perimeter = cv2.arcLength(contour, False)
circularity = 4 * math.pi * area / (perimeter**2) # porovnání kulatosti kontury a ideálního kruhu
accept = "NE" # flag pro výpis
print(area, circularity)
if 0.5 < circularity < 1: # doplňte vhodné hodnoty pro odfiltrování nekulatých objektů
accept = "ANO"
passed_contours.append(contour)
print(f"Kontura {i}: Plocha = {area:.2f}, Obvod = {perimeter:.2f}, Kulatost = {circularity:.4f}, Přijímáme = {accept}")
cv2.drawContours(filtered_image, passed_contours, -1, 255, -1) # vykreslení všech přijatých kontur do jednoho obrazu
plot_images(filtered_image)
1835.0 0.6639042195822296 Kontura 0: Plocha = 1835.00, Obvod = 186.37, Kulatost = 0.6639, Přijímáme = ANO 832.5 0.556653585381271 Kontura 1: Plocha = 832.50, Obvod = 171.27, Kulatost = 0.5567, Přijímáme = ANO 1248.5 0.6631665107259167 Kontura 2: Plocha = 1248.50, Obvod = 153.81, Kulatost = 0.6632, Přijímáme = ANO 6343.5 0.4178398444944354 Kontura 3: Plocha = 6343.50, Obvod = 436.78, Kulatost = 0.4178, Přijímáme = NE
5) Slučte hloubkovou informaci s maskou vyfiltrovaných jablek¶
OpenCV obsahuje vhodnou funkci pro logické sloučení dvou obrazů. Výsledek vizualizujte pomocí vhodné colormapy.
ball_depth = cv2.bitwise_and(depths_normalized,contour_drawn) #
subplot_image(ball_depth, subplots=(2,1), cmaps=['gray', 'viridis', 'plasma', 'inferno'])
6) Nalezněte koule, které jsou nejdále a nejblíže od kamery¶
a) Vizualizujte indexy přijatých kontur (koulí) na hloubkovém obrazu z předchozího kroku¶
number_contours_img = ball_depth.copy()
for i, contour in enumerate(contours):
center, size, _ = cv2.minAreaRect(contour) # vhodná funkce cv2 na obdélníkové ohraničení kontury
x, y = center
w, h = size
x, y = int(x), int(y)
number_contours_img = cv2.putText(number_contours_img, str(i) , (x, y-25), cv2.FONT_HERSHEY_SIMPLEX, 1, 255, 1, cv2.LINE_AA)
plot_images(number_contours_img) ###
b) Vypište index koule visící nejblíže a nejdále od kamery¶
Nezapomeňte správně pracovat s NaN hodnotami.
ball_depth
array([[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], dtype=uint8)
results = []
for contour in contours:
contour_mask = np.zeros(contour_drawn.shape, dtype=np.uint8) # příprava masky pro aplikaci na originální data
# cv.drawContours(image, contours, contourIdx, color[, thickness[]]])
contour_mask = cv2.drawContours(contour_mask, [contour], -1, 255, -1) # vykreslení kontury do masky
... ### aplikace masky, výběr nejbližšího bodu a uložení
d = cv2.bitwise_and(depths_normalized, contour_mask) #
d[d==0] = 255
results.append(d.min())
min_depth_index = np.argmin(results)
max_depth_index = np.argmax(results)
print('Nejblíže ke kameře je koule: č.{}'.format(max_depth_index))
print('Nejdál ke kameře je koule: č.{}'.format(min_depth_index))
Nejblíže ke kameře je koule: č.2 Nejdál ke kameře je koule: č.0