主にプログラミングに関して。Python, .NET Framework(C#), JavaScript, その他いくらか。
記事にあるサンプルやコードは要検証。使用に際しては責任を負いかねます

OpenCV: 他人から見た顔と自分で見た顔の違いをプログラミングで

                
tags: opencv python
 "人は相手の顔を見るとき、左右どちらかの半分しかほぼ見ていない。右利きの人の大半は相手の顔の左半面で印象を判断する"という研究結果があるのをブルーバックスの『単純な脳、複雑な私』を読んで知った。
http://www.amazon.co.jp/dp/4062578301

ここに二枚の画像がある。この二枚をパッと瞬間的に見て、それぞれ男女どちらに見えるだろう。
13110701.jpg
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.100.5720

上の画像が男性、下の画像が女性だと見えたなら、そう見えた人は"相手の顔の左半面で判断する傾向がある人"である。これら二枚の画像は実は男性の顔を半分、女性の顔半分を切り出して合成したもので、上と下ではそれが左右反転したという違いしかない。それでも性別の判断が一貫せずに反転したなら、それは顔の左右どちらか半面だけを見て相手の特徴を判断しているということである。
研究論文: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.100.5720


 上記の研究を知って、個人的に腑に落ちることがあった。ぼくの目は奥二重なのだけど、たいていの人には二重だと認識されていない。相手から見てぼくの右半面にある目を見れば二重のしわは確認できる。しかし相手から見て左半面にある目のほうは、力を入れて目を開かない限りは目の上の脂肪で二重のしわが隠れていて一重に見える。二重だと認識されやすい目がある右半面のほうは、ほとんどの人に認識されていないということだろう。

 『単純な脳、複雑な私』ではこれを踏まえて、自分を鏡で見る自分の顔と、相手が認識する自分の顔は印象が違うものだと述べている。なぜなら鏡は左右が反転するから、鏡に写る自分の左半面は、多くの人が意識しない右半面にすりかわっている。

 以上から考えるに、おそらく見ていないほうの半面は、脳が勝手に鏡像をつくって補完しているんだろう。というわけで、OpenCVの顔認識を使って、多くの人が見る私、少数の人が見える私を生成してくれるプログラムを書いてみた。顔は左右対称に思えて、実は細部で非対称になっている。多くの人が見る私と、少数の人が見る私では少しばかり印象が違ってくる。
上段:オリジナル
中段:多くの人が見る私(左半面の折り返し)
下段;少数の人が見る私(右半面の折り返し)
13110702.jpg
元画像となるものはAT&T Laboratories Cambridgeの研究用素材を用いた。
http://www.cl.cam.ac.uk/research/dtg/attarchive/facedatabase.html

突貫で書いてリファクタリングもろくにしていないスクリプト。動作はPython 2.7, OpenCV 2.4.6で確認。
#!/usr/bin/env python

import exceptions

import numpy as np
import Image
import cv2
import cv2.cv as cv

help_message = '''
USAGE: facedetect.py [image file]
'''

def detect(img, cascade):
rects = cascade.detectMultiScale(img, scaleFactor=1.3, minNeighbors=4, minSize=(5, 5), flags = cv.CV_HAAR_SCALE_IMAGE)
if len(rects) == 0:
return []
rects[:,2:] += rects[:,:2]
return rects

def draw_rects(img, rects, color):
for x1, y1, x2, y2 in rects:
cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)

def getXCenterByEyes(rects):
if len(rects) != 2:
raise exceptions.ValueError("couldn't detect eyes")
if rects[0][0] < rects[1][0]:
rect1 = rects[0]
rect2 = rects[1]
else:
rect1 = rects[1]
rect2 = rects[0]
x1 = (rect1[2] + rect1[0]) * 0.5
x2 = (rect2[2] + rect2[0]) * 0.5

return int((x2 + x1) * 0.5)

def main():
import sys, getopt
print help_message

filename = sys.argv[1]

cascade_fn = "../../data/haarcascades/haarcascade_frontalface_alt.xml"
nested_fn = "../../data/haarcascades/haarcascade_eye.xml"

face_cascade = cv2.CascadeClassifier(cascade_fn)
eye_cascade = cv2.CascadeClassifier(nested_fn)

try:
image = cv.LoadImageM(filename)
except:
return
img = np.asarray(image)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray)
im = cv.fromarray(img)

rects = detect(gray, face_cascade)
x0, y0, x1, y1 = rects[0]
print rects[0]
cv_face = cv.GetSubRect(im, (x0, y0, x1 - x0, y1 - y0))
im = cv.fromarray(cv_face)

gray = cv2.cvtColor(np.asarray(cv_face), cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray)
subrects = detect(gray, eye_cascade)
print subrects
center = getXCenterByEyes(subrects)

converted_color = cv2.cvtColor(np.asarray(im), cv.CV_BGR2RGB)
original_image = Image.fromstring("RGB", cv.GetSize(cv_face), converted_color.tostring())

for x in xrange(4):
center += 1
left_side = original_image.crop((0, 0, center, im.height))
new_image = Image.new("RGB", (center * 2, im.height))
new_image.paste(left_side, (0, 0, center, new_image.size[1]))
new_image.paste(left_side.transpose(Image.FLIP_LEFT_RIGHT), (center, 0, new_image.size[0], new_image.size[1]))
new_image.save(filename + "_major" + str(x) + ".jpg")


for x in xrange(4):
center -= 1
right_side = original_image.crop((center, 0, im.width, im.height))
new_image = Image.new("RGB", ((im.width - center) * 2, im.height))
new_image.paste(right_side, ((im.width - center), 0, new_image.size[0], new_image.size[1]))
new_image.paste(right_side.transpose(Image.FLIP_LEFT_RIGHT), (0, 0, im.width - center, new_image.size[1]))
new_image.save(filename + "_minor" + str(x) + ".jpg")

if __name__ == '__main__':
main()


画像の加工にOpenCVを使うこともできるが、不慣れならPILを使ったほうが手っ取り早い。
http://opencv.jp/opencv-2svn/py/cookbook.html#getsubrect
スポンサーサイト



OpenCVのhaartrainingに使う、正解画像リストを作るのに役立つ道具を作った

                
tags:
 以前、画像からゾンビの顔を検出するものを作った。このときは技術的なところには全然詳しく触れなかった。
OpenCVを使ってちょっとおふざけをしてみた

 このゾンビの顔検出はOpenCVのhaartrainingというのを使っている。haartrainingとは、要は画像から検出したいターゲットがあるときに、ターゲットを含んだ画像と含んでいない画像を大量に与え、コンピュータに検出器を作らせてしまおうというものだ。最近のデジカメに入っている顔検出機能にhaartrainingは使われていたりするんだろう。詳しくは下記のサイトで学べる。
参考:http://gihyo.jp/dev/feature/01/opencv/0004

 リンク先を見ればわかるがこのhaartrainingをするとき、正解画像リストを作るのが面倒だ。画像を開いてターゲットの座標を確認したら、それをテキストファイルに打ち込んでいかなければならない。なので補助道具をPyGTKで作った。画像ビューアと座標入力の機能を両方とも備えている。ターゲットの座標はキーボードを叩かなくても、マウスのドラッグで入力できる。
120603_2.jpg


 というわけでGUIツールも入れたhaartrainingセットを置いておく。
 作成したツールは解凍後のフォルダ直下にHelpHaar.pyという名前で入れてある。pフォルダにターゲットを含む画像、nフォルダにターゲットを含まない画像を入れること。ツールのGUIにあるfinishボタンを押すとok.txtとng.txtをじぇねれーとする。上書き確認なく実行される点に留意。
https://skydrive.live.com/?cid=14d4b360c466b0c5#cid=14D4B360C466B0C5&id=14D4B360C466B0C5%21135

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import re
import glob

##import pygtk
##pygtk.require('2.0')
import gtk


XML = """<?xml version="1.0"?>
<interface>
<requires lib="gtk+" version="2.16"/>
<!-- interface-naming-policy project-wide -->
<object class="GtkWindow" id="window1">
<property name="default_width">600</property>
<property name="default_height">250</property>
<child>
<object class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<child>
<object class="GtkFrame" id="frame1">
<property name="visible">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="alignment1">
<property name="visible">True</property>
<property name="yalign">0</property>
<property name="left_padding">12</property>
<child>
<object class="GtkEventBox" id="eventbox1">
<property name="visible">True</property>
<child>
<object class="GtkImage" id="image1">
<property name="width_request">500</property>
<property name="height_request">500</property>
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
<property name="stock">gtk-missing-image</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="label" translatable="yes">&lt;b&gt;frame1&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="vbox1">
<property name="width_request">191</property>
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkHBox" id="hbox4">
<property name="visible">True</property>
<child>
<object class="GtkEntry" id="entry1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="entry2">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="frame2">
<property name="visible">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="alignment2">
<property name="visible">True</property>
<property name="left_padding">12</property>
<child>
<object class="GtkTextView" id="textview1">
<property name="height_request">140</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="label" translatable="yes">&lt;b&gt;frame2&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox3">
<property name="visible">True</property>
<child>
<object class="GtkButton" id="button_back">
<property name="label" translatable="yes">&#x2190;</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_forward">
<property name="label" translatable="yes">&#x2192;</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="xalign">0.51999998092651367</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_finish">
<property name="label" translatable="yes">finish</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="fill">False</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>"""

class helpHaarTraining:
def __init__(self):
self.builder = gtk.Builder()
#sample.gladeファイルから画面を作成
self.builder.add_from_string(XML)
## self.builder.add_from_file(r'haar.glade')

#オブジェクトを取得、マウスイベントとコネクト
self.window = self.builder.get_object("window1")
self.window.connect("destroy", lambda w: gtk.main_quit())
self.window.show()
self.image = self.builder.get_object("image1")
self.ebox = self.builder.get_object("eventbox1")
self.ebox.connect("button-press-event", self.start_point)
self.ebox.connect("button-release-event", self.end_point)
self.button_back = self.builder.get_object("button_back")
self.button_back.connect("clicked", self.change_picture, -1)
self.button_forward = self.builder.get_object("button_forward")
self.button_forward.connect("clicked", self.change_picture, 1)
self.button_finish = self.builder.get_object("button_finish")
self.button_finish.connect("clicked", self.finish, -1)
self.textview = self.builder.get_object("textview1")
self.tbuffer=gtk.TextBuffer()
self.textview.set_buffer(self.tbuffer)

#初期設定
self.num_present_image = 0
self.change_picture(self.window ,0)
self.textdata = ""

#描画
self.window.show_all()

def store_haar_area(self):
"""画像ファイル名と目標物の座標をself.textdataにストア
"""
text = self.tbuffer.get_text(self.tbuffer.get_start_iter(),
self.tbuffer.get_end_iter())
li_rect = text.replace(',', '').replace('(', '').replace(')', '').split('\n')
li_rect = [nums for nums in li_rect if nums]
text_rect = "%s %s %s\n" %(self.img_filename, len(li_rect), " ".join(li_rect))
self.textdata += text_rect
print(text_rect)
self.tbuffer.set_text("")

def change_picture(self, widget, num):
"""表示画像を切り替える
"""
if self.tbuffer.get_text(self.tbuffer.get_start_iter(),
self.tbuffer.get_end_iter()):
self.store_haar_area()
images = glob.glob("p/*.png") + glob.glob("p/*.jpg")
if num == -1 and self.num_present_image == 0:
num_next_image = len(images) - 1
elif num == 1 and self.num_present_image == len(images) - 1:
num_next_image = 0
else:
num_next_image = self.num_present_image + num
filename = images[num_next_image]
pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
width = pixbuf.get_width()
height = pixbuf.get_height()
chouhen = width if width >= height else height
scale = 500.0 / chouhen
print width * scale
scaled_buf = pixbuf.scale_simple(int(width * scale), int(height * scale), gtk.gdk.INTERP_BILINEAR)
self.image.set_from_pixbuf(scaled_buf)
self.imgscale = scale
self.img_filename = filename
self.num_present_image = num_next_image
self.window.show_all()

def start_point(self, widget, event):
"""目標物を含む矩形領域の左上の座標を得る
"""
x, y, state = event.window.get_pointer()
x = int(x / self.imgscale)
y = int(y / self.imgscale)
self.x1 = x
self.y1 = y
text = self.tbuffer.get_text(self.tbuffer.get_start_iter(),
self.tbuffer.get_end_iter())
self.tbuffer.set_text(text + "(%s, %s), " %(x-1, y-1))

def end_point(self, widget, event):
"""目標物を含む矩形領域の右下の座標を得る
"""
x, y, state = event.window.get_pointer()
x = int(x / self.imgscale) - self.x1
y = int(y / self.imgscale) - self.y1
text = self.tbuffer.get_text(self.tbuffer.get_start_iter(),
self.tbuffer.get_end_iter())
self.tbuffer.set_text(text + "(%s, %s)\n" %(x-1, y-1))

def finish(self, widget, event):
"""ok.txtとng.txtを作る
"""
if self.tbuffer.get_text(self.tbuffer.get_start_iter(),
self.tbuffer.get_end_iter()):
self.store_haar_area()

images = glob.glob("p/*.png") + glob.glob("p/*.jpg")
textdata = self.textdata
for filename in images:
if not (filename in textdata):
pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
width = pixbuf.get_width() - 1
height = pixbuf.get_height() - 1
textdata += "%s 1 0 0 %s %s\n" %(filename, width, height)
with open("ok.txt", "w") as f:
f.write(textdata)

textdata = ""
images = glob.glob("n/*.png") + glob.glob("n/*.jpg")
for filename in images:
if not (filename in textdata):
textdata += "%s\n" %(filename)
with open("ng.txt", "w") as f:
f.write(textdata)

def main():
gtk.main()
return 0

if __name__ == "__main__":
helpHaarTraining()
main()
プロフィール

h

Author:h

最新記事
リンク
作ったものなど
月別アーカイブ
カテゴリ
タグリスト

検索フォーム
Amazon