Detekcja i rozpoznawanie obrazu z wyświetlaniem na matrycy LED|ESP32 cam

Typ_projektu
microPython
Zdjecie główne
Krótki opis projektu

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.

Niezbędne elementy

1. Płytka ESP32 -CAM z wbudowaną kamerką TY-OV2640

2. Matryca LED RGB WS2812B 16x16

3. Kabel USB

Sprzęt

Projekt nie wymagał żadnego szczególnego sprzętu do jego wykonania- był on zorientowany stricte na prace z kodem.

Opis projektu

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:

  1. Wgrać kod "finalversion-zdjecie" na płytkę ESP32 za pomocą środowiska Thonny.
  2. 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.
  3. 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.
Zdjęcia
kod programu
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)

    
Youtube
Tagi
ESP32CAM MatrycaLED imagedetection rozpoznawanieobrazu ESP32
Odnośniki zewnętrzne
https://dev.to/shilleh/how-to-use-esp32-cam-with-micropython-4odo
https://how2electronics.com/esp32-cam-based-object-detection-identification-with-opencv/