恋のキューピッド

From Usipedia
Jump to: navigation, search
2人の政治家を並べて写した時の様子.(※日米関係を揶揄する意図は全くありません.公人なら肖像権的に扱いやすいので使っているだけです.)

大学二年次のクリスマスシーズンにとある団体がサークル同士の交流を目的としてクリスマスパーティーを開催しました.そのパーティーでは主に音楽系サークルが合唱・演奏を披露したのですが,他のサークルでもクリスマスに関係した作品なら展示出来る事が分かりました.そこで,部長が私に「何かクリスマスっぽいもの作って」と無茶ぶりして出来たのがこのプログラムです.展示の前を通りかかったカップルを応援します.

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
Namespaces
Variants
Views
Actions
Categories