見出し画像

BoeingDGK 2025年大会機体紹介

BoeingDGK ソフト担当

1/12関東ブロック出場機体「INFINITY」

大会結果

東東京ノードで優勝して推薦されたRoboCupJunior RescueLine 2025
関東ブロックでは、
競技順位 7位
プレゼンテーションポスター賞
(下に添付してあります)
をいただきました。全国大会の推薦はありません。
正直言葉にできないほどに悔しかったです。自分たちの甘かった覚悟や認識を7位という結果で突きつけられた感じがしました。

この悔しさと反省を来年大会に生かすため、振り返りといった形で初めてのブログを書かせていただきます。もしよければ読んでいってください。


コンセプト

この機体のコンセプトは
 ・コンパクトであること
 ・整備性を高く保つこと
 ・デバッグのしやすい機体にすること
です。

少しでもハードウェアで他のチームにアドバンテージを取ろうと思い、150*150*150に収まるだいぶコンパクトな機体を設計しました。ソフト担当は壁に機体をぶつける心配をしなくていいので楽でしたが、ハードウェア担当たちはだいぶきつそうでした。

こだわりポイント

LED Matrix

設計時にプログラムの進行状況を外部から確認するために表示器をつけようと考えていて、候補の中で最も自由度の高かったLED matrixを搭載しました。
8×8のピクセルをそれぞれ光らせることができて、しかもI2Cで制御できるからピンを減らす心配もないという優れものです。光るパターンの詳細は冒頭のポスターに記載しています。

アーム

このロボットで最も目を引くのがこのアームです。小さい機体になるべく大きいアーム、荷台を確保するために、昇降機構をリンク機構にしてコンパクトに納めました。(上は動画です。よかったら見てください)


搭載部品

小型ですが十分な量の部品を搭載しています。

センサー類

・赤外線フォトトランジスタ L-51ROPT1D1 ×9
・カラーセンサー S9706 ×2
・ロードセル SC616C 1kg ×2
・ToFセンサー Vl53l0x ×2
・9軸センサー BMX055 ×1
カメラセンサ OpenMV camera H7 ×1 (センサーではない気がするけど)

アクチュエーター

・シリアルサーボ STS3032 ×2 (タイヤの駆動に使用)
・ステッピングモータ 28BYJ-48 ×1 (アーム昇降)
・サーボ SG92R ×2 (アーム開閉)
     MG90D ×1 (荷台開閉)
ステッピングはトルク弱すぎて失敗でした。もう使いません。

その他

・Adafruit Mini 8x8 LED Matrix ×1 

この中でも特に優秀だったのがタイヤに使ったSTS3032です。もうこれ使ったらDCモータとか普通のサーボには戻れないと思います。


ソフトウェア

これは著者の担当分野なのでなるべく詳しく書くつもりです。わかりにくいかもしれませんが、読んでくれると嬉しいです。

開発環境

・Arduino IDE (メインプログラム)
・OpenMV IDE (カメラセンサー)
・Edge Implusle (↑の学習)
開発環境はこの3つです。さらに周りで使っている人はいなかったのですが、Arduino Cloudというweb上で複数の媒体からarduinoをコーディングできるやつがあり、便利なので併せて使っていました。

コンセプト

コンセプトは
・書き換えやすいこと
・デバッグがしやすいこと
・高速で回すこと

でした。
これを満たすためにプログラムをほとんどVoid型の関数で作成するなどの徹底したモジュール化を行い、全体で3500行あるプログラムのうちVoid loop()内はなんと60行!(これが良いことかはわかりませんが)
なのでどのプロセスが進行中なのかが外から見ても一目で分かるようになってます。
デバッグのしやすさに関しては失敗です。大会当日にエラーがでて大幅な
減点をしてしまったのですが(エラー無くても負けてましたが)、原因は単純なのにもかかわらず、シリアルモニタや前方のLEDmatrixへの表示をしなかったがために大会中に気づけず、直せませんでした。これは次大会までの最も大きな反省点です。

プログラム概要

ライバルチームと比べて特別なことはしていません。私がArduino IDEを使いたかったので比較的簡単なEsp32を主要マイコンとして2つ。さらにボール回収用でOpenMV H7を使いました。(Pythonが分からずほぼchatGPTなので解説はできません)
マイコンの役割は2つに分けています。
マインマイコン:ライントレース用
サブマイコン :障害物、救助用

センサーから取得したデータをUartで相互通信させないといけなかったので非常にめんどくさいです。このとき送りあっているのは、今システムで動いているプロセスをメインとサブで共有するためのMODEという変数、プラスでセンサーの値諸々です。

赤旗はマスター権を示す。MODEによって旗は移動し、MasterだったものがSlaveになる。

ライントレース中はメインに、それ以外はサブをMasterにし、Slave側をセンサーのように使います。
2つのESPがセンサー用の値取得用マイコンではなく、それぞれ機体が動かすプログラムが入ってるのでこのような形になりました。わかりにくいですが、コーディングしてて楽しいので私は気に入っています。

救出

多分ここが一番重要ですね。関東ブロックで全員救出に成功していたのはうちだけでした。(競技中ではなく優秀チームデモンストレーションでですが)

プロセス自体は簡単で、Edge Impluseの機械学習モデルで画像検出しオブジェクトの中心座標を求め、OpenMVからUartでサブマイコンにデータを垂れ流しっぱなしにするというものです。サブマイコンは必要な時にだけデータを読みます。

アルゴリズムをざっくり書くと

対象をx色のボールとする (以下xボールと記載)
→xボールが見つかるまで回転
→誤認かどうかを確認
→xボールに接近
→回収
→xボールに対応する救助コーナーが見つかるまで回転(以下xコーナーと記載)
→xコーナーに接近し、体を押し付ける
→向きを180°変えて荷台を開き、回収成功

な感じです。xを決めるときは「何個回収したか」と「残り時間」で大体決めます(xは救助関数の引数にしている)。さらに銀色ボールは2個回収してからコーナーに入れに行きます。

まだ試行回数が少なく、徹底したバグ出しができてないからかもしれませんが、私としてはこのアルゴリズムに欠陥は無いように感じられます。
しかしながら、カメラが不確実なために間違ったデータがサブに送られ、虚空を追っかけてしまうこともしばしば。次は機械学習を見直してリベンジします。

ソフトウェア紹介は以上です。
最後に関東のプログラムをリンクで貼るので気になる方は見てみてください。(虫食いですが)

最後に

以上が2025年関東ブロック機体【INFINITY】の概要です。
全体を通しての反省点は
・試行回数が少なく、バグ出しができていなかったこと
・プログラムが複雑で、チームメンバーと共有できていなかったこと

が大きいと私は思いました。

2026年大会こそは輝かしい結果を手に入れるために、これからもBoeingDGKは活動を続けていきます!皆さまの応援が大きな力となっています。次大会もさらなる高みを目指して挑戦を続けますので、引き続き応援のほどよろしくお願いいたします!

また、本年より株式会社JLCPCB様にご支援いただけることとなりました。心より感謝申し上げます。これからはよろしくお願いいたします!

関連リンク

Twitter
https://x.com/BoeingDGK

Youtube

https://www.youtube.com/@DGKboeing

プログラム

公開可能なプログラムです。2つはArduino Cloud, カメラはコードで付けます。

メインマイコン(ESP32-DevkitC-32e)
https://app.arduino.cc/sketches/12414157-1f25-49f5-8c63-ec39ca67ab67?view-mode=preview

サブマイコン(ESP32-DevkitC-32e)
https://app.arduino.cc/sketches/2f55bd9f-ee59-4e03-8896-348811a123ad?view-mode=preview

OpenMV H7

import sensor, image, time, os, ml, math, uos, gc
from ulab import numpy as np
from pyb import UART
import machine

# センサー初期化
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((240, 240))
sensor.skip_frames(time=2000)

# LEDオブジェクトの作成
red_led = machine.LED("LED_RED")  # 赤色LED
blue_led = machine.LED("LED_BLUE")  # 青色LED

# モデルとラベルの読み込み
net = None
labels = None
min_confidence = 0.7

try:
    net = ml.Model("trained.tflite", load_to_fb=uos.stat('trained.tflite')[6] > (gc.mem_free() - (64 * 1024)))
except Exception as e:
    raise Exception('Failed to load "trained.tflite", did you copy the .tflite and labels.txt file onto the mass-storage device? (' + str(e) + ')')

try:
    labels = [line.rstrip('\n') for line in open("labels.txt")]
except Exception as e:
    raise Exception('Failed to load "labels.txt", did you copy the .tflite and labels.txt file onto the mass-storage device? (' + str(e) + ')')

# 色の設定と閾値リスト
colors = [
    (255, 0, 0), (0, 255, 0), (255, 255, 0),
    (0, 0, 255), (255, 0, 255), (0, 255, 255),
    (255, 255, 255),
]
threshold_list = [(math.ceil(min_confidence * 255), 255)]

# UART設定
uart = UART(3, 115200)
MODE = 0

# 赤色検出の閾値設定
thresholds_red = (30, 100, 15, 127, -64, 127)  # 赤色のHSV値範囲

def fomo_post_process(model, inputs, outputs):
    # FOMOモデルの後処理関数
    ob, oh, ow, oc = model.output_shape[0]
    x_scale = inputs[0].roi[2] / ow
    y_scale = inputs[0].roi[3] / oh
    scale = min(x_scale, y_scale)
    x_offset = ((inputs[0].roi[2] - (ow * scale)) / 2) + inputs[0].roi[0]
    y_offset = ((inputs[0].roi[3] - (ow * scale)) / 2) + inputs[0].roi[1]

    l = [[] for i in range(oc)]
    for i in range(oc):
        img = image.Image(outputs[0][0, :, :, i] * 255)
        blobs = img.find_blobs(
            threshold_list, x_stride=1, y_stride=1, area_threshold=1, pixels_threshold=1
        )
        for b in blobs:
            rect = b.rect()
            x, y, w, h = rect
            score = img.get_statistics(thresholds=threshold_list, roi=rect).l_mean() / 255.0
            x = int((x * scale) + x_offset)
            y = int((y * scale) + y_offset)
            w = int(w * scale)
            h = int(h * scale)
            l[i].append((x, y, w, h, score))
    return l

clock = time.clock()

while True:
    if uart.any():
        request = uart.read(1)
        print("Received request:", request)  # デバッグメッセージ
        try:
            MODE = int.from_bytes(request, "big")  # バイト列を整数に変換
        except ValueError:
            print("Invalid request received")
            MODE = 0
    if MODE == 1:

        #red_led.on()  # 撮影2秒前に赤色LEDを点灯
        clock.tick()
        img = sensor.snapshot()

        detected_objects = {1: [], 2: []}  # ラベル1, 2のオブジェクトを格納

        # ラベル3(赤色物体)の検出
        red_blobs = img.find_blobs([thresholds_red], pixels_threshold=10, area_threshold=10, merge=True)
        if red_blobs:
            # 複数の赤色物体がある場合、範囲を計算
            top_left = min(red_blobs, key=lambda b: (b.rect()[0], b.rect()[1])).rect()
            bottom_right = max(red_blobs, key=lambda b: (b.rect()[0] + b.rect()[2], b.rect()[1] + b.rect()[3])).rect()

            x, y, w, h = (
                top_left[0],
                top_left[1],
                bottom_right[0] + bottom_right[2] - top_left[0],
                bottom_right[1] + bottom_right[3] - top_left[1],
            )

            center_x = x + w // 2
            center_y = y + h // 2

            img.draw_rectangle(x, y, w, h, color=colors[3], thickness=2)
            img.draw_cross(center_x, center_y, color=colors[3], thickness=2)

            uart.write(f"3,{center_x},{center_y},{w},{h}\n")
            print(f"Label 3: Center=({center_x},{center_y}), Width={w}, Height={h}")

        # 他のラベル(1, 2, 4)の検出
        for i, detection_list in enumerate(net.predict([img], callback=fomo_post_process)):
            if i == 0:
                continue
            if len(detection_list) == 0:
                continue

            for x, y, w, h, score in detection_list:
                center_x = math.floor(x + (w / 2))
                center_y = math.floor(y + (h / 2))
                img.draw_rectangle(x, y, w, h, color=colors[i])

                # ラベル1, 2のデータを格納
                if i in [1, 2]:
                    detected_objects[i].append((x, y, x + w, y + h, center_x, center_y))
                else:
                    # ラベル4は個別送信
                    uart.write(f"{i},{center_x},{center_y},{w},{h}\n")
                    print(f"Label {i}: Center=({center_x},{center_y}), Width={w}, Height={h}")

        # ラベル1, 2の範囲計算および送信
        for label in [1, 2]:
            if len(detected_objects[label]) == 1:
                # 単数の場合はそのデータを送信
                obj = detected_objects[label][0]
                uart.write(f"{label},{obj[4]},{obj[5]},{obj[2] - obj[0]},{obj[3] - obj[1]}\n")
                print(f"Label {label}: Center=({obj[4]},{obj[5]}), Width={obj[2] - obj[0]}, Height={obj[3] - obj[1]}")
            elif len(detected_objects[label]) > 1:
                # 複数の場合は範囲計算
                top_left = min(detected_objects[label], key=lambda rect: (rect[0], rect[1]))
                bottom_right = max(detected_objects[label], key=lambda rect: (rect[2], rect[3]))
                center_x = (top_left[0] + bottom_right[2]) // 2
                center_y = (top_left[1] + bottom_right[3]) // 2

                img.draw_rectangle(top_left[0], top_left[1],
                                   bottom_right[2] - top_left[0],
                                   bottom_right[3] - top_left[1],
                                   color=colors[label], thickness=2)
                img.draw_circle(center_x, center_y, 5, color=colors[label], thickness=2)

                uart.write(f"{label},{center_x},{center_y},{bottom_right[2] - top_left[0]},{bottom_right[3] - top_left[1]}\n")
                print(f"Label {label}: Center=({center_x},{center_y}), Width={bottom_right[2] - top_left[0]}, Height={bottom_right[3] - top_left[1]}")

        print(clock.fps(), "fps", end="\n\n")
    else:
        #red_led.off()
        print("not activated")


いいなと思ったら応援しよう!