人数カウント。。。?
巷のマーケティングの記事などを読むと「店舗の状態を把握するには、何かを購入した人数だけではなく、売り場やROIの人数を時間帯などで把握することが重要」なんて書かれていたりします。では、人数カウントってどうやってやるのがいいのか??なんて記事もあったりしますが、概ね以下のようなものでやることが多いようです。
- 赤外線(ブレイクビーム)
- サーマル
- レーザー
- 超音波
- 人手によるカウント(イベント時等)
このように色々と方法はあるものの、意外と精度の問題、例えば赤外線の場合入り口のある程度の高さに水平に装置が設置される、線が途切れる回数をカウントすることで人数を把握していますが、並列に同時に人間が通れば当然一人分しかカウントされません。またレーザーやサーマルのように天井から床に向けて設置する機材もありますが精度は高いけど高価であったり、なかなか安定しないものもあります。
そういった中で最近注目されているのが「IPカメラ等による人数カウント」です。BOSCHやMOBOTIXのカメラのようにカメラ自体に人数カウントを行なう機能がある場合にはカメラだけでも大丈夫ですが、一般的なIPカメラやUSBカメラの場合には、カメラから送られてきた画像から人数を読み取るアプリケーションが必要になります。こういったアプリケーションは当然非常に高度なものになるので、普通の人が1から作れるものではありません。そこで現在世界中で広く使用されている画像認識のソフトウェアとして、OpenCVが有名になっております。
OpenCVの使い方
OpenCVは元々Cで書かれており、しばらくC言語でのみ実装が可能でした。これも中々敷居が高いため利用する方もそれなりの方が多かったのですが、Pythonをサポートするようになったことで、世界的にも爆発的に利用者が増えました。今回ご紹介する環境は、
- Windows 10 (64bit)
- Anaconda3
- Python 2.7(Anacondaのenvとして登録)
- condaでのOpenCV 3.1のインストールを行ったもの
で実行しています。
ここでは詳細を述べませんが、基本的な動作要件を満たしていれば、下記コードは動くのではと思います。
コードを公開した理由
OpenCVによる人数カウント、という言葉でGoogleやら色々な検索エンジンを検索すると、ヒントは出てくるもののコードそのものには中々当たりません。私も相当数調べたのですが、結局そこに該当するものには当たりませんでした。今回私が作成したコードも素人が短い時間で作成した、あまり役に立たないものかもしれませんが、これでOpenCVでの人数カウント人口が増え、ひいてはこのコードを改良してくれる奇特な方がいたら、なおラッキーということが理由です。もし「こうしたら良くなる」とか「そもそもこうしたほうがいい」なんていうコメントを頂けると、非常に嬉しいです。
今回のコード
今回は以下のコード一本で作成しております。
人数データをどこかに書き込むとかそういう高尚な機能は無く、とりあえず画面に表示する仕組みにしております。
コード上は静的な動画ファイルを読み取ってカウントすることにしていますが、実際はカメラのMP4のRTSPソースからでも実行できますし、やっていませんがUSBカメラでも実行できるはずです。静的な動画を読み取る行の直下にストリーム読み取りの行があるので、そちらを使用して下さい。
#使い方
上記で動画ファイルやソースを適宜変更して、
python 02.CV31+fC+TR+RC+CP.py
と実行することで(ファイル名はなんでもいいですが)実行されます。
まず初期化されると人数カウントを行なう場所にマウスで線を引く必要がありますので、カウントしたい開始場所をクリックし、そのままボタンを押しっぱなしにして、終了場所までドラッグしてください。
線は何本でも引けますので、必要数引いたら「ESC」キーを押下し、実行して下さい。
コード
import numpy as np
import cv2
#cap = cv2.VideoCapture('D:\\Work_Documents\\sandbox\\OpenCV\\with_EEN\\viaVLC\\EN-CDUM-002a+2016-08-29+14-38-40.mp4') #Open video file
cap = cv2.VideoCapture('C:\\Work_Documents\\sandbox\\OpenCV\\with_EEN\\viaVLC\\camera0490_02.mp4')
#cap = cv2.VideoCapture('http://127.0.0.1:8080')
fps = 15 #int(cap.get(5)+4)
print 'Current FPS is ' + str(fps)
#cv2.ocl.setUseOpenCL(False)
fgbg = cv2.createBackgroundSubtractorKNN(detectShadows = True) #Create the background substractor
# initialize var and windows
itr = 0
crossed = 0
font = cv2.FONT_HERSHEY_SIMPLEX
old_center = np.empty((0,2), float)
'''
cv2.namedWindow("Frame", cv2.WINDOW_KEEPRATIO | cv2.WINDOW_NORMAL)
cv2.namedWindow("Background Substraction", cv2.WINDOW_KEEPRATIO | cv2.WINDOW_NORMAL)
cv2.namedWindow("Contours", cv2.WINDOW_KEEPRATIO | cv2.WINDOW_NORMAL)
'''
# define functions
def padding_position(x, y, w, h, p):
return x - p, y - p, w + p * 2, h + p * 2
# find a nearest neighbour point
def serchNN(p0, ps):
L = np.array([])
for i in xrange(ps.shape[0]):
L = np.append(L,np.linalg.norm(ps[i]-p0))
return ps[np.argmin(L)]
# check intersect 2 lines
def isIntersect(ap1, ap2, bp1, bp2):
calc1 = ((ap1[0] - ap2[0]) * (bp1[1] - ap1[1]) + (ap1[1] - ap2[1]) * (ap1[0] - bp1[0])) * ((ap1[0] - ap2[0]) * (bp2[1] - ap1[1]) + (ap1[1] - ap2[1]) * (ap1[0] - bp2[0]))
calc2 = ((bp1[0] - bp2[0]) * (ap1[1] - bp1[1]) + (bp1[1] - bp2[1]) * (bp1[0] - ap1[0])) * ((bp1[0] - bp2[0]) * (ap2[1] - bp1[1]) + (bp1[1] - bp2[1]) * (bp1[0] - ap2[0]))
if (calc1 < 0):
if (calc2 < 0):
return True
return False
# apply convexHull to the contour
def convHull(cnt):
epsilon = 0.1*cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
hull = cv2.convexHull(cnt, returnPoints = True)
return hull
# detect a centroid from a coutour
def centroidPL(cnt):
M = cv2.moments(cnt)
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
return cx,cy
# display 1st frame and set counting line
ret, img = cap.read()
img = cv2.putText(img, 'Please draw a line with drug the mouse.', (img.shape[1]/2-300, img.shape[0]/2), font, 1, (0,0,255), 2, cv2.LINE_AA)
img = cv2.putText(img, 'Finish the draw, press ESC. \n Retry, press "r".', (img.shape[1]/2-300, img.shape[0]/2+40), font, 1, (0,0,255), 2, cv2.LINE_AA)
img = cv2.putText(img, 'Retry, press "r".', (img.shape[1]/2-300, img.shape[0]/2+80), font, 1, (0,0,255), 2, cv2.LINE_AA)
img = cv2.resize(img, (img.shape[1]/2, img.shape[0]/2))
imgr = img.copy()
sx,sy = -1,-1
ex,ey = -1,-1
def draw_line(event,x,y,flags,param):
global sx,sy,ex,ey
if event == cv2.EVENT_LBUTTONDOWN:
sx,sy = x,y
elif event == cv2.EVENT_LBUTTONUP:
cv2.line(img,(sx,sy),(x,y),(255,0,0), 2)
ex,ey = x,y
cv2.namedWindow('Draw_Line')
cv2.setMouseCallback('Draw_Line',draw_line)
while(1):
cv2.imshow('Draw_Line',img)
k = cv2.waitKey(20) & 0xFF
if k == 27:
break
elif k == ord('r'):
img = imgr.copy()
continue
cv2.destroyAllWindows()
# initialize line
lp0 = (sx, sy)
lp1 = (ex, ey)
nlp0 = np.array([lp0[0], lp0[1]], float)
nlp1 = np.array([lp1[0], lp1[1]], float)
while(cap.isOpened()):
try:
ret, o_frame = cap.read() #read a frame
frame = cv2.resize(o_frame, (o_frame.shape[1]/2, o_frame.shape[0]/2))
#Use the substractor
fgmask = fgbg.apply(frame)
fgmask_o = fgmask.copy()
fgmask = cv2.threshold(fgmask, 244, 255, cv2.THRESH_BINARY)[1]
kernel = np.ones((5,5), np.uint8)
# fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_CLOSE, kernel)
fgmask = cv2.dilate(fgmask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3)), iterations = 2)
im2, contours, hierarchy = cv2.findContours(fgmask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# initialize var iteration
new_center = np.empty((0,2), float)
for c in contours:
if (itr % fps == 0):
continue
# calc the area
cArea = cv2.contourArea(c)
if cArea < 300: # if 1280x960 set to 50000, 640x480 set to 12500
continue
# apply the convex hull
c = convHull(c)
# rectangle area
x, y, w, h = cv2.boundingRect(c)
x, y, w, h = padding_position(x, y, w, h, 5)
# center point
cx, cy = centroidPL(c)
new_point = np.array([cx, cy], float)
new_center = np.append(new_center, np.array([[cx, cy]]), axis=0)
if (old_center.size > 1):
#print cArea and new center point
print 'Loop: ' + str(itr) + ' Coutours #: ' + str(len(contours))
print 'New Center :' + str(cx) + ',' + str(cy)
#print 'New Center :' + str(new_center)
# calicurate nearest old center point
old_point_t = serchNN(new_point, old_center)
# check the old center point in the counding box
if (cv2.pointPolygonTest(c, (old_point_t[0], old_point_t[1]), True) > 0):
old_point = old_point_t
print 'Old Center :' + str(int(old_point[0])) + ',' + str(int(old_point[1]))
# put line between old_center to new_center
cv2.line(frame, (int(old_point[0]), int(old_point[1])), (cx, cy), (0,0,255), 2)
# cross line check
if (isIntersect(nlp0, nlp1, old_point, new_point)):
print 'Crossing!'
crossed += 1
# put floating text
cv2.putText(frame, 'CA:' + str(cArea)[0:-2] , (x+10, y+20), font, 0.5, (255,255,255), 1, cv2.LINE_AA)
# draw center
cv2.circle(frame,(cx,cy),5,(0,0,255),-1)
# draw rectangle or contour
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 3) #rectangle contour
# cv2.drawContours(frame, [c], 0, (0,255,0), 2)
# cv2.polylines(frame, [c], True, (0,255,0), 2)
# put fixed text, line and show images
cv2.putText(frame, 'Crossing:' + str(crossed), ((o_frame.shape[1]/3), 30), font, 1, (255,255,255), 1, cv2.LINE_AA)
cv2.line(frame, (lp0), (lp1), (255,0,0), 2)
cv2.imshow('Frame',frame)
cv2.imshow('Background Substraction',fgmask_o)
cv2.imshow('Contours',fgmask)
# increase var number and renew center array
old_center = new_center
itr += 1
except:
#if there are no more frames to show...
print('EOF')
break
#Abort and exit with 'Q' or ESC
k = cv2.waitKey(30) & 0xff
if k == 27:
break
cap.release() #release video file
cv2.destroyAllWindows() #close all openCV windows
コードの解説
後編でコードの解説を行いますので、しばしお待ち下さい。