Mój projekt polegał na zbudowaniu systemu rozpoznawania prostych figur geometrycznych przy użyciu płytki ESP32 z wbudowaną kamerą oraz matrycą LED RGB WS2812B 16x16. Główna idea polegała na tym, aby za pomocą kamerki zamontowanej na płytce ESP32 rejestrować obrazy, przesyłać je do komputera, rozpoznawać kształty na zdjęciach, a następnie wyświetlać rozpoznane figury na matrycy LED.
1. Płytka ESP32 -CAM z wbudowaną kamerką TY-OV2640
2. Matryca LED RGB WS2812B 16x16
3. Kabel USB
Projekt nie wymagał żadnego szczególnego sprzętu do jego wykonania- był on zorientowany stricte na prace z kodem.
Mój projekt polega na stworzeniu systemu rozpoznawania prostych figur geometrycznych, takich jak koło, kwadrat, prostokąt i trójkąt, przy użyciu płytki ESP32 CAM z kamerą oraz matrycy LED RGB 16x16 WS2812B. Projekt ten obejmuje zarówno część sprzętową, jak i programistyczną, umożliwiającą przetwarzanie obrazu, rozpoznawanie kształtów oraz ich wizualizację na matrycy LED.
Do realizacji projektu potrzebne są:
- płytka ESP32 CAM z kamerą,
- matryca LED RGB WS2812B 16x16,
- kabel USB do podłączenia płytki ESP32 do komputera.
Etap 1: Połączenie ESP32 z matrycą LED
Pierwszym krokiem jest prawidłowe połączenie płytki ESP32 z matrycą LED. Matryca posiada trzy złącza: żeńskie, męskie oraz zasilające. Skupiamy się na żeńskim złączu, które zawiera trzy piny: Din (przesył danych), GND (uziemienie) i 5V (zasilanie). Podłączamy je w następujący sposób:
- Din do pinu D4(w moim przypadku) na płytce ESP32,
- GND do minusa na płytce,
- 5V do plusa na płytce.
Etap 2: Kod na ESP32
Kod odpowiedzialny za komunikację z matrycą i kamerą został podzielony na dwie części. Pierwszy z nich, "finalversion-zdjecie", jest wgrywany na płytkę ESP32. Ten program odpowiada za wykonanie zdjęcia, przesłanie go do komputera za pośrednictwem serwera HTTP oraz odbiór informacji zwrotnej na temat rozpoznanego kształtu, który następnie jest rysowany na matrycy LED. Przed wgraniem tego kodu na płytkę, należy załadować plik Micropython ("micropython_camera_feeeb5ea3_esp32_idf4_4(2).bin"), który należy pobrać z podanego linku: https://github.com/lemariva/micropython-camera-driver/blob/master/firmware/micropython_camera_feeeb5ea3_esp32_idf4_4.bin
Etap 3: Kod na komputerze
Druga część kodu, "finalversion-obraz", jest uruchamiana na komputerze i pełni kluczową rolę w przetwarzaniu obrazów. Program ten tworzy serwer HTTP, dzięki któremu komunikuje się z płytką ESP32. Wykorzystując bibliotekę OpenCV, analizuje przesłane zdjęcia, rozpoznaje kształty i zwraca informację o rozpoznanym kształcie do ESP32. Ze względu na rozmiar i złożoność biblioteki OpenCV, kod ten nie mógłby być uruchamiany bezpośrednio na ESP32, dlatego operacje przetwarzania obrazu odbywają się na komputerze.
Etap 4: Konfiguracja i uruchomienie projektu
Aby wszystko działało poprawnie, należy:
- Wgrać kod "finalversion-zdjecie" na płytkę ESP32 za pomocą środowiska Thonny.
- Uruchomić kod "finalversion-obraz" na komputerze, co spowoduje uruchomienie serwera HTTP. Otrzymane IP serwera wprowadzamy do kodu na ESP32 w odpowiednie miejsce. Należy także uzupełnić nazwę sieci Wi-Fi oraz hasło.
- Po uruchomieniu płytki ESP32, jej adres IP zostanie wyświetlony. Wklejamy go w odpowiednie miejsce w kodzie na komputerze, co finalizuje konfigurację i umożliwia poprawne działanie projektu.
finalversion-zdjecie
import machine
import neopixel
import camera
import urequests
import network
import uasyncio as asyncio
import json
# Konfiguracja matrycy LED (16x16 WS2812B)
LED_PIN = 4 # Pin podłączony do matrycy
WIDTH = 16
HEIGHT = 16
NUM_PIXELS = WIDTH * HEIGHT
np = neopixel.NeoPixel(machine.Pin(LED_PIN), NUM_PIXELS)
# Konfiguracja Wi-Fi
def connect_to_wifi(ssid, password):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print("Connecting to WiFi...")
wlan.connect(ssid, password)
while not wlan.isconnected():
pass
print("Connected! Network details:", wlan.ifconfig())
# Funkcja czyszczenia matrycy
def clear_matrix():
for i in range(NUM_PIXELS):
np[i] = (0, 0, 0)
np.write()
# Funkcja do rysowania smutnej buźki
def display_sad_face():
clear_matrix()
sad_face_pixels = [
(7, 3), (8, 3), # Górna część
(5, 3), (6, 3), (9, 3), (10, 3),
(4, 4), (11, 4),
(3, 5), (12, 5),
(3, 6), (12, 6),
(3, 7), (12, 7),
(3, 8), (12, 8),
(3, 9), (12, 9),
(3, 10), (12, 10),
(4, 11), (11, 11),
(5, 12), (6, 12), (9, 12), (10, 12),
(7, 12), (8, 12),
(6,6), (9,6),
(5, 9), (6, 9),(7, 9), (8, 9), (9, 9), (10, 9),
(5,10), (10,10)
]
for x, y in sad_face_pixels:
np[x + y * WIDTH] = (255, 0, 255) # Fioletowy
np.write()
# Funkcje wyświetlania wzorów na matrycy
def display_triangle():
clear_matrix()
base_y = HEIGHT - 1 # Dolny rząd matrycy (podstawa trójkąta)
apex_y = 0 # Górny rząd matrycy (wierzchołek trójkąta)
start_x = 0 # Początek zapalania pikseli w rzędzie
end_x = WIDTH - 1 # Koniec zapalania pikseli w rzędzie
for y in range(base_y, apex_y - 1, -1): # Iteracja od dołu matrycy do góry
for x in range(start_x, end_x + 1):
np[x + y * WIDTH] = (0, 255, 0) # Zielony
start_x += 1 # Przesuń początek w prawo
end_x -= 1 # Przesuń koniec w lewo
np.write() # Wyślij dane do matrycy
np.write()
# Poprawione rysowanie koła
def display_circle():
clear_matrix()
# Zdefiniowane współrzędne pikseli, które tworzą okrąg
circle_pixels = [
(7, 3), (8, 3), # Górna część
(5, 3), (6, 3), (9, 3), (10, 3),
(4, 4), (11, 4),
(3, 5), (12, 5),
(3, 6), (12, 6),
(3, 7), (12, 7),
(3, 8), (12, 8),
(3, 9), (12, 9),
(3, 10), (12, 10),
(4, 11), (11, 11),
(5, 12), (6, 12), (9, 12), (10, 12),
(7, 12), (8, 12) # Dolna część
]
# Zapalanie pikseli na podstawie zdefiniowanych współrzędnych
for x, y in circle_pixels:
np[x + y * WIDTH] = (0, 0, 255) # Niebieski
np.write()
# Funkcja do rysowania kwadratu
def display_square():
clear_matrix()
for x in range(4, 12):
for y in range(4, 12):
np[x + y * WIDTH] = (255, 0, 0) # Czerwony
np.write()
# Funkcja do rysowania prostokąta
def display_rectangle():
clear_matrix()
for x in range(3, 13):
for y in range(5, 11):
np[x + y * WIDTH] = (255, 255, 0) # Żółty
np.write()
# Funkcja do robienia zdjęcia
def take_photo():
try:
print('Taking photo...')
camera.init(0, format=camera.JPEG)
buffer = camera.capture()
camera.deinit()
return buffer
except Exception as e:
print(f"Error taking photo: {e}")
camera.deinit()
return None
# Wysyłanie zdjęcia do serwera
async def process_photo():
server_url = "http://192.168.242.207:5000/upload" # Zmień na adres swojego serwera Flask
try:
print("Processing photo...")
photo = take_photo()
if photo:
headers = {'Content-Type': 'image/jpeg'}
print("Sending photo to server...")
response = urequests.post(server_url, data=photo, headers=headers)
if response.status_code == 200:
result = response.json()
print(f"Server response: {result}")
shape = result.get("shape", None) # Oczekujemy, że serwer zwraca pojedynczy kształt
if shape:
if shape == "Unknown":
display_sad_face() # Rysowanie smutnej buźki
else:
draw_shape_on_matrix(shape) # Rysowanie kształtu na matrycy
else:
print("No shape detected.")
display_sad_face() # Rysowanie smutnej buźki
return shape
else:
print(f"Server error: {response.text}")
return None
else:
print("No photo taken.")
return None
except Exception as e:
print(f"Error sending photo: {e}")
return None
# Funkcja rysowania kształtu na matrycy
def draw_shape_on_matrix(shape):
print(f"Drawing shape: {shape}")
if shape == "Square":
display_square()
elif shape == "Rectangle":
display_rectangle()
elif shape == "Triangle":
display_triangle()
elif shape == "Circle":
display_circle()
else:
display_sad_face() # Rysowanie smutnej buźki w przypadku nierozpoznanego kształtu
# Główna funkcja
async def main():
# Przetwarzanie zdjęcia i wysyłanie go do serwera
await process_photo()
print("Photo processed. Program completed.")
# Uruchamianie programu
try:
connect_to_wifi("Hot chicken spot", "1916winwin") # Zmień na swoje dane Wi-Fi
asyncio.run(main())
except Exception as e:
print(f"Critical error: {e}")
finalversion-obraz
from flask import Flask, request, jsonify
import cv2
import numpy as np
import requests
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # Maksymalny rozmiar: 10 MB
def preprocess_image(image_path):
"""Wczytaj obraz i przetwórz go (progowanie, usuwanie szumu)."""
image = cv2.imread(image_path, cv2.IMREAD_COLOR)
if image is None:
raise ValueError(f"Nie udało się wczytać obrazu: {image_path}")
# Konwersja do skali szarości
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Usunięcie szumu (rozmycie Gaussa)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# Adaptacyjne progowanie
thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 11, 3)
return thresh, image
def detect_shapes(thresh, original_image):
"""Wykryj jeden najbardziej istotny kształt na obrazie."""
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
largest_contour = None
largest_area = 0
detected_shape = "Unknown"
for contour in contours:
# Filtracja konturów na podstawie pola
area = cv2.contourArea(contour)
if area < 500: # Pomijamy małe kontury
continue
# Wybór największego konturu
if area > largest_area:
largest_contour = contour
largest_area = area
if largest_contour is not None:
# Aproksymacja konturu
epsilon = 0.02 * cv2.arcLength(largest_contour, True)
approx = cv2.approxPolyDP(largest_contour, epsilon, True)
# Analiza kształtu
if len(approx) == 3:
detected_shape = "Triangle"
elif len(approx) == 4:
# Weryfikacja kwadratu/prostokąta
x, y, w, h = cv2.boundingRect(approx)
aspect_ratio = float(w) / h
if 0.95 <= aspect_ratio <= 1.05:
detected_shape = "Square"
else:
detected_shape = "Rectangle"
elif len(approx) > 4:
# Weryfikacja okręgu
((x, y), radius) = cv2.minEnclosingCircle(largest_contour)
circle_area = np.pi * radius * radius
if abs(largest_area - circle_area) < 0.1 * largest_area:
detected_shape = "Circle"
# Rysowanie konturu i nazwy kształtu na obrazie
cv2.drawContours(original_image, [largest_contour], -1, (0, 255, 0), 2)
x, y, w, h = cv2.boundingRect(largest_contour)
cv2.putText(original_image, detected_shape, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX,
0.5, (255, 0, 0), 2)
return detected_shape
@app.route('/upload', methods=['POST'])
def upload_image():
try:
# Pobieranie obrazu z żądania
file = request.data
nparr = np.frombuffer(file, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if img is None:
return jsonify({"error": "Invalid image format"}), 400
# Zapisywanie zdjęcia tymczasowo na serwerze
temp_image_path = "received_image.jpg"
cv2.imwrite(temp_image_path, img)
# Przetwarzanie obrazu
thresh, original_image = preprocess_image(temp_image_path)
shape = detect_shapes(thresh, original_image)
# Wysyłanie informacji do ESP32
esp_ip = "http://192.168.242.159" # Zmień na poprawny adres IP ESP32
try:
print(f"Sending shape to ESP32: {shape}")
response = requests.post(esp_ip, json={"shape": shape})
response.raise_for_status()
print("Informacje wysłane do ESP32")
except requests.exceptions.RequestException as e:
print(f"Błąd wysyłania danych do ESP32: {e}")
# Wysyłanie odpowiedzi JSON z wykrytym kształtem do klienta
return jsonify({"shape": shape}), 200
except Exception as e:
print(f"Error processing image: {e}")
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
https://how2electronics.com/esp32-cam-based-object-detection-identification-with-opencv/