آموزش ردیابی توپ و دروازه‌ها با دوربین OpenMV در ربات فوتبالیست RCJ

در لیگ RCJ Soccer یکی از چالش‌های اصلی، داشتن دید ۳۶۰ درجه برای تشخیص توپ و دروازه‌هاست. یک روش ساده و کاربردی برای این کار، استفاده از آینه کاتادیوپتریک (Catadioptric Mirror) و دوربین OpenMV است.

blogpost cover

۱. مقدمه

در لیگ RCJ Soccer یکی از چالش‌های اصلی، داشتن دید ۳۶۰ درجه برای تشخیص توپ و دروازه‌هاست. یک روش ساده و کاربردی برای این کار، استفاده از آینه کاتادیوپتریک (Catadioptric Mirror) و دوربین OpenMV است. در شکل زیر دید کاتادیوپتریک یا دید 360 درجه با استفاده از یک آینه محدب و یک دوربین OpenMV مشاهده میکنید.

Catadioptric Camera - an overview | ScienceDirect Topics

این ترکیب به ربات اجازه می‌دهد که با یک دوربین ساده، تمام زمین فوتبال را ببیند و موقعیت توپ و دروازه‌ها را شناسایی کند.

در این آموزش یاد می‌گیریم:

  • تنظیم دوربین OpenMV برای دید ۳۶۰ درجه

  • ردیابی توپ (رنگ نارنجی)

  • ردیابی دروازه زرد و آبی

  • ارسال مختصات تشخیص داده شده به ربات از طریق I2C


۲. سخت‌افزار مورد نیاز

  • دوربین OpenMV H7 یا H7 Plus

  • آینه کاتادیوپتریک ۳۶۰ درجه (مخصوص ربات‌های فوتبالیست)

  • ربات RCJ Soccer با میکروکنترلر (مثل STM32 یا آردوینو)

  • کابل USB برای پروگرام کردن OpenMV


۳. تنظیم دوربین OpenMV

  1. نصب نرم‌افزار OpenMV IDE روی لپ‌تاپ

  2. اتصال دوربین به کامپیوتر از طریق USB

  3. انتخاب QVGA (320×240) به‌عنوان رزولوشن تصویر

  4. خاموش کردن تنظیمات خودکار:

    • Auto Gain

    • Auto White Balance

چون برای ردیابی رنگ باید این موارد ثابت باشند.


۴. توضیح کد

📍 راه‌اندازی دوربین

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(time=100)

sensor.set_auto_gain(False)      # غیرفعال کردن تنظیم خودکار نور
sensor.set_auto_whitebal(False)  # غیرفعال کردن تراز سفیدی خودکار
📍 تعریف رنگ‌ها (Thresholds)

برای پیدا کردن اجسام، باید محدوده رنگ تعریف کنیم:

threshold_Ball = [10, 60, 20, 60, 10, 80]     # توپ نارنجی
threshold_Yellow = [30, 90, -20, 20, 25, 80]  # دروازه زرد
threshold_Blue = [40, 60, -10, 10, -30, -10]  # دروازه آبی

📍 پیدا کردن بزرگ‌ترین Blob

گاهی چند لکه هم‌رنگ دیده می‌شوند. ما باید بزرگ‌ترین Blob را به‌عنوان هدف انتخاب کنیم:

 
def bigest(blobs):
    m = blobs[0].area()
    b = blobs[0]
    for blob in blobs:
        if blob.area() > m:
            m = blob.area()
            b = blob
    return b

📍 پردازش تصویر

در حلقه اصلی، هر بار تصویر گرفته می‌شود و رنگ‌ها بررسی می‌شوند:

 
yellow_blobs = img.find_blobs([threshold_Yellow], pixels_threshold=10, area_threshold=10, merge=True, margin=10)
blue_blobs   = img.find_blobs([threshold_Blue], pixels_threshold=3, area_threshold=3, merge=True, margin=10)
orenge_blobs = img.find_blobs([threshold_Ball], pixels_threshold=5, area_threshold=10, merge=True, margin=10)

سپس برای هر کدام (اگر پیدا شدند) مختصات مرکز محاسبه می‌شود:

 
x_yellow = int(goaly.x() + goaly.w()/2)
y_yellow = int(goaly.y() + goaly.h()/2)

📍 نمایش روی تصویر

برای دیباگ و تست، روی تصویر مستطیل یا دایره می‌کشیم:

 
img.draw_rectangle(goaly.rect(), (250,250,0), fill=False)   # زرد
img.draw_rectangle(goalb.rect(), (0,0,250), fill=False)     # آبی
img.draw_circle(x_ball, y_ball, r+3, (255,255,255), fill=False) # توپ

📍 ارسال داده به ربات

در پایان مختصات تشخیص داده شده از طریق I2C فرستاده می‌شود:

data = ustruct.pack("

۵. اجرای کد:

 
  1. کد را در OpenMV IDE کپی کنید.
  2. روی برد Run بزنید.
  3. دوربین باید توپ و دروازه‌ها را تشخیص دهد و روی تصویر نمایش دهد.
  4. اگر نور یا رنگ توپ/دروازه متفاوت بود، باید مقادیر Threshold‌ها را تغییر دهید.

۶. نکات مهم:

  • محیط مسابقه نورهای مختلف دارد؛ بهتر است قبل از شروع بازی Threshold رنگ‌ها را تنظیم کنید.
  • برای دقت بیشتر می‌توانید از فیلترهای Morphological (erode/dilate) یا Gaussian blur استفاده کنید.
  • اگر تصویر نویز زیادی داشت، مقدار pixels_threshold و area_threshold را بالاتر ببرید.

۷. نتیجه:

با این روش، ربات شما می‌تواند با دید ۳۶۰ درجه توپ و دروازه‌ها را شناسایی کند و مختصات آنها را به کنترلر اصلی بفرستد. در ادامه می‌توان الگوریتم‌های مسیریابی و تاکتیک‌های فوتبالی را روی این داده‌ها پیاده‌سازی کرد.

کد نهایی و کامل:

import sensor
import time
import pyb
import ustruct

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(time=100)
# sensor.set_auto_gain(False)  # must be turned off for color tracking
sensor.set_auto_whitebal(True)  # must be turned off for color tracking
sensor.set_auto_exposure(False, exposure_us=5000)  # Adjust exposure time in microseconds
sensor.set_auto_gain(False, gain_db=10)
# Initialize I2C
i2c = pyb.I2C(2, pyb.I2C.SLAVE, addr=0x42)

# Values to send
x_ball = 0
y_ball = 0
x_yellow = 0
y_yellow = 0
x_blue = 0
y_blue = 0


clock = time.clock()

threshold_Ball = [-30, 60, 20, 60, 10, 80]
threshold_Yellow = [60, 80, -30, 10, 50, 100]
threshold_Blue = [30, 30, -20, 10, -50, -20]
tr = [
    threshold_Ball,
    threshold_Yellow,
    threshold_Blue
]

def bigest(blobs):
    m = blobs[0].area()
    b = blobs[0]
    for blob in blobs:
        if blob.area() > m:
            m = blob.area()
            b = blob
    return b

while True:
    clock.tick()
    img = sensor.snapshot()

    yellow_blobs = img.find_blobs([threshold_Yellow], pixels_threshold=3, area_threshold=3, merge=True, margin=10)
    blue_blobs = img.find_blobs([threshold_Blue], pixels_threshold=3, area_threshold=3, merge=True, margin=10)
    orenge_blobs = img.find_blobs([threshold_Ball], pixels_threshold=5, area_threshold=10, merge=True, margin=10)

    if len(yellow_blobs) > 0:
        goaly = bigest(yellow_blobs)
        img.draw_rectangle(goaly.rect(), (250,250,0), fill=True)
        x_yellow = int(goaly.x() + goaly.w()/2)
        y_yellow = int(goaly.y() + goaly.h()/2)
    else:
        x_yellow = 0
        y_yellow = 0
    if len(blue_blobs) > 0:
        goalb = bigest(blue_blobs)
        img.draw_rectangle(goalb.rect(), (0,0,250), fill=True)
        x_blue = int(goalb.x() + goalb.w()/2)
        y_blue = int(goalb.y() + goalb.h()/2)
    else:
        x_blue = 0
        y_blue = 0
    if len(orenge_blobs) > 0:
        ball = bigest(orenge_blobs)
        r = int(ball.w()/2)
        x_ball = int(ball.x() + ball.w()/2)
        y_ball = int(ball.y() + ball.h()/2)
        img.draw_circle(x_ball, y_ball, r, (250,100,0), fill=True)
    else:
        x_ball = 0
        y_ball = 0

    try:
        data = ustruct.pack("