United Statesからアクセスされていますが、言語設定をEnglishに切り替えますか?
Switch to English site
Skip to main content

AIによる人追跡ショッピングカートの開発 ~パート4: ソフトウェア

人を追跡するショッピングカートの制御ユニットを開発するシリーズ記事。最終的なソフトの開発とテストに進みます。

Follow Trolley

はじめに

このシリーズのパート3 では、人追跡カートの機構の組み立てについて説明し、いくつかのハードテスト様のPythonスクリプトを作成しました。この記事では、人追跡カートに実装するオリジナルのPythonアプリ「FastMOT」について解説するとともに、実際のテストをしてみます。 

カートの操作部

以前作成したテストスクリプトを参考にし、オリジナルの制御ボードに組み込むPythonモジュールを作成してみました。

import RPi.GPIO as GPIO
import time

front_prox_pin = 24
rear_prox_pin = 26

motor_left_pwm_pin = 32
motor_right_pwm_pin = 33

'''
Important note on direction pins
	Setting high set direction to forward
	Setting low sets direction to reverse
'''
motor_left_direction = 29
motor_right_direction = 31

red_pin = 19
amber_pin = 21
green_pin = 23

prox_pins = [front_prox_pin, rear_prox_pin]
stacklight_pins = [red_pin, amber_pin, green_pin]
motor_pins = [motor_left_pwm_pin, motor_right_pwm_pin, motor_left_direction, motor_right_direction]

class Trolley(object):
	LIGHT_RED = 0
	LIGHT_AMBER = 1
	LIGHT_GREEN = 2

	MOTOR_LEFT = 0
	MOTOR_RIGHT = 1

	FORWARD = True
	REVERSE = False
	STOP = -1

...


モジュールには、複数の関数を含む1つのクラスのみが含まれており、各関数はカートのハードウェアを制御する特定の機能を実行します。

def __init__(self):
		super(Trolley, self).__init__()
		GPIO.setmode(GPIO.BOARD)
		GPIO.setup(prox_pins, GPIO.IN)
		GPIO.setup(stacklight_pins, GPIO.OUT, initial=GPIO.LOW)
		GPIO.setup(motor_pins, GPIO.OUT, initial=GPIO.LOW)

		self.pwmLeft = GPIO.PWM(motor_left_pwm_pin, 20000)
		self.pwmRight = GPIO.PWM(motor_right_pwm_pin, 20000)

		self.pwmLeft.start(0)
		self.pwmRight.start(0)
		
		self.frontProxBlocked = False
		self.rearProxBlocked = False

		self.motorState = Trolley.STOP

		GPIO.add_event_detect(front_prox_pin, GPIO.BOTH, callback=self._proximityCallback)
		GPIO.add_event_detect(rear_prox_pin, GPIO.BOTH, callback=self._proximityCallback)

		self.stop()
		self.setStacklight(Trolley.LIGHT_AMBER, True)

クラスがインスタンス化されると、Pythonインタープリターによって __init(self)__ 関数が呼び出されます。この関数では、ハードウェアの初期化が行われます。  具体的には、GPIO ピンを適切なモードに設定し、モーターの制御に使用される2つのPWMチャネルを有効にし、近接センサーを処理するイベントコールバックを追加して、アンバースタックライトをオンにしています。

パート3で説明されているように、モータコントローラは、Jetson Nanoが生成するPWM信号ではなく、アナログ電圧入力を期待します。PWM周波数は十分に高く設定されているため、低パスフィルタは信号を滑らかにし、一定のDC電圧を生成し、それがバッファされてコントローラを命令できるようになっています。

def _proximityCallback(self, channel):
		if channel == front_prox_pin:
			self.frontProxBlocked = GPIO.input(channel)

			# Check direction & stop if necessary
			if self.frontProxBlocked and self.motorState == Trolley.FORWARD:
				self.stop()

		if channel == rear_prox_pin:
			self.rearProxBlocked = GPIO.input(channel)

			# Check direction & stop if necessary
			if self.rearProxBlocked and self.motorState == Trolley.REVERSE:
				self.stop()

近接センサーの状態変化を処理するためにコールバックを使用しました。  これにより、プログラムがwhileループで入力をチェックする状態のままになるのを回避できます。入力ピンの状態が変化すると、GPIOチャネルを引数としてコールバックが起動されます。次に、これがコード内で読み取られ、if ステートメントに入力されます。  if ステートメントは、センサーがフロントセンサーかリアセンサーかを判断し、方向をチェックします。  カートがトリガーされたセンサーの方向に移動している場合は、すべての動きが停止します。 

def setMotor(self, motor, direction=True, dutyCycle=0):
		if motor == Trolley.MOTOR_LEFT:
			GPIO.output(motor_left_direction, direction)
			time.sleep(0.05)
			self.pwmLeft.ChangeDutyCycle(dutyCycle)

		if motor == Trolley.MOTOR_RIGHT:
			GPIO.output(motor_right_direction, direction)
			time.sleep(0.05)
			self.pwmRight.ChangeDutyCycle(dutyCycle)

モーターを制御するために、モーター、速度、方向の 3 つのパラメーターを取るヘルパー関数が作成されました。関数内では、方向を制御するGPIOピンは、前進の場合はハイ、後進の場合はローに設定されます。次に、モーターコントローラーの方向リレーが確実に動いてから、モーターに指令するPWM値を出力するように、小さな遅延が追加されます。 

def turnLeft(self, dutyCycle=25):
		# Forklift steering - engage right motor
		if not self.frontProxBlocked or self.rearProxBlocked:
			# Stop left motor
			self.setMotor(Trolley.MOTOR_LEFT, dutyCycle=0)

			self.setMotor(Trolley.MOTOR_RIGHT, direction=Trolley.FORWARD, dutyCycle=dutyCycle)

	def turnRight(self, dutyCycle=25):
		# Forklift steering - engage left motor
		if not self.frontProxBlocked or self.rearProxBlocked:
			# Stop right motor
			self.setMotor(Trolley.MOTOR_RIGHT, dutyCycle=0)

			self.setMotor(Trolley.MOTOR_LEFT, direction=Trolley.FORWARD, dutyCycle=dutyCycle)

カートを左右に操舵する制御を行う追加の関数が記述されました。  これは、上記の関数のラッパーです。近くの人にぶつかるのを回避するために、これらの操舵関数は、まず近接センサーをチェックして、どちらもブロックされていないことを確認します。両方の近接センサーがクリアな状態で、コードはモーターを目的の方向と速度に設定し続けます。

カートは後輪駆動であるため、モーターを目的の旋回方向とは反対の方向に設定する必要があります。  つまり、左に旋回するには、左のモーターを後進に設定し、右のモーターを前進に設定します。その逆も同様です。 

追跡機能

パート1では、2 つの異なる人物追跡アプリケーションを検討し、FastMOT に落ち着きました。Python Trollyクラスが手元にあるので、追跡をハードウェアコントロールに接続する作業に進みました。 

def step(self, frame):
  """
  Runs multiple object tracker on the next frame.
  Parameters
  ----------
  frame : ndarray
   The next frame.
  """
  detections = []
  if self.frame_count == 0:
   detections = self.detector(frame)
   self.tracker.initiate(frame, detections)
  else:
   if self.frame_count % self.detector_frame_skip == 0:
    tic = time.perf_counter()
    self.detector.detect_async(frame)
    self.preproc_time += time.perf_counter() - tic
    tic = time.perf_counter()
    self.tracker.compute_flow(frame)
    detections = self.detector.postprocess()
    self.detector_time += time.perf_counter() - tic
    tic = time.perf_counter()
    self.extractor.extract_async(frame, detections)
    self.tracker.apply_kalman()
    embeddings = self.extractor.postprocess()
    self.extractor_time += time.perf_counter() - tic
    tic = time.perf_counter()
    self.tracker.update(self.frame_count, detections, embeddings)
    self.association_time += time.perf_counter() - tic
    self.detector_frame_count += 1
   else:
    tic = time.perf_counter()
    self.tracker.track(frame)
    self.tracker_time += time.perf_counter() - tic

  if self.draw:
   self._draw(frame, detections)
  self.frame_count += 1

アプリケーションを -g オプションで起動すると、検出状態が、カメラからのライブ映像に重ねられてインタフェース上に表示されます。  これには、境界ボックスと識別アルゴリズムによって割り当てられた ID が含まれます。

while not args.gui or cv2.getWindowProperty("Video", 0) >= 0:
   frame = stream.read()
   if frame is None:
    break

   if args.mot:
    mot.step(frame)
    if log is not None:
     for track in mot.visible_tracks:
      tl = track.tlbr[:2] / config['resize_to'] * stream.resolution
      br = track.tlbr[2:] / config['resize_to'] * stream.resolution
      w, h = br - tl + 1
      log.write(f'{mot.frame_count},{track.trk_id},{tl[0]:.6f},{tl[1]:.6f},'
         f'{w:.6f},{h:.6f},-1,-1,-1\n')

   if args.gui:
    cv2.imshow('Video', frame)
    if cv2.waitKey(1) & 0xFF == 27:
     break
   if args.output_uri is not None:
    stream.write(frame)

  toc = time.perf_counter()
  elapsed_time = toc - tic

この知識を得て、アプリケーションのメインエントリポイントである「app.py」を見てみました。プログラムはwhileループで実行され、フレームをトラッカーに繰り返し送り込み、境界が追加された画像を画面に表示します。

追跡データは、プロパティ「mot.visible_tracks」によって返されます。このデータ構造に何が含まれているかを把握するために、各トラックを反復処理してコンソールに出力してみました。境界ボックスの左上隅と右下隅の座標が含まれていたので、検出された人物の水平中心を簡単に把握できます。

if mot.visible_tracks:
     t.setStacklight(Trolley.LIGHT_RED, state=True)
     # Trolley command
     for track in mot.visible_tracks:
      tl, br = tuple(track.tlbr[:2]), tuple(track.tlbr[2:])
      tx = tl[0]
      ty = tl[1]
      bx = br[0]
      by = br[1]
      mx = int(((bx - tx) / 2) + tx)
      my = int(((by - ty) / 2) + ty)
      print("ID: {}, middleX: {}, middleY: {}, motorStatus: {}".format(track.trk_id, mx, my, motorStatus))
      cv2.drawMarker(frame, (mx, my), color=(0, 255, 0), markerType=cv2.MARKER_CROSS, thickness=2)

      websocketSendData['data'].update({
       'trk_id': track.trk_id,
       'mx': mx,
       'my': my,
       })
     
      if mx > fwh+centreLimits and motorStatus == 0:
       print("Right")
       motorStatus = 1
       t.turnRight(dutyCycle=7)

      if mx < fwh-centreLimits and motorStatus == 0:
       print("Left")
       motorStatus = -1
       t.turnLeft(dutyCycle=7)

      if mx < fwh+(centreLimits/2) and motorStatus == 1 and not t.getFrontProx():
       print("Forward after right")
       motorStatus = 0
       t.forward(dutyCycle=12)

      if mx > fwh-(centreLimits/2) and motorStatus == -1 and not t.getFrontProx():
       print("Forward after left")
       motorStatus = 0
       t.forward(dutyCycle=12)
    else:
     t.setStacklight(Trolley.LIGHT_RED)
     t.stop()

境界ボックスの重心がわかったので、診断補助としてビデオ出力に線を追加する作業に移りました。カートの制御ロジックはシンプルで、簡単に拡張できます。  コードにはフレームの中央に「デッドバンド」があり、前方の近接センサーがブロックされていない場合は、前方への移動のみが行われます。

対象の人物がこのデッドバンド領域から左または右に移動すると、コードは近接センサーがブロックされていないか確認し、ターゲットをフレームの中央に配置すべく移動を開始します。これは人物を追跡するためのかなり粗雑なBangBangアルゴリズム(ON/OFF制御)ですが、テストでは満足のいくパフォーマンスであることがわかりました。

カートを制御するためのより良いソリューションは、PIDループを使用して人物をより正確に追跡することです。これにより、現在の制御アルゴリズムのぎくしゃくした動きも軽減されます。さらに、追跡を支援するために広角カメラレンズを使用する必要があります。  視野が狭いと、オブジェクト追跡モデルと再識別が多少困難になることがわかりました。

デモ動画

まとめ

このプロジェクトでは、AIと機械学習モデルを実行する際のNVIDIA Jetson Nanoの処理能力を実証し、その後、AIの補助的な使用法を実証して、人追跡カートを構築しました。

ソースコード等一式はGitHubのリポジトリに公開しています

シリーズ記事のリンク:

Engineer of mechanical and electronic things by day, and a designer of rather amusing, rather terrible electric "vehicles" by night.

コメント