Rilevazione di Oggetti in Tempo Reale con YOLOv8 e JavaScript ed onnx

AI, INTELLIGENZA ARTIFICIALE, SVILUPPO SW

Nell’ambito dell’elaborazione delle immagini e del machine learning, la rilevazione di oggetti in tempo reale è una delle sfide più emozionanti e utili. Di seguito vi riporto  un esempio pratico che illustra come utilizzare YOLOv8, una potente rete neurale per il riconoscimento di oggetti, integrata in un’applicazione web basata su HTML e JavaScript

Il  progetto per poter essere inserito nel blog usa una struttura monolitica in cui sia la parte html che java script sono stati inseriti in un unico file, ma questo è solo per motivi di embedding del codice all’interno del blog wordpress.

Per la parte visiva, ho realizzato  un design minimalista. Il CSS interno si occupa di formattare gli elementi `canvas` e `video`, assicurando che siano ben visibili e presentabili. Si tratta di una scelta estetica che garantisce  una chiara visualizzazione dei risultati.

Il cuore dell’applicazione risiede nel corpo del documento. Un pulsante permette agli utenti di avviare la videocamera. Appena attivata, il flusso video viene visualizzato in un elemento `video` e contemporaneamente su un `canvas`, dove avverrà la magia della rilevazione degli oggetti.

Il JavaScript incorporato nel documento gestisce diverse funzioni cruciali:
– Attivazione della Videocamera: Il codice interagisce con le API dei media del browser per accedere e visualizzare il flusso video.
– Rilevamento degli Oggetti: Si utilizza un ciclo di animazione per catturare continuamente frame dal video, che vengono poi elaborati per rilevare oggetti.
– Funzioni di Supporto: Ho implementato una serie di funzioni per la preparazione dei dati dell’immagine, il loro invio al modello YOLOv8 e l’interpretazione dei risultati.
– Caricamento e Esecuzione del Modello: Il modello viene caricato da un URL specificato e utilizzato per elaborare i dati delle immagini.

A valle del  rilevamento, i risultati vengono visualizzati sul `canvas`. Ogni oggetto rilevato viene evidenziato con un box e un’etichetta che ne indica la categoria.

Il processo di rilevamento degli oggetti inizia con la cattura di un frame dal video e la sua trasformazione in un formato adatto per l’analisi. Si utilizzano tecniche di manipolazione dell’immagine per adattare i dati del frame ai requisiti del modello YOLOv8, includendo il ridimensionamento dell’immagine e la conversione dei pixel in tensori.

Una volta che il frame è pronto, viene passato attraverso la rete neurale YOLOv8. La magia del deep learning entra in gioco qui, poiché la rete analizza l’immagine e identifica oggetti, fornendo come output i cosiddetti “bounding boxes” e le etichette corrispondenti.

Live Demo
YOLOv8 Video Analysis

DI seguito il codice dell’esempio online:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>YOLOv8 Video Analysis</title>
    <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>
    <style>
        canvas, video {
            display: block;
            border: 1px solid black;
            margin-top: 10px;
        }
    </style>
</head>

<body>
    <button id="startCamera">Start Camera</button>
    <video id="video" width="640" height="480" autoplay></video>
    <canvas></canvas>
    <script>
        const video = document.getElementById('video');
        const canvas = document.querySelector('canvas');
        const ctx = canvas.getContext('2d');
        const startCamera = document.getElementById('startCamera');

        // Start the video stream
        startCamera.addEventListener('click', async () => {
         const stream = await navigator.mediaDevices.getUserMedia({ video: true });
         video.srcObject = stream;
        });

        video.addEventListener('play', () => {
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            draw();
        });


        async function draw() {
            if (video.paused || video.ended) return;
            console.log("sto qui");
            ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
            const boxes = await detect_objects_on_video(canvas);
            console.log("Rilevati bounding boxes:", boxes);
            draw_boxes(boxes);
            requestAnimationFrame(draw);
        }


        // Funzione per disegnare i box
        function draw_boxes(boxes) {
            boxes.forEach(([x1,y1,x2,y2,label]) => {
                ctx.strokeStyle = "#00FF00";
                ctx.lineWidth = 3;
                ctx.font = "18px serif";
                ctx.strokeRect(x1, y1, x2-x1, y2-y1);
                ctx.fillStyle = "#00ff00";
                const width = ctx.measureText(label).width;
                ctx.fillRect(x1, y1, width+10, 25);
                ctx.fillStyle = "#000000";
                ctx.fillText(label, x1, y1+18);
            });
        

  /**
 * Function receives a canvas, extracts its image data,
 * passes it through YOLOv8 neural network and returns an array
 * of detected objects and their bounding boxes
 * @param canvas Canvas element containing the video frame
 * @returns Array of bounding boxes in format [[x1,y1,x2,y2,object_type,probability],..]
 */

async function detect_objects_on_video(canvas) {
    console.log("Inizio rilevamento oggetti");
    const context = canvas.getContext('2d');
    const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
    const [input, img_width, img_height] = prepare_input_from_canvas(imgData,       canvas.width, canvas.height);
    console.log("fine rilevamento oggetti");
    const output = await run_model(input);
    return process_output(output, img_width, img_height);
}


/**
 * Function used to convert canvas image data to tensor,
 * required as an input to YOLOv8 object detection network.
 * @param imgData ImageData from canvas
 * @param img_width Width of the canvas
 * @param img_height Height of the canvas
 * @returns Array of pixels
 */

function prepare_input_from_canvas(imgData, img_width, img_height) {
    console.log("inizio prepare")
    const canvas = document.createElement('canvas');
    canvas.width = 640; // resize if necessary
    canvas.height = 640;
    const context = canvas.getContext('2d');
 
    // Create a temporary canvas to hold the ImageData
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = img_width;
    tempCanvas.height = img_height;
    const tempCtx = tempCanvas.getContext('2d');

    // Put the ImageData into the temporary canvas
    tempCtx.putImageData(imgData, 0, 0);
    // Now draw from the temporary canvas to the target canvas
    context.drawImage(tempCanvas, 0, 0, img_width, img_height, 0, 0, 640, 640);
    const resizedImgData = context.getImageData(0, 0, 640, 640);
    const pixels = resizedImgData.data;
    const red = [], green = [], blue = [];
    for (let index = 0; index < pixels.length; index += 4) {
        red.push(pixels[index] / 255.0);
        green.push(pixels[index + 1] / 255.0);
        blue.push(pixels[index + 2] / 255.0);
    }

    const input = [...red, ...green, ...blue];
    console.log("fine prepare",input, img_width, img_height)
    return [input, img_width, img_height];
}

      /**
       * Function receives an image, passes it through YOLOv8 neural network
       * and returns an array of detected objects and their bounding boxes
       * @param buf Input image body
       * @returns Array of bounding boxes in format [[x1,y1,x2,y2,object_type,probability],..]
       */
      async function detect_objects_on_image(buf) {
          const [input,img_width,img_height] = await prepare_input(buf);
          const output = await run_model(input);
          return process_output(output,img_width,img_height);
      }


      /**
       * Function used to convert input image to tensor,
       * required as an input to YOLOv8 object detection
       * network.
       * @param buf Content of uploaded file
       * @returns Array of pixels
       */

      async function prepare_input(buf) {
          return new Promise(resolve => {
              const img = new Image();
              img.src = URL.createObjectURL(buf);
              img.onload = () => {
                  const [img_width,img_height] = [img.width, img.height]
                  const canvas = document.createElement("canvas");
                  canvas.width = 640;
                  canvas.height = 640;
                  const context = canvas.getContext("2d");
                  context.drawImage(img,0,0,640,640);
                  const imgData = context.getImageData(0,0,640,640);
                  const pixels = imgData.data;
                  const red = [], green = [], blue = [];

                  for (let index=0; index<pixels.length; index+=4) {
                      red.push(pixels[index]/255.0);
                      green.push(pixels[index+1]/255.0);
                      blue.push(pixels[index+2]/255.0);
                  }

                  const input = [...red, ...green, ...blue];
                  resolve([input, img_width, img_height])
              }
          })
      }


let loadedModel = null;
async function loadModel() {
    try {
        console.log("Caricamento modello in corso...");
        loadedModel = await ort.InferenceSession.create("https://www.domsoria.com/wp-content/themes/discover-rw/yolov8n.onnx");
        console.log("Modello caricato con successo");
    } catch (error) {
        console.error("Errore durante il caricamento del modello:", error);
    }
}

async function run_model(input) {
    try {
        if (!loadedModel) {
            console.log("Modello non caricato, tentativo di ricarica...");
            await loadModel();
            if (!loadedModel) {
                throw new Error("Impossibile caricare il modello");
            }
        }

        input = new ort.Tensor(Float32Array.from(input), [1, 3, 640, 640]);
        const outputs = await loadedModel.run({ images: input });
        return outputs["output0"].data;
    } catch (error) {
        console.error("Errore durante l'esecuzione del modello:", error);
    }
}

// Chiamata per caricare il modello quando la pagina viene caricata o il video viene avviato
loadModel();

      /**
       * Function used to convert RAW output from YOLOv8 to an array of detected objects.
       * Each object contain the bounding box of this object, the type of object and the probability
       * @param output Raw output of YOLOv8 network
       * @param img_width Width of original image
       * @param img_height Height of original image
       * @returns Array of detected objects in a format [[x1,y1,x2,y2,object_type,probability],..]
       */

      function process_output(output, img_width, img_height) {
          let boxes = [];
          for (let index=0;index<8400;index++) {
              const [class_id,prob] = [...Array(80).keys()]
                  .map(col => [col, output[8400*(col+4)+index]])
                  .reduce((accum, item) => item[1]>accum[1] ? item : accum,[0,0]);
              if (prob < 0.5) {
                  continue;
              }

              const label = yolo_classes[class_id];
              const xc = output[index];
              const yc = output[8400+index];
              const w = output[2*8400+index];
              const h = output[3*8400+index];
              const x1 = (xc-w/2)/640*img_width;
              const y1 = (yc-h/2)/640*img_height;
              const x2 = (xc+w/2)/640*img_width;
              const y2 = (yc+h/2)/640*img_height;
              boxes.push([x1,y1,x2,y2,label,prob]);
          }

          boxes = boxes.sort((box1,box2) => box2[5]-box1[5])
          const result = [];
          while (boxes.length>0) {
              result.push(boxes[0]);
              boxes = boxes.filter(box => iou(boxes[0],box)<0.7);
          }
          return result;
      }


      /**
       * Function calculates "Intersection-over-union" coefficient for specified two boxes
       * https://pyimagesearch.com/2016/11/07/intersection-over-union-iou-for-object-detection/.
       * @param box1 First box in format: [x1,y1,x2,y2,object_class,probability]
       * @param box2 Second box in format: [x1,y1,x2,y2,object_class,probability]
       * @returns Intersection over union ratio as a float number
       */

      function iou(box1,box2) {
          return intersection(box1,box2)/union(box1,box2);
      }

      /**
       * Function calculates union area of two boxes.
       *     :param box1: First box in format [x1,y1,x2,y2,object_class,probability]
       *     :param box2: Second box in format [x1,y1,x2,y2,object_class,probability]
       *     :return: Area of the boxes union as a float number
       * @param box1 First box in format [x1,y1,x2,y2,object_class,probability]
       * @param box2 Second box in format [x1,y1,x2,y2,object_class,probability]
       * @returns Area of the boxes union as a float number
       */

     function union(box1,box2) {
          const [box1_x1,box1_y1,box1_x2,box1_y2] = box1;
          const [box2_x1,box2_y1,box2_x2,box2_y2] = box2;
          const box1_area = (box1_x2-box1_x1)*(box1_y2-box1_y1)
          const box2_area = (box2_x2-box2_x1)*(box2_y2-box2_y1)
          return box1_area + box2_area - intersection(box1,box2)
      }


      /**
       * Function calculates intersection area of two boxes
       * @param box1 First box in format [x1,y1,x2,y2,object_class,probability]
       * @param box2 Second box in format [x1,y1,x2,y2,object_class,probability]
       * @returns Area of intersection of the boxes as a float number
       */

      function intersection(box1,box2) {
          const [box1_x1,box1_y1,box1_x2,box1_y2] = box1;
          const [box2_x1,box2_y1,box2_x2,box2_y2] = box2;
          const x1 = Math.max(box1_x1,box2_x1);
          const y1 = Math.max(box1_y1,box2_y1);
          const x2 = Math.min(box1_x2,box2_x2);
          const y2 = Math.min(box1_y2,box2_y2);
          return (x2-x1)*(y2-y1)
      }

      /**
       * Array of YOLOv8 class labels
       */

      const yolo_classes = [
          'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat',
          'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse',
          'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase',
          'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard',
          'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
          'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant',
          'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven',
          'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'
      ];
    </script>
</body>
</html>

 

Per scaricare il modello yolov8 da usare puoi farlo qui

YOLOv8

YOLOv8 (You Only Look Once, versione 8) rappresenta l’ultima evoluzione nella serie di modelli YOLO, una famiglia di algoritmi di rilevamento degli oggetti in tempo reale noti per la loro velocità e efficienza. YOLOv8, come i suoi predecessori, è progettato per analizzare le immagini in un singolo passaggio, identificando e classificando gli oggetti presenti in una frazione di secondo. Questa capacità lo rende particolarmente adatto per applicazioni come la sorveglianza video, la guida autonoma, e i sistemi di assistenza alla navigazione in tempo reale.

Architettura di YOLOv8

L’architettura di YOLOv8, pur mantenendo lo spirito originale del design “you only look once”, introduce miglioramenti significativi rispetto alle versioni precedenti. Ecco alcuni punti chiave:

  • Miglioramenti nel Riconoscimento e nella Precisione: YOLOv8 presenta una serie di ottimizzazioni che aumentano la precisione del rilevamento. Questo include affinamenti nella struttura della rete neurale, come l’uso di blocchi convoluzionali più avanzati e tecniche di normalizzazione. Inoltre, l’impiego di algoritmi di apprendimento profondo più sofisticati migliora l’accuratezza nel riconoscere una vasta gamma di oggetti in diverse condizioni di illuminazione e sfondi.
  • Velocità di Elaborazione: Una delle caratteristiche distintive di YOLOv8 è la sua straordinaria velocità di elaborazione. La rete è stata ottimizzata per garantire che possa funzionare in tempo reale, anche su hardware non specializzato. Questo è fondamentale per applicazioni che richiedono risposte immediate, come i sistemi di assistenza alla guida o di monitoraggio della sicurezza.
  • Miglioramento nell’Uso di Risorse: YOLOv8 è progettato per essere più efficiente in termini di utilizzo delle risorse, rendendolo adatto anche per dispositivi con capacità di calcolo limitate. Questa efficienza apre la porta a un’ampia gamma di applicazioni IoT e mobile, dove le risorse di calcolo e la potenza sono spesso limitate.
  • Training e Fine-tuning: Il modello offre flessibilità nel training e nel fine-tuning, permettendo agli sviluppatori di adattarlo a specifici set di dati e scenari di utilizzo. Questo è particolarmente utile in domini applicativi specializzati, come il riconoscimento di specifici tipi di oggetti in ambienti industriali o urbani.

Cosa è  ONNX?

ONNX, che sta per Open Neural Network Exchange, è un formato di file aperto pensato per rappresentare modelli di apprendimento automatico (machine learning). Questo formato è diventato popolare perché permette ai modelli di essere utilizzati tra diverse piattaforme e librerie senza la necessità di riscrivere il codice. Ecco alcuni aspetti chiave del formato ONNX:

Portabilità
ONNX è progettato per rendere i modelli di machine learning portabili. Significa che un modello addestrato in una libreria (come TensorFlow o PyTorch) può essere convertito in formato ONNX e poi eseguito utilizzando un’altra libreria o piattaforma che supporta ONNX.

Interoperabilità
L’interoperabilità è un grande vantaggio di ONNX. Supportando vari framework e strumenti, consente agli sviluppatori di utilizzare i loro strumenti preferiti per addestrare i modelli e poi di distribuirli in diversi ambienti di produzione che potrebbero utilizzare diverse tecnologie.

Efficienza
ONNX può aiutare a ottimizzare i modelli per diverse piattaforme e dispositivi. Alcune piattaforme offrono accelerazioni hardware specifiche per modelli ONNX, migliorando così le prestazioni durante l’inferenza.

Esempi di Utilizzo
Un esempio pratico di utilizzo di ONNX potrebbe essere l’addestramento di un modello di riconoscimento di immagini in PyTorch, la sua conversione in formato ONNX, e poi l’esecuzione su una piattaforma che supporta ONNX, magari con accelerazione hardware specifica, per l’inferenza in tempo reale.

 

Se vuoi farmi qualche richiesta o contattarmi per un aiuto riempi il seguente form

    Comments