物体検出モデルとマーカーによる位置検出で商品棚の品切れを検出してみました

2021.03.21

1 はじめに

CX事業本部の平内(SIN)です。

以前、商品棚の品切れを検出する物体検出モデルを作成して見ました。

今回は、この物体検出モデルと、マーカーを組み合わせて、より精度を上げる方法を検討してみました。

最初に、動作している様子です。 前段は、マーカー併用で品切れを検出しているもので、後半は、物体検出モデルの推論結果も併せて表示しています。

2 判定要領

(1) 問題点

「縦長矩形の商品の背景」で学習したモデルは、品切れの場所を高い信頼度で検出できます。

しかし、品切れの商品が2つ並んだりすると、途端に検出できなくなってしまいます。

この時、採用する信頼度を下げて確認すると、低い信頼度で、それなりに検出していることが確認できました。

(2) マーカー併用

そこで、商品棚配置したマーカーを検出し、その相対位置で各商品のエリアを計算します。

そこに、信頼度の低い検出結果を組み合わせて、エリアの重複度から商品の品切を判定することにしました。

3 コード

動作しているコードです。

Amazon Sage Makerで作成した物体検出モデルは、MXNetでMac上で使用しています。

import cv2
import numpy as np
import random
import mxnet as mx
from collections import namedtuple

DEVICE_ID = 1
WIDTH = 800
HEIGHT = 600
FPS = 24

MODEL_PATH = './model/deploy_model_algo_1'

class Model:
	def __init__(self):
		self.__SHAPE = 512
		input_shapes=[('data', (1, 3, self.__SHAPE, self.__SHAPE))]
		self.__Batch = namedtuple('Batch', ['data'])
		sym, arg_params, aux_params = mx.model.load_checkpoint(MODEL_PATH, 0)
		self.__mod = mx.mod.Module(symbol=sym, label_names=[], context=mx.cpu())
		self.__mod.bind(for_training=False, data_shapes=input_shapes)
		self.__mod.set_params(arg_params, aux_params)

	def inference(self, frame):
		# 入力インターフェースへの画像変換
		img = cv2.resize(frame, (self.__SHAPE, self.__SHAPE)) # 600*600 -> 512*512
		img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR -> RGB
		img = img.transpose((2, 0, 1)) # 512,512,3 -> 3,512,512
		img = img[np.newaxis, :] # 3,512,512 -> 1,3,512,512
		# 推論
		self.__mod.forward(self.__Batch([mx.nd.array(img)]))
		prob = self.__mod.get_outputs()[0].asnumpy()
		return np.squeeze(prob)

class Products:
	def __init__(self):
		self.__maker_max = 8
		self.__marker_index = [18, 17, 22, 16, 25, 14, 15, 10]
		self.__reference_points = [(0,0),(0,0),(0,0),(0,0),(0,0),(0,0),(0,0),(0,0)]
		self.__product_area = [[0] * 4] * 15
		self.__marker_detection = [False] * self.__maker_max
		self.__product_names = [
			"亀田の柿の種",
			"でん六豆",
			"ポリッピー(スパイス)",
			"ポリッピー(しお味)",
			"ポリッピー(チョコ)",
			"PRIME",
			"PRETEL",
			"Noir",
			"CRANZ(ORANGE)",
			"CRANZ(RED)",
			"カントリーマム",
			"CHEDDER CHEESE",
			"OREO",
			"チョコメリゼ",
			"フライドポテト(じゃがバター味)"
		]

	# マーカー検出結果
	def maker_detected(self):
		count = 0
		for detection in self.__marker_detection:
			if(detection):
				count += 1
		if(self.__maker_max == count):
			return True
		return False

	# マーカー位置の更新
	def refresh_reference_points(self, ids, corners):
		for i, index in  enumerate(self.__marker_index):
			position = self.__get_marker_mean(ids, corners, index)
			if(position != None):
				self.__marker_detection[i] = True
				self.__reference_points[i] = position
		self.__refresh_product_area()

	# 商品位置の更新
	def __refresh_product_area(self):
		for row in range(3):
			(x1, y1) = self.__reference_points[row*2 + 0]
			x1 += 40
			(x2, y2) = self.__reference_points[row*2 + 1]
			x2 -= 40
			(x3, y3) = self.__reference_points[row*2 + 2]
			x3 += 40
			(x4, y4) = self.__reference_points[row*2 + 3]
			x4 -= 40
			for col in range(5):
				self.__product_area[col + row*5] = [
					int(x1 + (x2-x1)/5 * col),
					int(y1 + (y2-y1)/5 * col),
					int(x3 + (x4-x3)/5 * (col+1)),
					int(y3 + (y4-y3)/5 * (col+1)),
				]
	
	# 商品の品切を判定して表示
	def disp_soldout(self, frame, soldout):
		pos = [False] * 15
		for b in soldout:
			for i in range(15):
				a = self.__product_area[i]
				multiplicity = self.__multiplicity(a, b)
				if(0.3 < multiplicity):
					pos[i] = True
		for i, p in enumerate(pos):
			if p:
				frame = cv2.rectangle(frame, (self.__product_area[i][0], self.__product_area[i][1]), (self.__product_area[i][2], self.__product_area[i][3]), (0,0,255), 3)
				print("{} が売り切れです。".format(self.__product_names[i]))
		
		return frame

	# 重複度の計算
	def __multiplicity(self, a, b):
		(ax_mn, ay_mn) = (a[0], a[1])
		(ax_mx, ay_mx) = (a[2], a[3])
		(bx_mn, by_mn) = (b[0], b[1])
		(bx_mx, by_mx) = (b[2], b[3])
		a_area = (ax_mx - ax_mn + 1) * (ay_mx - ay_mn + 1)
		b_area = (bx_mx - bx_mn + 1) * (by_mx - by_mn + 1)
		abx_mn = max(ax_mn, bx_mn)
		aby_mn = max(ay_mn, by_mn)
		abx_mx = min(ax_mx, bx_mx)
		aby_mx = min(ay_mx, by_mx)
		w = max(0, abx_mx - abx_mn + 1)
		h = max(0, aby_mx - aby_mn + 1)
		intersect = w*h
		return intersect / (a_area + b_area - intersect)

	# インデックスを指定してマーカーの中心座標を取得する
	def __get_marker_mean(self, ids, corners, index):
		for i, id in enumerate(ids):
			# マーカーのインデックス検索
			if(id[0] == index):
				v = np.mean(corners[i][0],axis=0) # マーカーの四隅の座標から中心の座標を取得する
				return [v[0], v[1]]
		return None

def main():

	# マーカー初期化
	aruco = cv2.aruco
	dictionary = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)

	# Webカメラ初期化
	cap = cv2.VideoCapture(DEVICE_ID)
	cap.set(cv2.CAP_PROP_FPS, FPS) 
	cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
	cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)

	# モデル初期化
	model = Model()

	# 商品クラス初期化
	products = Products()
	
	while True:

		print("-------------")
		_, frame = cap.read() 
		
		# マーカ検出
		corners, ids, _ = aruco.detectMarkers(frame, dictionary) 
		if ids is None:
			continue

		# マーカーが全部検出できるまで、判定を行わない
		if(products.maker_detected()==False):
			print("detecting markers...")
		else:
			# 推論
			prob = model.inference(frame)
			
			# 推論結果を画像の解像度に併せる
			soldout = []
			for p in prob:
				index = int(p[0])
				confidence = p[1]
				if(confidence > 0.2): # 信頼度
					x1 = int(p[2] * WIDTH)
					y1 = int(p[3] * HEIGHT)
					x2 = int(p[4] * WIDTH)
					y2 = int(p[5] * HEIGHT)
					soldout.append([x1, y1, x2, y2])

					# 推論結果表示
					frame = cv2.rectangle(frame,(x1, y1), (x2, y2), (255, 0, 0), 1)
					frame = cv2.rectangle(frame,(x1, y1), (x1 + 50, y1-20), (255, 0, 0), -1)
					label = "{:.2f}".format(confidence)
					frame = cv2.putText(frame,label,(x1+2, y1-2), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 1, cv2.LINE_AA)
			
			# 商品の品切を判定して表示
			frame = products.disp_soldout(frame, soldout)

		# マーカー位置を更新
		products.refresh_reference_points(ids, corners)
		#マーカ描画
		aruco.drawDetectedMarkers(frame, corners, ids, (255, 255, 100)) 
		
		cv2.imshow('frame', frame)
		cv2.waitKey(1) 

	cap.release() 
	cv2.destroyAllWindows() 

main()

4 最後に

機械学習のモデルの精度を上げることには限界がありますが、検出場所で判断ができる要件の場合、マーカーの組み合わせが相性が良さそううです。

また、マーカーを基準にすると、カメラのキャリブレーションも簡単になるように思います。