Costruire un ordinatore Lego Technic con Riconoscimento Avanzato degli Oggetti in Tempo Reale

Crea un computer con Lego Technic dotato di riconoscimento avanzato degli oggetti in tempo reale

Durante il mio stage presso Nullspace Robotics, ho avuto il privilegio di immergermi in un progetto che avrebbe migliorato le capacità dell’azienda. Abbiamo integrato la rilevazione degli oggetti e il riconoscimento delle immagini tramite apprendimento automatico per sviluppare una macchina che classifica i pezzi Lego Technic in tempo reale.

In questo articolo del blog, ti guiderò attraverso le sfide incontrate e come siamo riusciti a portare a termine questo progetto con successo.

Io e Amos Koh abbiamo trascorso l’estate del ’22 insegnando programmazione e robotica agli studenti mentre lavoravamo su questo progetto per Nullspace. Puoi trovarci ai link sotto l’articolo.

Nullspace Robotics è il principale fornitore di educazione robotica e di programmazione a Singapore per gli studenti delle scuole primarie e secondarie. Una grande parte delle loro attività consiste nel costruire robot con pezzi Lego Technic che vengono separati in vassoi specifici. Puoi immaginare che sia un compito da incubo chiedere a un bambino di 8 anni con energia illimitata di aiutare a rimettere i pezzi nel vassoio quando tutto ciò che vogliono fare è costruire altre cose.

Nullspace ci ha incaricato di creare una macchina in grado di separare i pezzi Lego Technic in categorie specifiche con il minimo intervento umano, per risolvere una delle principali sfide di efficienza durante una lezione di robotica.

Definizione della sfida

Il progetto comprendeva 3 parti principali: rilevazione in tempo reale di oggetti e movimento, riconoscimento di immagini e costruzione dell’hardware della macchina. A causa dei limiti di tempo dello stage, ci siamo concentrati principalmente sui primi due punti, che riguardavano gli aspetti software del progetto.

Una delle principali sfide era riconoscere le parti in movimento e identificarle all’interno dello stesso fotogramma. Abbiamo considerato due approcci: integrare il riconoscimento delle immagini tramite apprendimento automatico nella telecamera di rilevazione degli oggetti o mantenere i processi separati.

Alla fine, abbiamo deciso di separare la rilevazione degli oggetti e il riconoscimento. Questo approccio prevedeva di catturare prima una foto adatta dopo aver rilevato l’oggetto e quindi eseguire un modello per classificare l’immagine. L’integrazione dei processi avrebbe richiesto l’esecuzione del modello praticamente su ogni frame per classificare tutti gli oggetti rilevati. Separarli ha eliminato la necessità che il modello fosse in un costante stato di elaborazione, garantendo un’operazione più fluida e computazionalmente efficiente.

Rilevazione degli oggetti

Abbiamo utilizzato idee dai progetti citati sotto l’articolo per implementare il nostro programma di rilevamento di oggetti/movimenti e personalizzarlo per i pezzi Lego.

Nel nostro caso, abbiamo utilizzato concetti simili di rilevamento del movimento perché la nostra macchina riguardava un sistema di nastro trasportatore di colore uniforme, quindi un qualsiasi movimento rilevato era dovuto a un pezzo Lego che si muoveva sul nastro.

Abbiamo applicato sfocature gaussiane e altre tecniche di elaborazione dell’immagine a tutti i fotogrammi e li abbiamo confrontati con i fotogrammi precedenti. Sono state eseguite ulteriori elaborazioni per isolare (disegnare riquadri di delimitazione attorno) gli oggetti che causano il movimento, come mostrato di seguito:

for f in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True):        frame = f.array # acquisisci l'array NumPy grezzo che rappresenta l'immagine    text = "Nessun pezzo" # inizializza il testo occupato/non occupato    # ridimensiona l'immagine, convertila in scala di grigi e sfocala    frame = imutils.resize(frame, width=500)    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)    gray = cv2.GaussianBlur(gray, (21, 21), 0)    # se la media dell'immagine è None, inizializzala    if avg is None:        print("[INFO] avvio del modello di sfondo...")        avg = gray.copy().astype("float")        rawCapture.truncate(0)        continue    # accumula la media ponderata tra il fotogramma corrente e    # fotogrammi precedenti, quindi calcola la differenza tra il fotogramma    # corrente e la media in esecuzione    cv2.accumulateWeighted(gray, avg, 0.5)    frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))        # applica una soglia all'immagine delta, dilata l'immagine sogliata per riempire    # i buchi, quindi trova i contorni sull'immagine sogliata    thresh = cv2.threshold(frameDelta, conf["delta_thresh"], 255,        cv2.THRESH_BINARY)[1]    thresh = cv2.dilate(thresh, None, iterations=2)    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,        cv2.CHAIN_APPROX_SIMPLE)    cnts = imutils.grab_contours(cnts)    # ciclo sui contorni        for c in cnts:        # se il contorno è troppo piccolo, ignoralo        if cv2.contourArea(c) < conf["min_area"]:            continue        # calcola il riquadro di delimitazione del contorno, disegnalo sull'immagine,        # e aggiorna il testo        (x, y, w, h) = cv2.boundingRect(c)        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)        piece_image = frame[y:y+h,x:x+w]        text = "Pezzo trovato"        # cv2.imshow("Image", image)

Per garantire che il movimento sia effettivamente causato da un pezzo di lego, la stabilità della rilevazione del movimento è stata valutata utilizzando un contatore di movimento, che verificava che il movimento fosse rilevato per un certo numero di frame prima di concludere che il movimento fosse effettivamente dovuto a un pezzo di lego e non a rumore casuale. L’immagine finale viene quindi salvata e inserita nel nostro CNN per classificarla.

If text == "Pezzo trovato":        # per salvare le immagini dei bounding box        motionCounter += 1        print("motionCounter= ", motionCounter)        print("image_number= ", image_number)        # Salva l'immagine se viene rilevato il movimento per 8 o più frame successivi        if motionCounter >= 8:            image_number +=1            image_name = str(image_number)+"image.jpg"            cv2.imwrite(os.path.join(path, image_name), piece_image)            motionCounter = 0 #resettiamo il contatore di movimento # classifica l'immagine salvata con il nostro modello, vedi sotto

Creazione del modello

Creazione del dataset

Abbiamo creato il dataset di immagini noi stessi anziché utilizzare immagini di pezzi di lego technic trovati online perché volevamo replicare le condizioni in cui il modello avrebbe successivamente rilevato e classificato i pezzi. Abbiamo quindi progettato un semplice sistema di nastro trasportatore utilizzando proprio i pezzi di lego technic! Lo abbiamo poi collegato a un motore lego spike prime per mantenere in movimento il nastro trasportatore.

Progettazione dell’architettura del modello

Per affrontare il cuore della sfida, ho adattato un modello di apprendimento automatico che ho trovato nel repository di Aladdinpersson su GitHub. Questo modello presentava strati convoluzionali con una sequenza da 128 a 64 a 32 a 16, una scelta architettonica progettata per migliorare il riconoscimento delle immagini.

Invece di utilizzare un modello pre-addestrato, abbiamo progettato la nostra rete neurale convoluzionale perché:

  1. Non abbiamo avuto bisogno di una estrazione delle caratteristiche particolarmente profonda per le nostre immagini
  2. Volevamo mantenere il modello piccolo e ridurre la sua complessità, riducendo allo stesso tempo il costo computazionale dell’esecuzione del modello. Questo ci avrebbe permesso di eseguire il CNN come modello tflite in modo più efficiente sul Pi.

La normalizzazione dei dati è stato un passaggio cruciale per garantire una precisione di addestramento coerente, specialmente data la variazione nell’intervallo di valori catturati da diverse immagini a causa delle differenze di illuminazione.

In questo modello, vari strati come ReLU, dense, softmax e flatten hanno svolto un ruolo cruciale. L’attivazione ReLU, ad esempio, era essenziale per la classificazione delle immagini in quanto mitigava il problema dei gradienti che svaniscono nel riconoscimento delle immagini. Gli strati densi, d’altra parte, sono standard nei modelli di Tensorflow, facilitando reti neurali densamente collegate. L’attivazione Softmax è stata utilizzata per calcolare le probabilità per ogni categoria nel nostro dataset.

Per le funzioni di perdita, abbiamo utilizzato la Sparse Categorical Cross Entropy di Keras, una scelta appropriata per compiti di classificazione multi-classe. L’ottimizzatore Adam di Keras, famoso per la sua efficienza, è stato utilizzato per ottimizzare il modello.

Addestramento e Ottimizzazione

Le epoche sono state selezionate con cura per trovare un equilibrio tra addestramento e sovrapposizione, con una preferenza per un numero inferiore a 200 per garantire prestazioni ottimali del modello. Per un addestramento del modello accelerato, abbiamo utilizzato Google Colab, che forniva accesso alle risorse delle GPU, garantendo velocità di addestramento significativamente più rapide rispetto ai nostri laptop.

L’architettura completa del modello è mostrata di seguito:

data_augmentation = keras.Sequential([    layers.RandomFlip("horizontal",                        input_shape=(img_height,                                    img_width,                                    1)),    layers.RandomRotation(0.2),    layers.RandomZoom(0.1),    ])model = keras.Sequential(    [        data_augmentation,            layers.Rescaling(1./255, input_shape = (img_height,img_width,1)), #normalizza i dati in ingresso        layers.Conv2D(128, 3, padding="same", activation='relu'),        layers.MaxPooling2D(pool_size=(2,2)),        layers.Conv2D(64, 3, padding="same", activation='relu'), #dovrebbero essere 16 o 32 unità? prova con più dati        layers.MaxPooling2D(pool_size=(2,2)),        layers.Conv2D(32, 3, padding="same", activation='relu'),        layers.MaxPooling2D(pool_size=(2,2)),                layers.Conv2D(16, 3, padding="same", activation='relu'),        layers.MaxPooling2D(pool_size=(2,2)),                layers.Dropout(0.1),        layers.Flatten(),        layers.Dense(10,activation = 'relu'),        layers.Dense(7,activation='softmax'), #numero di classi di output            ])        model.compile(    optimizer=keras.optimizers.Adam(),    loss=[keras.losses.SparseCategoricalCrossentropy(from_logits=False),],    metrics=["accuracy"],)model_history = model.fit(x_train, y_train, epochs=200, verbose=2, validation_data=(x_test,y_test), batch_size=25)  #penso che 25/32 sia la migliore dimensione del batch

Risultati del modello

Il modello è stato addestrato con 6000 immagini appartenenti a 7 categorie di mattoncini Lego Technic. Ha raggiunto una precisione di validazione finale del 93%. Di seguito sono mostrati i diagrammi che illustrano la progressione dell’addestramento e una matrice di confusione per valutare le prestazioni:

Implementazione del modello su Raspberry Pi

Il modo più efficiente per eseguire una rete neurale su un Raspberry Pi è utilizzando un modello tflite (TensorFlow Lite). Abbiamo salvato il modello localmente e poi l’abbiamo caricato sul Pi.

from tflite_runtime.interpreter import Interpreter# Carica il modello TFLite e alloca i tensori.interpreter = Interpreter(model_path="lego_tflite_model/detect.tflite") # inserisci il percorso del modello tfliteinterpreter.allocate_tensors()

Continuando dal ciclo del contatore di movimento sopra, le immagini adatte sono state poi fornite alla rete neurale per la classificazione:

 # continuando da se il testo == "Piece found":            # Apri l'immagine, ridimensionala e aumenta il contrasto            input_image = Image.open('lego-pieces/'+ nome_immagine)            input_image = ImageOps.grayscale(input_image)            input_image = input_image.resize((128,128))            input_data = img_to_array(input_image)            input_data = increase_contrast_more(input_data)            input_data.resize(1,128,128,1)                        # Passa l'array np.array dell'immagine al modello tflite. Questo fornirà un vettore di probabilità            interpreter.set_tensor(input_details[0]['index'], input_data)            interpreter.invoke()            output_data = interpreter.get_tensor(output_details[0]['index'])                        # Ottieni l'indice del valore più alto nel vettore di probabilità.            # Questo valore di indice corrisponderà al vettore di etichette creato in precedenza (ad esempio, l'indice 1 significherà che l'oggetto è molto probabilmente labels[1])            category_number = np.argmax(output_data[0])            # Restituisci l'etichetta di classificazione dell'immagine                classification_label = labels[category_number]                            print("Etichetta immagine per " + nome_immagine + " è:", classification_label)                                        else:        motionCounter = 0 # ripristina il contatore di movimento per cercare nuovi oggetti

La flessibilità è stata una considerazione chiave. Il contatore di movimento poteva essere regolato per il processo di acquisizione delle immagini per creare il dataset o impostare la soglia per quando l’immagine dovrebbe essere acquisita per la classificazione, migliorando così la versatilità del sistema.

Dimostrazione

Il culmine dei nostri sforzi è stata una presentazione dell’accuratezza complessiva del sistema, supportata da foto e video che ne mostrano il funzionamento. La configurazione della cinghia trasportatrice (sopra) è stata una parte essenziale di questa dimostrazione:

Lavori futuri e aree di miglioramento

Software: Il modello trarrebbe sicuramente beneficio da una fotocamera di qualità superiore per immagini di migliore qualità. Inoltre, un’estensione futura consisterebbe anche nell’includere un modello di controllo della qualità nel funzionamento, per garantire che le immagini utilizzate per classificare i pezzi siano adatte.

Hardware: Il sistema della cinghia trasportatrice costruito temporaneamente per i nostri test e la dimostrazione dovrà essere ampliato per ospitare più pezzi. Sarà inoltre necessario ideare e implementare un metodo per separare più pezzi Lego e assicurarsi che solo un pezzo sia visibile nel campo della telecamera in un determinato momento. Sono disponibili progetti simili online che dettagliano possibili metodi.

Conclusioni

La mia esperienza presso Nullspace Robotics è stata la mia prima incursione nella costruzione di una mia rete neurale per scopi pratici. Avendo progettato modelli come parte di corsi di formazione in passato, è un’esperienza completamente diversa creare un modello destinato alla produzione effettiva, dove è necessario considerare vari fattori come risorse, scopi d’uso e come adattare il dataset e il modello ai nostri scopi. Non vedo l’ora di continuare il mio percorso nell’apprendimento automatico e sfruttare le più recenti tecnologie AI per sviluppare soluzioni sempre più innovative.

Vorrei ringraziare Nullspace per l’opportunità di lavorare su questo progetto e sono entusiasta di vedere quale sarà il futuro dell’azienda mentre spinge i limiti dell’educazione robotica.

Controlla il repository completo su Github o HuggingFace per il codice, l’accesso alle immagini del dataset e ulteriori informazioni sul progetto:

Github: https://github.com/magichampz/lego-sorting-machine-ag-ak/

HuggingFace: https://huggingface.co/magichampz

Incontra gli sviluppatori

Aveek: https://www.linkedin.com/in/aveekg00/

Amos: https://www.linkedin.com/in/ak726/

Riferimenti:

Rilevamento del movimento basilare e tracciamento con Python e OpenCV – PyImageSearch

In questo tutorial, ti mostrerò come utilizzare Python e OpenCV per eseguire rilevamenti di movimento basilari e tracciamenti. Scopri come…

pyimagesearch.com

Rilevare il movimento con OpenCV – analisi delle immagini per principianti

Come rilevare e analizzare oggetti in movimento con OpenCV

towardsdatascience.com

Machine-Learning-Collection/ML/TensorFlow/Basics/tutorial15-customizing-modelfit.py alla master ·…

Una risorsa per apprendere l’apprendimento automatico e il deep learning…

github.com

Scene eliminate

Salvo diversamente specificato, tutte le immagini sono dell’autore