Projektor do interaktywnego mapowania obrazu

Projektor do interaktywnego mapowania obrazu

Bączek to tradycyjna zabawka popularna wśród dzieci. Jeśli myślicie, że tak prostej zabawki nie da się urozmaicić za pomocą systemu zawierającego Raspberry Pi… to jesteście w błędzie. Zaprezentowane urządzenie używa projektora i kamery, sprzężonych z OpenCV, aby zabawę bączkiem wprowadzić na zupełnie nowy wymiar.

Autor opisanej konstrukcji ma córkę w wieku przedszkolnym. Jak często bywa z tego rodzaju konstrukcjami, inspiracją do opracowania systemu były jej zabawy. Dziewczynka w przedszkolu nauczyła się puszczać bączek na sznurku, jednak taka zabawa po pewnym czasie się nudzi... Aby uczynić zabawę ciekawszą, autor skonstruował relatywnie proste urządzenie wyposażone w komputer jednopłytkowy Raspberry Pi, kamerę i projektor. System lokalizuje bączek i rzutuje na nim obraz z projektora.

W dalszej części artykułu opisano dokładniej sposób, w jaki aplikacja zainstalowana na komputerze jednopłytkowym dokonuje lokalizacji pozycji zabawki w czasie rzeczywistym. Rozwiązanie tego rodzaju można zastosować nie tylko do zabawy – algorytm analizy obrazu to poważne narzędzie, stosowane w wielu różnych aplikacjach.

Potrzebne elementy

Do budowy systemu potrzebne będą następujące elementy i podzespoły (rysunek 1):

  • komputer jednopłytkowy Raspberry Pi (autor wybrał model 4, ale poprzednie wersje również powinny się sprawdzić);
  • kamera rejestrująca w podczerwieni, na przykład moduł kamery Raspberry Pi PiNoIR V2.1;
  • filtr podczerwieni – taki, który przepuszcza tylko podczerwień, dostępnych jest kilka takich filtrów dla kamery PiNoIR. Autor używa filtra pasmowego, centrowanego na 850 nm z uwagi na wybrany oświetlacz;
  • światło podczerwone – diody LED, emitujące światło o centralnej długości fali równej 850 nm. Można wybrać inną, ale trzeba wtedy zmienić filtr pasmowy;
  • projektor – autor rekomenduje użycie projektora LED;
  • bączek zabawka;
  • reflektor – odbłyśnik podczerwieni w postaci samoprzylepnej taśmy. Funkcję tego elementu mogą spełniać np. naklejki odblaskowe stosowane do poprawy widoczności osób na drodze, jeśli działają także w podczerwieni.
Rysunek 1. Potrzebne elementy składowe systemu

Zasada działania systemu

Kamera systemu pracuje w podczerwieni i jest sprzężona z oświetlaczem, co pozwala jej wykrywać bączek. Koncepcja działania systemu polega na tym, aby projektor wyświetlał obraz na bączku oraz wokół niego, odwzorowujący pozycję oraz dodający ciekawy wizualny efekt. W tym celu system obserwuje w podczerwieni obszar, gdzie ma się poruszać bączek. Ogólny schemat układu został pokazany na rysunku 2.

Rysunek 2. Ogólny schemat budowy i zasady działania systemu

Układ składa się z dwóch zasadniczych elementów – kamery podczerwonej z oświetlaczem i filtrem oraz projektora. Oba elementy podłączone są do komputera jednopłytkowego Raspberry Pi, jak pokazano na rysunku 2. Kamera i projektor muszą być względem siebie nieruchome, aby możliwa była translacja pozycji, wykrywanej przez kamerę, na lokalizację na obrazie, wyświetlanym przez projektor. Źródło podczerwieni oświetla całą powierzchnię poniżej układu. Na wierzchniej stronie bączka przyklejone są odblaskowe kawałki reflektora działające w podczerwieni. Obracając się, zlewają się one w jeden okrąg, jak widać to na fotografii 1. Kamera rejestrująca tylko promieniowanie podczerwone, obserwując scenę, wykrywa światło odbite od reflektorów, dzięki czemu, jak pokazano na rysunku 3, elementy z przyklejonym reflektorem, jak bączek, są wyraźnie widoczne na obrazie.

Fotografia 1. Bączek z przyklejonym reflektorem. Gdy się obraca, reflektory zlewają się w okrąg
Rysunek 3. Biurko autora obserwowane w świetle widzialnym (po lewej) i podczerwieni (po prawej). Widoczne na obrazie reflektory podczerwieni zapewniają bardzo duży kontrast przy obserwacji IR

W ten sposób, dla kamery IR, obracający się bączek jest okręgiem, a gdy się zatrzyma, jest punktem (dokładniej – czterema punktami). System musi zatem umieć wykrywać, rozróżniać i lokalizować takie dwa kształty. Do tego celu autor opracował skrypt, napisany w Pythonie, który korzysta z biblioteki OpenCV do analizy obrazów w czasie rzeczywistym.

Oświetlacz IR i kamera

Kluczowym elementem urządzenia jest kamera rejestrująca w podczerwieni. Jest to w zasadzie zwykła kamera, która nie ma filtra usuwającego podczerwień. Normalne krzemowe matryce CMOS i CCD są czułe nie tylko na światło widzialne, ale także na promienie IR.

Zakres spektralny czułości typowej matrycy CMOS rozciąga się od 400 nm do około 1000 nm, a CCD od 400 nm do 1100 nm. Światło widzialne to zakres od 400 nm do 700 nm, więc pozostaje około 300...400 nm długości promieniowania, które można wykorzystać do innych celów. Typowo, aby obraz z kamery odpowiadał temu, co widzi ludzkie oko, instaluje się przed matrycami specjalne filtry, które blokują podczerwień (promieniowanie elektromagnetyczne powyżej 700 nm).

Autor stosuje oświetlacze o długości emitowanej fali równej 850 nm. Są one niewidoczne dla ludzkiego oka, jednak doskonale widoczne dla zastosowanej kamery – modułu NoIR, czyli bez filtra, klasycznej kamery przeznaczonej do Raspberry Pi. Całość uzupełnia filtr pasmowy 850 nm, który sprawia, że do kamery dociera tylko promieniowanie o takiej długości fali.

Fotografia 2. Oświetlacz zintegrowany z kamerą – wnętrze oraz zewnętrzna strona z zamontowanym filtrem pasmowym IR

Zintegrowany moduł do obrazowania zaprojektowany został w Fusion360 i wydrukowany na drukarce 3D. Integruje on w sobie centralnie umieszczoną kamerę oraz wianuszek ośmiu podczerwonych diod LED, umieszczonych dookoła niej. Moduł ma także miejsce na umieszczenie filtra podczerwonego, który ogranicza światło docierające do kamery. Na fotografii 2 pokazano moduł kamery wraz z elementami składowymi. Diody LED oświetlacza zasilane są z napięcia 5 V z Raspberry Pi.

Rysunek 4. Schemat podłączenia diod IR oświetlacza

Na rysunku 4 pokazano schemat połączenia diod w oświetlaczu. Diody połączone są równolegle po cztery, a następnie szeregowo. To optymalny sposób wykorzystania napięcia zasilania 5 V. Szeregowo z diodami dołączony jest rezystor, którego zadaniem jest stabilizacja prądu diod. Opornik trzeba dobrać do zastosowanych diod, pamiętając, że płynący prąd to czterokrotność prądu znamionowego jednej diody (z uwagi na połączenie równoległe), a spadek napięcia na diodach jest dwukrotnością spadku na pojedynczej diodzie, z uwagi na połączenie szeregowe dwóch LED (efektywnie). W przypadku typowej diody IR LED (5 mm; 850 nm; Optosupply OSRICA5B31A) prąd diody wynosi 20 mA, a spadek napięcia typ. 1,4 V. Dla takiego elementu konieczne jest dobranie opornika o rezystancji zbliżonej do 27,5 Ω, na przykład 30 Ω czy nawet 33 Ω – rekomendowana jest raczej wyższa wartość niż wyznaczona, aby nie przekroczyć rekomendowanego prądu diody (20 mA), nawet gdy napięcie zasilania wzrośnie powyżej 5 V lub któraś z diod będzie wykazywała niższy spadek napięcia niż typowa wartość.

Rysunek 5. Gotowy oświetlacz z zamontowanym opcjonalnym rozpraszaczem

Jeśli wybrane przez nas diody podczerwone mają zbyt mały kąt rozpraszania, to oświetlacz nie będzie równomiernie pokrywał oświetlanej powierzchni. W takiej sytuacji autor konstrukcji rekomenduje zainstalowanie dodatkowego dyfuzora światła, który można wykonać np. z bibuły półprzepuszczalnej lub dowolnego innego materiału dyfuzyjnego, który przepuszcza podczerwień – niektóre materiały, takie jak tworzywa sztuczne, mogą tłumić ten zakres spektralny. Materiał rozpraszający można zamontować na oświetlaczu, jak pokazano na rysunku 5.

Oprogramowanie

Do rozpoznawania obrazu zastosowano narzędzia pochodzące z biblioteki OpenCV. Jest to wieloplatformowa biblioteka funkcji przeznaczonych do obróbki i analizy obrazów, która jest w pełni otwarta. Autorzy kodu tej biblioteki skupiają się głównie na opracowywaniu narzędzi przeznaczonych do analizy obrazu w czasie rzeczywistym, co idealnie nadaje się do omawianego zastosowania.

Jakkolwiek sama biblioteka napisana jest w C++, to istnieją nakładki (tak zwane wrappery), które umożliwiają korzystanie z tej biblioteki w innych językach programowania, takich jak C#, Python czy Java. W tym przypadku autor zdecydował się na napisanie skryptu w Pythonie, który odpowiedzialny będzie za analizowanie obrazu z kamery w czasie rzeczywistym. Na listingu 1 zaprezentowano skrypt, który pracuje w nieskończonej pętli while True i rozpoznaje bączka, używając do tego funkcji HoughCircles(), aby następnie wygenerować efekty wizualne na podstawie analizy obrazu.

Listing 1. Skrypt napisany w Pythonie, który odnajduje na obrazie z kamery bączek i wyświetla wokół niego dodatkowe elementy

import cv2
from datetime import datetime
import math
import numpy as np
import time

WIDTH = 800
HEIGHT = 600
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
# zamknięcie programu, jeśli nie wykryto kamery
if not cap.isOpened():
print("Camera Not Found")
sys.exit()

stagenumber = 0

while True: # Główna pętla programu
_, frame = cap.read() # Przechwycenie obrazu
img = frame.copy()
img1 = img[300 : 600, 140: 620]
today = datetime.today()
if today.second % 2 ==0:
twosecloop = 0
else:
twosecloop = 1
# Import tła
if stagenumber == 1:
stage1=cv2.imread("stage_p.png")
elif stagenumber == 2:
if today.microsecond < 200000:
stage1=cv2.imread("stage_water01.png")
elif today.microsecond < 400000:
stage1=cv2.imread("stage_water02.png")
elif today.microsecond < 600000:
stage1=cv2.imread("stage_water03.png")
elif today.microsecond < 800000:
stage1=cv2.imread("stage_water04.png")
else:
stage1=cv2.imread("stage_water05.png")
elif stagenumber == 3:
stage1=cv2.imread("stage_c.png")
elif stagenumber == 4:
stage1=cv2.imread("stage_w.png")
elif stagenumber == 5:
stage1= img1
else:
stage1= cv2.imread("stage_b.png")
stage = stage1
# konwersja obrazu na skalę szarości
gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
# Detekcja bączka
circles = cv2.HoughCircles(
gray, cv2.HOUGH_GRADIENT, dp=7, minDist=50,
param1=240, param2=60, minRadius=6, maxRadius=15)
# Generowanie efektów wizualnych
if circles is not None:
if stagenumber == 1:
for i in circles[0].astype(‘uint16’):
timeloop=(twosecloop+today.microsecond)/2000000 #loop from 0 to 1, every 2 sec
for n in range(6):

petalx=int((i[2]+40)*math.cos(6.28/6*(n+timeloop*2)))
petaly=int((i[2]+40)*math.sin(6.28/6*(n+timeloop*2)))
cv2.circle(stage,(i[0]+petalx,i[1]+petaly),10,(255,255,255),-1)
cv2.ellipse(stage,(i[0]+petalx,i[1]+petaly),(10,10),0,180,360,(0,0,255),-1)
cv2.circle(stage,(i[0]+petalx,i[1]+petaly),10,(0,0,0),1,cv2.LINE_AA)
cv2.circle(stage,(i[0]+petalx,i[1]+petaly),4,(255,255,255),-1)
cv2.circle(stage,(i[0]+petalx,i[1]+petaly),4,(0,0,0),1,cv2.LINE_AA)
elif stagenumber == 2: # fale
for i in circles[0].astype(‘uint16’):
timeloop=today.microsecond/1000000
rad=int(15*timeloop)
col_b=int(255)
col_g=int(250)
col_r=int(120)
cv2.circle(stage,(i[0],i[1]),15+rad,(col_b,col_g,col_r),int(6*timeloop),cv2.LINE_AA)
cv2.circle(stage,(i[0],i[1]),30+rad,(col_b,col_g,col_r),6,cv2.LINE_AA)
cv2.circle(stage,(i[0],i[1]),45+rad,(col_b,col_g,col_r),int(6*(1-timeloop)),cv2.LINE_AA)
else:
for i in circles[0].astype(‘uint16’):
timeloop=today.microsecond/1000000
rad=int(30*timeloop)
rads=int(15*timeloop)
col_b=int(255)
col_g=int(250)
col_r=int(120)
if timeloop < 0.8:
col_b=int(255)
col_g=int(250)
col_r=int(120)
else:
col_b=int(col_b-(col_b)*(timeloop-0.8)*5)
col_g=int(col_g-(col_g)*(timeloop-0.8)*5)
col_r=int(col_r-(col_r)*(timeloop-0.8)*5)
cv2.circle(stage,(i[0],i[1]),i[2]+rad,(col_b,col_g,col_r),6)
cv2.circle(stage,(i[0],i[1]),i[2]+rad*2,(col_b,col_g,col_r),6)
for n in range(9):
petalx=int((i[2]+10+rad*1.5)*math.cos(6.28/9*(n+timeloop*2)))
petaly=int((i[2]+10+rad*1.5)*math.sin(6.28/9*(n+timeloop*2)))
cv2.circle(stage,(i[0]+petalx,i[1]+petaly),rads,(col_b,col_g,col_r),-1)
cv2.imshow(‘screen’, stage1)
k = cv2.waitKey(2) # wczytanie znaku z klawiatury
if k == ord("s"):
cv2.imwrite("sample.png", stage) # zapisanie obrazu
# zmiana tła
if k == ord("0") or k == 158:
stagenumber = 0
elif k == ord("1") or k == 156:
print (chr(k))
stagenumber = 1
elif k == ord("2") or k == 153:
stagenumber = 2
elif k == ord("3") or k == 155:
stagenumber = 3
elif k == ord("4") or k == 150:
stagenumber = 4
elif k == ord("5") or k == 157:
stagenumber = 5
if k == 27:
break # wyjście przy naciśnięciu klawisza Eszc

cv2.destroyAllWindows() # zamknięcie wszystkich okien
cap.release()

Funkcja HoughCircles() z biblioteki OpenCV to implementacja metody gradientów Hougha, która jest przeznaczona do wykrywania, w przypadku tej funkcji, okręgów. W ogólności transformacja Hougha to metoda używana w systemach widzenia komputerowego do wykrywania kształtów, które można opisać analitycznie – prostych, okręgów itp. Dokładny opis matematyczny tej funkcji wykracza poza ramy tego artykułu, więc do zrozumienia kodu wystarczy nam opis parametrów, z jakimi uruchamiana jest funkcja:

circles = HoughCircles(
InputArray image,
int method,
double dp,
double minDist,
double param1 = 100,
double param2 = 100,
int minRadius = 0,
int maxRadius = 0
)

Przyjmuje ona szereg parametrów, które opisano szerzej w tabeli 1, a zwraca macierz circles, która jest wektorem trzy- lub czterowymiarowym w postaci (x, y, promień) lub (x, y, promień, liczba głosów), gdzie liczba głosów to parametr proporcjonalny do tego, jak bardzo pewien danego okręgu jest algorytm.

Pozostała część skryptu zajmuje się importowaniem obrazu z kamery i jego przygotowaniem:

_, frame = cap.read()
img = frame.copy()
img1 = img[300:600, 140:620]

gdzie cap to obiekt, związany z kamerą, z której pobierany jest obraz o rozdzielczości WIDTH × HEIGHT (stałe definiowane są we wcześniejszej części programu):

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)

Przygotowanie obrazu jest istotne, gdyż pozwala na dopasowanie się do niemalże dowolnej kamery, a także na skalibrowanie obrazu i projektora ze sobą, niezależnie od względnego położenia obu tych elementów.

Dalsza część programu odpowiada za wyświetlanie odpowiedniego tła dla obrazu oraz efektu wizualnego, który podąża za wykrytym bączkiem. Finalnie, dodana jest także obsługa klawiatury:

k = cv2.waitKey(2)

Za jej pomocą można zmieniać tryby działania (od 0 do 5), zapisać wygenerowany obraz (klawisz „s”) lub po prostu wyłączyć program (klawisz „Esc”).

Integracja systemu z projektorem i kalibracja kamery

W pierwszym kroku trzeba zmontować wszystkie elementy razem – projektor wraz z Raspberry Pi i z kamerą z oświetlaczem. Następnie moduł można zamontować na ścianie lub tak jak autor na drzwiach szafy. To drugie rozwiązanie wydaje się bardzo wygodne, gdyż pozwala na zdejmowanie i zakładanie systemu w dowolnym momencie.

Urządzenie jest w całości zmontowane na wspólnej desce. Do niej przymocowany jest projektor (za pomocą miejsc na śruby, przeznaczonych do normalnego montażu), Raspberry Pi oraz moduł kamery. Całość jest następnie montowana w wybranym przez nas miejscu.

Rysunek 6. Montaż urządzenia i jego instalacja na ścianie

Zmontowany system pokazano na rysunku 6.

Jak pokazano na rysunku 7, występuje przerwa i przesunięcie pomiędzy obszarem wyświetlania projektora a obszarem widzianym okiem kamery. Rozmiar ekranu projektora zależy od typu projektora i odległości od podłogi. Trzeba więc dostosować rozmiar tego ekranu tak, aby pasował on do obrazu z kamery. W tym celu należy wyświetlić efekt na podłodze, a następnie obserwować, jak obraz zmienia się względem rzeczywistego. W programie można wykadrować niepotrzebny obszar z obrazu wejściowego kamery.

Rysunek 7. Metoda kalibracji układu poprzez zmianę parametrów wskazanych w kodzie

Na stronie projektu dostępne są dwa obrazy kalibracyjne. Korzystając z nich, można dostosować obrazy do siebie. W tym celu definiowana jest wielkość obrazu z kamery (parametry WIDTH oraz HEIGHT w skrypcie), który jest następnie przycinany (niebieska ramka na rysunku 7). Dodatkowo można też skorzystać z wbudowanych funkcji projektora. Większość projektorów ma funkcję powiększania części obszaru ekranu. Używając tej funkcji, można powiększyć okno wyświetlanego efektu, tak aby wyświetlić coś, co będzie wyglądało jak obraz pełnoekranowy. W ten sposób zmiana kalibracji nie wpływa na szybkość przetwarzania.

Podsumowanie

Po zmontowaniu i skalibrowaniu układu jest on gotowy do działania. Aby uprościć jego użytkowanie można, np. dodać w systemie operacyjnym Raspberry Pi automatyczne uruchamianie skryptu po uruchomieniu zasilania. Dzięki temu od razu po podłączeniu cały system będzie gotowy do działania. Zaprezentowany projekt jest dobrym wstępem do zabawy i nauki OpenCV czy też ogólnie widzenia maszynowego. Zastosowanie tego rodzaju algorytmów oferuje ogromne możliwości automatyzacji systemów, gdyż pozwala komputerowi samodzielnie widzieć i rozpoznawać otaczający nas świat. Platforma Raspberry Pi jest doskonałym narzędziem do nauki OpenCV i podobnych bibliotek. Ten kompaktowy komputer jednopłytkowy oferuje dostatecznie dużo mocy obliczeniowej w swojej najnowszej wersji, aby sprostać wymaganiom stawianym przez większość tego rodzaju algorytmów. Możliwość podłączenia kamery do tego modułu jeszcze bardziej upraszcza testy. Dodatkowo system ten jest na tyle mały, że można go bez problemu zintegrować z różnymi innymi urządzeniami – robotami, infokioskami itp. Tylko wyobraźnia jest tutaj ograniczeniem.

Nikodem Czechowski, EP

Bibliografia:

Artykuł ukazał się w
Elektronika Praktyczna
maj 2022
Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik grudzień 2024

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio listopad - grudzień 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje listopad - grudzień 2024

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna grudzień 2024

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich styczeń 2025

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów