恋のキューピッド
From Usipedia
大学二年次のクリスマスシーズンにとある団体がサークル同士の交流を目的としてクリスマスパーティーを開催しました.そのパーティーでは主に音楽系サークルが合唱・演奏を披露したのですが,他のサークルでもクリスマスに関係した作品なら展示出来る事が分かりました.そこで,部長が私に「何かクリスマスっぽいもの作って」と無茶ぶりして出来たのがこのプログラムです.展示の前を通りかかったカップルを応援します.
Contents |
概要
- 2つの顔が隣り合っていたらハートマークで囲います
- 相手に対して内心思っている事が吹き出しで表示されます
- ネガティブな事でもランダムに表示するので,目上の人とは一緒に写らないようにして下さい
- お一人様でも「目隠し」や「サンタの帽子」を楽しめます
- 画像を保存して好きなメールアドレスにメールを送れます
操作方法
d | デバッグモードを切り替える |
a | サンタの帽子を表示するかどうか切り替える |
m | 目隠しを表示するかどうか切り替える |
h | ハートを表示するかどうか切り替える |
b | 吹き出しを表示するかどうか切り替える |
s | 現在の画像を保存する |
q | 終了 |
OpenCVで透過画像を扱う
OpenCVのcvLoadImageはアルファチャンネルの読み込みに対応していません. 透過させる色を事前に決め(#000000など),1pxずつ描画する時にその色を無視することで透過を表現します. これだと#000000を使えなくなるので,画像では代わりに近い色(#010101)を使います. 詳しくは次のソースコード中の drawImage をご覧下さい.
ソースコード
Xcodeのプロジェクト名は「CoupleKiller」です.
次の素材が必要です.
- ハートの画像(heart.png)
- サンタクロースの帽子の画像(hat.png)
- 表示する吹き出しの画像(balloon0.png - balloon15.png)
- 人の顔(正面)を識別する分類器(haarcascade_frontalface_default.xml,標準サンプルに付属)
main.cpp
#include <iostream> #include <cstdlib> #include <cmath> #include <ctime> #include <string> #include <vector> #include <sstream> using namespace std; #include <opencv2/objdetect/objdetect.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #define WINDOW_TITLE "CoupleKiller" #define WINDOW_WIDTH 640 #define WINDOW_HEIGHT 480 #define IMG_SCALE 2 // Webカメラからの入力そのままだと大きすぎるので WINDOW_WIDTH/IMG_SCALE x WINDOW_HEIGHT/IMG_SCALE のように小さくして扱う 1だと重い,4だとちょっと離れたら認識しなくなる #define BALLOON_SCALE 0.8 // 顔に対する吹き出しの大きさ #define BALLOON_N 16 // 用意した吹出しの数 #define NEAR_THRESHOLD 32 // [px] 奥行き(width)とyがこれ以上ずれていたらペアとして扱わない using namespace cv; // 分類器や素材は他のプロジェクトからも使うので分離 // rootPathとrootPathを参照してる部分は要修正 String rootPath = "/Users/usi3/Desktop/CoupleKiller/"; String cascadeName = rootPath+"haarcascade_frontalface_default.xml"; CvCapture* capture = 0; Mat frame, frameCopy, image; CascadeClassifier cascade; IplImage *heartImage; IplImage *hatImage; IplImage *balloonImage[BALLOON_N]; long int heartDisappearTime = 999; // ハートが非表示だった時間 int balloonAid, balloonBid; // 吹き出しA, Bの画像id bool debugMode = false, balloonMode = true, heartMode = true, blindfoldMode = false, hatMode = false; const static Scalar CVCOLORS[] = { CV_RGB(0,0,255), CV_RGB(0,128,255), CV_RGB(0,255,255), CV_RGB(0,255,0), CV_RGB(255,128,0), CV_RGB(255,255,0), CV_RGB(255,0,0), CV_RGB(255,0,255), CV_RGB(0,0,0), } ; // canvasの(x, y)にimageをwidth x heightにresizeしてから直接描く // 参考:http://opencv.jp/sample/basic_structures.html // cvLoadImageはアルファチャンネルの読み込みに対応していないので, // r=0, g=0, b=0の場合透過させる(黒を表現したい場合はr=1, g=1, b=1などにする) void drawImage(Mat& canvas, IplImage* image, int x=0, int y=0, int width=100, int height=100){ Mat resized(width, height, CV_8UC1); resize(image, resized, Size(width, height)); const int i_min = x<0 ? -x : 0; // if(x+i<0) continue; if(y+j<0) continue; はスマートじゃない const int j_min = y<0 ? -y : 0; // 描画対象が左上方向にはみ出てる場合 const int i_max = ((x + resized.cols ) > canvas.cols ) ? canvas.cols - x : resized.cols; // 右下方向にはみ出てる場合 const int j_max = ((y + resized.rows) > canvas.rows) ? canvas.rows - y : resized.rows; for (int j = j_min; j < j_max; ++j){ for (int i = i_min; i < i_max; ++i){ unsigned long resizedBase = resized.step * j + i * 3; int r = resized.data[resizedBase + 0]; int g = resized.data[resizedBase + 1]; int b = resized.data[resizedBase + 2]; if (r || g || b) { unsigned long canvasBase = canvas.step * (y+j) + (x+i) * 3; canvas.data[canvasBase + 0] = r; canvas.data[canvasBase + 1] = g; canvas.data[canvasBase + 2] = b; } } } resized.release(); } void drawHeart(Mat& img, Rect *a, Rect *b){ int x = a->x < b->x ? a->x : b->x; int y = a->y < b->y ? a->y : b->y; int rightx = a->x+a->width < b->x+b->width ? b->x+b->width : a->x+a->width; int bottomy = a->y+a->height < b->y+b->height ? b->y+b->height : a->y+a->height; // heart padding // 写っているペアの大きさに応じてハートのpaddingを変更する int delta = 20+10*a->width*IMG_SCALE/WINDOW_WIDTH; x -= delta; y -= 2*delta; rightx += delta; bottomy += 3*delta; if(debugMode){ Rect heartRect(x, y, rightx-x, bottomy-y); //rectMul(&heartRect, IMG_SCALE); rectangle(img, heartRect, CVCOLORS[4]); } drawImage(img, heartImage, x, y, rightx-x, bottomy-y); } void drawBalloon(Mat& img, int id, int x=0, int y=0, int width=100, int height=100){ if(debugMode){ rectangle(img, Rect(x, y, width, height), CVCOLORS[5]); } drawImage(img, balloonImage[id], x, y, width, height); // } // Rectの各要素をn倍する // 入力画像をIMG_SCALEに応じて小さくすると色々と小さくなるのでこれで修正する void rectMul(Rect *r, int n){ r->x *= n; r->y *= n; r->width *= n; r->height *= n; } // ベースは標準サンプルの facedetect.cpp void detectAndDraw(Mat& img, CascadeClassifier& cascade){ vector<Rect> faces; // CV_8UC1: 1チャネル8ビットunsigned char Mat gray, smallImg(cvRound(img.rows/IMG_SCALE), cvRound(img.cols/IMG_SCALE), CV_8UC1); // 色空間をカラーからグレースケールへ変換 cvtColor(img, gray, CV_BGR2GRAY); // IMG_SCALEに応じて小さくする resize(gray, smallImg, smallImg.size(), 0, 0, INTER_LINEAR); equalizeHist(smallImg, smallImg); double t = (double)cvGetTickCount(); cascade.detectMultiScale( smallImg, faces, 1.1, 2, 0 //|CV_HAAR_FIND_BIGGEST_OBJECT //|CV_HAAR_DO_ROUGH_SEARCH |CV_HAAR_SCALE_IMAGE , Size(30, 30) ); t = (double)cvGetTickCount() - t; //printf("detection time = %g ms\n", t/((double)cvGetTickFrequency()*1000.)); int i = 0; for(vector<Rect>::const_iterator r=faces.begin(); r!=faces.end(); r++, i++){ Scalar color = CVCOLORS[i%8]; Point center; center.x = cvRound((r->x + r->width*0.5)*IMG_SCALE); center.y = cvRound((r->y + r->height*0.5)*IMG_SCALE); //cout << center.x << " " << center.y << endl; // check positions rectMul((Rect*)(&(*r)), IMG_SCALE); // 以降facesはIMG_SCALEに応じて拡大済み if(debugMode){ rectangle(img, *r, color); } if (blindfoldMode) { rectangle(img, Point(r->x, r->y+r->height*0.2), Point(r->x+r->width, r->y+r->height*0.5), CVCOLORS[8], CV_FILLED); } if (hatMode) { int height = r->width*1.2*1.35889; drawImage(img, hatImage, r->x-0.1*r->width, r->y-height*0.9, r->width*1.2, height); } } // 最も近い顔のペアを探す // 左の顔・右の顔 を厳密に区別する(顔と吹き出しを1対1対応させるため) int mind = 640; Rect paira, pairb; for(vector<Rect>::const_iterator b=faces.begin(); b!=faces.end(); b++){ for(vector<Rect>::const_iterator a=faces.begin(); a!=faces.end(); a++){ if(a != b){ if(abs(b->y + 0.5*b->height - a->y - 0.5*a->height) <= NEAR_THRESHOLD && abs(b->width - a->width) <= NEAR_THRESHOLD){ int d = abs(b->x + b->width - a->x - a->width); if(d < mind){ mind = d; if (a->x < b->x) { paira = *a; pairb = *b; }else{ paira = *b; pairb = *a; } } } } } } // 顔は認識された次の瞬間には認識されなくなるかもしれない // 認識するたびに吹き出しを変えると,吹き出しがすごい勢いで変わることがあるので, // 認識できなくなった時間が一定以上の場合のみ吹き出しを更新する if(mind != 640){ if(heartDisappearTime > 60){ balloonAid = rand()%16; balloonBid = rand()%16; heartDisappearTime = 0; } //printf("OK "); if (heartMode) { drawHeart(img, &paira, &pairb); } if (balloonMode) { drawBalloon(img, balloonAid, paira.x+0.5*(1-BALLOON_SCALE)*paira.width, paira.y-BALLOON_SCALE*paira.width, BALLOON_SCALE*paira.width, BALLOON_SCALE*paira.width); drawBalloon(img, balloonBid, pairb.x+0.5*(1-BALLOON_SCALE)*pairb.width, pairb.y-BALLOON_SCALE*pairb.width, BALLOON_SCALE*pairb.width, BALLOON_SCALE*pairb.width); } }else{ heartDisappearTime++; } imshow(WINDOW_TITLE, img); } // Matを画像ファイルに書き出し void saveImage(Mat& img){ time_t timer = time(NULL); struct tm *date = localtime(&timer);; char cstr[256]; strftime(cstr, 255, "%Y%m%d%H%M%S", date); imwrite(rootPath+"log/"+String(cstr)+".png", Mat(img)); } // 初期化 void cvInit(){ if(!cascade.load(cascadeName)){ cerr << "ERROR: Could not load classifier cascade" << endl; return; } capture = cvCaptureFromCAM(1); cvSetCaptureProperty(capture, CV_CAP_PROP_FRAME_WIDTH, WINDOW_WIDTH); cvSetCaptureProperty(capture, CV_CAP_PROP_FRAME_HEIGHT, WINDOW_HEIGHT); if(!capture){ cerr << "Capture from CAM didn't work" << endl; return; } cvNamedWindow(WINDOW_TITLE, 1); // 画像読み込み // widthStepを固定するためにあえて古いcvLoadImageで読み込む // 参考:http://imagingsolution.blog107.fc2.com/blog-entry-178.html heartImage = cvLoadImage(String(rootPath+"heart.png").c_str(), CV_LOAD_IMAGE_COLOR); hatImage = cvLoadImage(String(rootPath+"hat.png").c_str(), CV_LOAD_IMAGE_COLOR); for(int i=0; i<BALLOON_N; i++){ stringstream ss; ss << rootPath << "balloon" << i << ".png"; balloonImage[i] = cvLoadImage(ss.str().c_str(), CV_LOAD_IMAGE_COLOR); } } int main(int argc, char *argv[]){ srand(time(NULL)); cvInit(); for(;;){ IplImage* iplImg = cvQueryFrame(capture); frame = iplImg; if(frame.empty()){ return; } frame.copyTo(frameCopy); detectAndDraw(frameCopy, cascade); int key = waitKey(10); switch (key) { case 'd': debugMode = debugMode ? false : true; break; case 'a': hatMode = hatMode ? false : true; break; case 'm': blindfoldMode = blindfoldMode ? false : true; break; case 'h': heartMode = heartMode ? false : true; break; case 'b': balloonMode = balloonMode ? false : true; break; case 's': saveImage(frameCopy); break; case 'q': cvReleaseCapture( &capture ); return 0; break; } } return 0; }
sendmail.rb
CoupleKillerではsキーを押した時に画像をlog/以下に保存します. このRubyスクリプトはlog/以下の最新の画像ファイルを,コマンドライン引数で指定されたメールアドレスにGmail経由で送信します.
変数messageの個人情報は伏字にしました.
# -*- coding: euc-jp -*- require 'net/smtp' require 'rubygems' require 'tmail' require 'tlsmail' require 'kconv' require 'openssl' require 'base64' # 日本語メールクラス # http://trivia.cocolog-nifty.com/blog/2008/11/ruby-d30b.html # の JMail クラスを Gmail に特化して変更 class Gmail # 初期化処理 def initialize() @mail = TMail::Mail.new @mail.mime_version = '1.0' end # アカウントの設定 def set_account(from_address) @mail.from = from_address @mail.reply_to = from_address end # パスワードの設定 def set_password(password) @password = password end # 宛先を設定する。 def set_to(to_addresses) @mail.to = to_addresses end # タイトルを設定する。 def set_subject(subject) work = Kconv.tojis(subject).split(//,1).pack('m').chomp @mail.subject = "=?ISO-2022-JP?B?"+work.gsub('\n', '')+"?=" end # 本文を設定する。 def set_text(text) main_text = TMail::Mail.new main_text.set_content_type 'text', 'plain', {'charset'=>'iso-2022-jp'} main_text.body = Kconv.tojis(text) @mail.parts.push main_text end # メールを送信する。 def send() @mail.date = Time.now @mail.write_back smtpserver = Net::SMTP.new('smtp.gmail.com', 587) smtpserver.enable_tls(OpenSSL::SSL::VERIFY_NONE) smtpserver.start('gmail.com', @mail.from[0], @password, :login) do |smtp| smtp.sendmail(@mail.encoded, @mail.from, @mail.to) end end # ファイルを添付する。 def set_attach(file_path) attach = TMail::Mail.new %r|(^.*)/(.+$)|.match(file_path) file_path, file_name = $1, $2 tmp_file_path = File.expand_path(file_name,file_path) attach.body = Base64.encode64 File.read(tmp_file_path) attach.set_content_type 'application','zip','name'=>file_name attach.set_content_disposition 'attachment','filename'=>file_name attach.transfer_encoding = 'base64' @mail.parts.push attach end end # 最新の画像ファイルを探す fileName = "" Dir.entries("./").sort.reverse.each do |name| if /^\d/ =~ name fileName = name break end end message = <<"EOS" ※このメールは添付ファイル(#{fileName})に写っている方が指定したメールアドレスに自動送信したものです この度はA主催のB Party 2011にて「恋のキューピッド」を遊んで頂きありがとうございました. 添付ファイルの #{fileName} が撮影した画像です.お確かめ下さい. ---- C大学 マイクロコンピュータクラブ URL1 MailAddress1 C大学 工学部 情報工学科 2年 Name1 解説ページ: http://usi3.com/Cupid EOS gmail = Gmail.new() gmail.set_account('youraccount@gmail.com') gmail.set_password('yourpassword') gmail.set_to(ARGV) gmail.set_subject('B Party 2011 サークル展示「恋のキューピッド」') gmail.set_text(message) gmail.set_attach("./"+fileName) gmail.send