Réalisation d'un suivi d'une led avec une caméra OV7670

Bonjour, mon projet (tipe de prepa) consiste à suivre une led (représentant une planète) à l'aide d'une caméra OV7670 sans fifo et de 2 moteurs CC (pivots en série).
L'objectif d'utiliser arduino pour détecter le point le + lumineux de l'image et d'envoyer les coordonnées sur python via Firmata. Ensuite, en fonction de x et y, les 2 moteurs tournent à une certaine vitesse pour ramener la led à la position initiale sur l'image. Attention: c'est la caméra qui est fixe sur les moteurs et la led que je fais bouger (simplement la lampe torche de mon smartphone à travers un petit trou avec du carton).

J'ai un gros problème: la caméra sort des données YUV (Y=luminance), donc je garde seulement la luminance ça c'est ok. Le problème c'est que le pixel obtenu par le programme semble toujours random, et la luminance c'est pire. Que la cam soit dans le noir ou avec un lampe juste devant, Y reste autour de 170, ou 140, ou 200, ou même 250 (255 étant le max). La valeur de Y dépend du programme, de l'humeur de la caméra, et je sais pas trop...

J'ai essayé de simplement chercher le pixel (x,y), j'ai essayé en appliquant un seuil (ex Y>180), j'ai essayé d'appliquer un filtre qui fait la moyenne sur un carré de pixel 33; rien à faire.
Quelque chose qui me saoule un peu mais j'y peu rien: mon module caméra ne possède pas de I2C donc impossible de modifier la résolution de base (320
240), cela ralentis le traitement mais c'est pas trop grave.

Chose à savoir: j'ai déjà réussi à récuperer des images de la caméra et à les afficher sur l'IDE avec un plugin, j'arrive à envoyer (x,y) via le port série principale de la méga, python controle les moteurs à l'aide de Firmata (Serial1 de la mega et passe à travers un uno pour avec un 2e port usb), le programme python est normalement bon, juste (x,y) sont mauvais donc les moteurs font nimp. Le problème provient surement de la loop arduino.

Je vais donner les programmes, j'espère que vous pourrez m'aider, j'ai jusqu'à ce we dernier délais (je viens de passer 1 semaine complète à tout tester pour résoudre ce problème)

from pyfirmata import ArduinoMega, util
import serial
import time

# Firmata et Serial
board = ArduinoMega('COM15')         # Contrôle moteurs
xy_serial = serial.Serial('COM18', 9600, timeout=1)  # 1s max d'attente

# Moteurs
motor1_dir = board.digital[3]   # Moteur X
motor1_pwm = board.digital[5]
motor2_dir = board.digital[4]  # Moteur Y
motor2_pwm = board.digital[6]
motor1_pwm.mode = 3
motor2_pwm.mode = 3

# Image dimensions (OV7670 = 320x240)
IMAGE_WIDTH = 320
IMAGE_HEIGHT = 240

# Données initiales
x0, y0, maxVal = None, None, None  # Coordonnées initiales (centre de référence)
X, Y = [], []            # Historique des points

def zone_morte(val, val0, image_dim):
    """Renvoie True si val est dans la zone morte de ±10%"""
    margin = image_dim * 0.10
    return abs(val - val0) <= margin

def calcul_vitesse(val, val0, image_dim):
    """Renvoie vitesse normalisée entre 0.3 et 1 selon éloignement"""
    distance = abs(val - val0)
    margin = image_dim * 0.10
    max_dist = (image_dim / 2) - margin
    if distance <= margin:
        return 0  # Zone morte
    ratio = min(distance / max_dist, 1)
    return round(0.4 + (0.3 * ratio), 2) #Vmax=70%

def direction(val, val0):
    """Renvoie 0 ou 1 pour définir la direction selon position"""
    return 1 if val > val0 else 0

def update_moteur(motor_dir, motor_pwm, val, val0, image_dim):
    if zone_morte(val, val0, image_dim):
        motor_pwm.write(0)
    else:
        dir_val = direction(val, val0)
        vitesse = calcul_vitesse(val, val0, image_dim)
        motor_dir.write(dir_val)
        motor_pwm.write(vitesse)

# Démarrage
time.sleep(2)
print("Initialisation terminée. Attente coordonnées Arduino...")

try:
    while True:
        line = xy_serial.readline().decode().strip()
        if ',' in line:
            try:
                x, y, maxVal = map(int, line.split(','))
                if x0 is None or y0 is None:
                    x0, y0 = x, y  # Initialisation
                    print(f"Coordonnée de référence : x0 = {x0}, y0 = {y0}")
                else:
                    X.append(x)
                    Y.append(y)
                    print(f"Reçu x = {x}, y = {y}, maxVal = {maxVal}")

                    # Moteur 1 (X)
                    #update_moteur(motor1_dir, motor1_pwm, x, x0, IMAGE_WIDTH)

                    # Moteur 2 (Y)
                    #update_moteur(motor2_dir, motor2_pwm, y, y0, IMAGE_HEIGHT)

            except Exception as e:
                print("Erreur de parsing :", line, "|", e)

except KeyboardInterrupt:
    print("Arrêt manuel.")
    motor1_pwm.write(0)
    motor2_pwm.write(0)
    board.exit()
#include <Wire.h>
#include <Firmata.h>

int analogInputsToReport = 0;
byte reportPINs[TOTAL_PORTS];
byte previousPINs[TOTAL_PORTS];
byte portConfigInputs[TOTAL_PORTS];
boolean isResetting=false;

#define OV7670_ADDR 0x21  // Adresse I2C de la caméra
const int vsyncPin = 2;    // Synchro début d'image
const int mclkPin = 11;
const int pclkPin  = 12;    // Horloge pixel
const int dataPins[8] = {22, 23, 24, 25, 26, 27, 28, 29}; // D0 à D7 

int maxVal = -1;   //Initialisation luminance (valeur impossible)
int brightX = 0, brightY = 0;

void setPinModeCallback(byte pin, int mode){
  if (Firmata.getPinMode(pin) == PIN_MODE_IGNORE)
    return;
  if (IS_PIN_DIGITAL(pin)) {
    if (mode == INPUT || mode == PIN_MODE_PULLUP) {
      portConfigInputs[pin / 8] |= (1 << (pin & 7));
    } else {
      portConfigInputs[pin / 8] &= ~(1 << (pin & 7));
    }
  }
  Firmata.setPinState(pin, 0);
  switch (mode) {
    case PIN_MODE_ANALOG:
      if (IS_PIN_ANALOG(pin)) {
        if (IS_PIN_DIGITAL(pin)) {
          pinMode(PIN_TO_DIGITAL(pin), INPUT);   
#if ARDUINO <= 100
          digitalWrite(PIN_TO_DIGITAL(pin), LOW); 
#endif
        }
        Firmata.setPinMode(pin, PIN_MODE_ANALOG);
      }
      break;
    case INPUT:
      if (IS_PIN_DIGITAL(pin)) {
        pinMode(PIN_TO_DIGITAL(pin), INPUT);   
#if ARDUINO <= 100
        digitalWrite(PIN_TO_DIGITAL(pin), LOW); 
#endif
        Firmata.setPinMode(pin, INPUT);
      }
      break;
    case PIN_MODE_PULLUP:
      if (IS_PIN_DIGITAL(pin)) {
        pinMode(PIN_TO_DIGITAL(pin), INPUT_PULLUP);
        Firmata.setPinMode(pin, PIN_MODE_PULLUP);
        Firmata.setPinState(pin, 1);
      }
      break;
    case OUTPUT:
      if (IS_PIN_DIGITAL(pin)) {
        if (Firmata.getPinMode(pin) == PIN_MODE_PWM) {
          digitalWrite(PIN_TO_DIGITAL(pin), LOW);
        }
        pinMode(PIN_TO_DIGITAL(pin), OUTPUT);
        Firmata.setPinMode(pin, OUTPUT);
      }
      break;
    case PIN_MODE_PWM:
      if (IS_PIN_PWM(pin)) {
        pinMode(PIN_TO_PWM(pin), OUTPUT);
        analogWrite(PIN_TO_PWM(pin), 0);
        Firmata.setPinMode(pin, PIN_MODE_PWM);
      }
      break;
    case PIN_MODE_SERVO:
      if (IS_PIN_DIGITAL(pin)) {
        Firmata.setPinMode(pin, PIN_MODE_SERVO);
      }
      break;
    case PIN_MODE_I2C:
      if (IS_PIN_I2C(pin)) {
        Firmata.setPinMode(pin, PIN_MODE_I2C);
      }
      break;
    case PIN_MODE_SERIAL:
#ifdef FIRMATA_SERIAL_FEATURE
      serialFeature.handlePinMode(pin, PIN_MODE_SERIAL);
#endif
      break;
    default:
      Firmata.sendString("Unknown pin mode");
  }
}

void analogWriteCallback(byte pin, int value){
  if (pin < TOTAL_PINS) {
    switch (Firmata.getPinMode(pin)) {
      case PIN_MODE_PWM:
        if (IS_PIN_PWM(pin))
          analogWrite(PIN_TO_PWM(pin), value);
        Firmata.setPinState(pin, value);
        break;
    }
  }
}

void digitalWriteCallback(byte port, int value){
  byte pin, lastPin, pinValue, mask = 1, pinWriteMask = 0;

  if (port < TOTAL_PORTS) {
    // create a mask of the pins on this port that are writable.
    lastPin = port * 8 + 8;
    if (lastPin > TOTAL_PINS) lastPin = TOTAL_PINS;
    for (pin = port * 8; pin < lastPin; pin++) {
      // do not disturb non-digital pins (eg, Rx & Tx)
      if (IS_PIN_DIGITAL(pin)) {
        // do not touch pins in PWM, ANALOG, SERVO or other modes
        if (Firmata.getPinMode(pin) == OUTPUT || Firmata.getPinMode(pin) == INPUT) {
          pinValue = ((byte)value & mask) ? 1 : 0;
          if (Firmata.getPinMode(pin) == OUTPUT) {
            pinWriteMask |= mask;
          } else if (Firmata.getPinMode(pin) == INPUT && pinValue == 1 && Firmata.getPinState(pin) != 1) {
            // only handle INPUT here for backwards compatibility
#if ARDUINO > 100
            pinMode(pin, INPUT_PULLUP);
#else
            // only write to the INPUT pin to enable pullups if Arduino v1.0.0 or earlier
            pinWriteMask |= mask;
#endif
          }
          Firmata.setPinState(pin, pinValue);
        }
      }
      mask = mask << 1;
    }
    writePort(port, (byte)value, pinWriteMask);
  }
}

void systemResetCallback(){
  isResetting = true;

  // initialize a defalt state
  // TODO: option to load config from EEPROM instead of default

#ifdef FIRMATA_SERIAL_FEATURE
  serialFeature.reset();
#endif
  for (byte i = 0; i < TOTAL_PORTS; i++) {
    reportPINs[i] = false;    // by default, reporting off
    portConfigInputs[i] = 0;  // until activated
    previousPINs[i] = 0;
  }

  for (byte i = 0; i < TOTAL_PINS; i++) {
    // pins with analog capability default to analog input
    // otherwise, pins default to digital output
    if (IS_PIN_ANALOG(i)) {
      // turns off pullup, configures everything
      setPinModeCallback(i, PIN_MODE_ANALOG);
    } else if (IS_PIN_DIGITAL(i)) {
      // sets the output to 0, configures portConfigInputs
      setPinModeCallback(i, OUTPUT);
    }
  }
  // by default, do not report any analog inputs
  analogInputsToReport = 0;

  /* send digital inputs to set the initial state on the host computer,
   * since once in the loop(), this firmware will only send on change */
  /*
  TODO: this can never execute, since no pins default to digital input
        but it will be needed when/if we support EEPROM stored config
  for (byte i=0; i < TOTAL_PORTS; i++) {
    outputPort(i, readPort(i, portConfigInputs[i]), true);
  }
  */
  isResetting = false;
}

void waitForVSync() {
  while (digitalRead(vsyncPin) == HIGH);
  while (digitalRead(vsyncPin) == LOW);
}

int readCameraByte() {   //Lecture données pixels
  return PINA;  // Lecture directe (8 bits)
}

void setupMCLK() {     //Horloge virtuel
  pinMode(mclkPin, OUTPUT);
  TCCR1A = _BV(COM1A0);
  TCCR1B = _BV(WGM12) | _BV(CS10);
  OCR1A=0;
}

void setup() {
  Serial.begin(57600); //Port USB Firmata
  Serial1.begin(9600); //2e port pour communication python de x et y (données passant par UNO)
  Wire.begin();
  DDRA=0x00;

  pinMode(vsyncPin, INPUT);
  pinMode(pclkPin, INPUT);
  for (int i = 0; i < 8; i++) {
    pinMode(dataPins[i], INPUT);
  }
  setupMCLK();
  
  Firmata.setFirmwareVersion(FIRMATA_FIRMWARE_MAJOR_VERSION, FIRMATA_FIRMWARE_MINOR_VERSION);
  Firmata.attach(ANALOG_MESSAGE, analogWriteCallback);
  Firmata.attach(DIGITAL_MESSAGE, digitalWriteCallback);
  Firmata.attach(SET_PIN_MODE, setPinModeCallback);
  Firmata.attach(SYSTEM_RESET, systemResetCallback);
  Firmata.begin(Serial);
}

void loop() {
  Firmata.processInput();  // Communication avec Python si nécessaire

  waitForVSync();          // Attente début d'image
  bool isLuminance = true;  // On commence par supposer qu’on lit un Y
  maxVal=-1;
  brightX, brightY = 0,0;

  for (int y = 0; y < 240; y+=6) {      //Lecture 1 pixel sur 8 pour gagner en rapidité       
    for (int x = 0; x < 320; x+=8) {      
      while (digitalRead(pclkPin) == LOW);  // Attente front montant PCLK
      int pixelVal = readCameraByte();      // Lecture pixel
      while (digitalRead(pclkPin) == HIGH); // Fin cycle PCLK

      if (isLuminance) {
        if (pixelVal > maxVal && pixelVal>100) { //seuil valeur luminance (0-255)
          maxVal = pixelVal;
          brightX = x;
          brightY = y;
        }
      }
      isLuminance = !isLuminance;   //On saute les U/V un octet sur deux
    }
  }

  Serial1.print(brightX);
  Serial1.print(",");
  Serial1.print(brightY);
  Serial1.print(",");
  Serial1.println(maxVal);
}

Voila le programme que j'utilise pour simplement recevoir (x,y)

from pyfirmata import ArduinoMega, util
import serial

# Firmata et Serial
board = ArduinoMega('COM15')         # Contrôle moteurs
xy_serial = serial.Serial('COM18', 9600, timeout=0.5)

# Moteurs
motor1_dir = board.digital[3]   # Moteur X
motor1_pwm = board.digital[5]
motor2_dir = board.digital[4]  # Moteur Y
motor2_pwm = board.digital[6]
motor1_pwm.mode = 3
motor2_pwm.mode = 3

try: 
    while True:
        line = xy_serial.readline().decode().strip()
        if ',' in line:
            try:
                x, y, maxVal = map(int, line.split(','))
                print(f"Reçu x = {x}, y = {y}, maxVal = {maxVal}")
            except:
                print("Erreur de parsing :", line)


except KeyboardInterrupt:
    print("Arrêt manuel.")
    motor1_pwm.write(0)
    motor2_pwm.write(0)
    board.exit()

Dans la pièce: Reçu x = 192, y = 60, maxVal = 161
Dans le noir: Reçu x = 144, y = 6, maxVal = 133
Reçu x = 112, y = 18, maxVal = 157
Reçu x = 32, y = 6, maxVal = 175
Lampe juste devant la cam: Reçu x = 80, y = 216, maxVal = 247
Reçu x = 128, y = 72, maxVal = 171
Reçu x = 48, y = 72, maxVal = 159

Je tiens à rajouter un oubli: de base je voulais faire le traitement sur python via openCV et PIL mais openCV n'est pas dispo dans mon établissement (impossible à installer même avec session admin j'ai demandé et essayé), j'ai essayer d'utiliser une machine virtuelle (venv) mais cmd n'a pas les droits: j’exécute un programme mais rien ne s'affiche ni se produit. J'utilise donc une mega, qui est d'ailleurs surement mieux car l'envoie pixel par pixel prendrais du temps donc autant traiter sur l'IDE.

:warning:
Post mis dans la mauvaise section, on parle anglais dans les forums généraux. déplacé vers le forum francophone.

Merci de prendre en compte les recommandations listées dans Les bonnes pratiques du Forum Francophone