2012年11月4日日曜日

iPhoneでOpenCVを使う

久しぶりに投稿します。いろいろと書けるネタがいろいろ溜まっているのに書けていないですねー。 実はOpenCVがオフィシャルでiOSに対応しているというのに、それに対応した日本語の記事がなかなかないので、マシンパワーあるモバイル端末での画像処理の研究とかが捗ってないとかあったら悲しいので、ビルドの方法とか書きます。

 といっても、この記事の日本語訳でしかないんですが。

コマンドラインとか怖くて使えないという人をある程度想定して書いています。
後、iOSプログラミングするんだから,Mac使っててXcode入ってないと出来ないです。

1. ソースコードのダウンロード

ターミナルを開いて、以下のコマンドを打ちます。

cd ~/ ##どこか適当なディレクトリ.ソースコードをDLしてもいい所
git clone https://github.com/Itseez/opencv.git ## githubからソースコードをDL。git入ってない場合はmacportsあたりでDL

ここまでで、きっとソースコードのDLが終わっているはず

2. ソースコードのコンパイル


cd /
sudo ln -s /Applications/Xcode.app/Contents/Developer Developer

Developerにシンボリックリンクを張ります。


cd ~/## ソースコードをDLしたのと同じディレクトリに戻る
python opencv/ios/build_framework.py ios ## ios用のframeworkをビルド。結構時間かかった

3. Frameworksの追加

外部ライブラリを追加するときの手順と同じように、frameworksを追加します。




今までみたいにCMakeしてビルドしてっていうのをしなくていいので、だいぶとっかかりがしやすくなったと思います。

2012年7月22日日曜日

Blockを使ったNSArrayのソート

NSArrayのソートって、

- (NSArray *)sortedArrayUsingDescriptors:(NSArray *)sortDescriptors
とかを使ってしかソートできないのかと思ったらBlocks使ってソートできるみたい。

このメソッド
- (NSArray *)sortedArrayUsingComparator:(NSComparator)cmptr

使い方


// arrayにはNSDate型のオブジェクトが入っているとする
    NSArray *sorted = [array sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        return [(NSDate *)obj1 earlierDate:(NSDate*)obj2] == (NSDate*)obj1;
    }];
これで日付の降順にソートされたNSArrayを取得できます。
うだうだ別メソッドに書き出さなくてすむので、便利ですね!

2012年7月20日金曜日

パノラマ合成

パノラマ合成のアルゴリズムを実装してみましたので公開します。

環境
C++, OpenCV2.4

ソースコード
https://gist.github.com/3135917

使い方
入力として、画像ファイルを入れると,panorama.pngとしてパノラマ画像が出力されます。
画像サイズとか高さとかは割と適当なので、時間を見つけて直して行きます。
ToDo

  • カラー対応
  • サイズをきっちり揃える

コンパイル方法
MacPortsでOpenCVをインストールしたと仮定した後の話
g++ panorama_stitching.cpp `pkg-config --cflags opencv` `pkg-config --libs opencv` -lm
でコンパイルができます。多分


アルゴリズムの概説

  1. 各入力画像に対してSURFで特徴点を抽出
  2. 隣り合う画像(入力の前後)で対応点マッチング
  3. 前後の画像から、変換行列を最小二乗法で推定。
    座標系は円筒座標を仮定し、全ての写真は同じ奥行きにあると仮定
  4. 変換行列を逐次乗じながら、それぞれの画像を結果画像にマッピングして行く
注意)
入力するときは隣り合う画像を入力してください。連続する画像で対応点が取れないと破綻します。
変換行列の全体最適は行っていないので、少しずつ計算誤差が累積します。





2012年6月17日日曜日

OpenCV 2.4 で SURF が使えない

OpenCV 2.4 (c++版)でSURFを使おうとするとコンパイルが通りません。


こんな感じのコード

#include <iostream>
#include <string>

#include <cv.h>
#include <highgui.h>

using namespace std;
using namespace cv;

int main( int argc, char* argv[] ){
  if( argc < 1 ) return 1;
  
  Mat colorImage = imread( argv[1], 1);
  if( colorImage.empty() ) return -1;

  Mat grayImage;
  cvtColor( colorImage, grayImage, CV_BGR2GRAY );

  SURF calc_surf = SURF(500,4,2,true);

  vector<keypoint> kp_vec;
  vector<float> desc_vec;
  calc_surf( grayImage, Mat(), kp_vec, desc_vec);

  cout << "Image keypoints " << kp_vec.size() << endl;
  return 0;
}


これでコンパイルすると、


~/Development/cpp-study# g++ study.cpp `pkg-config --cflags opencv` `pkg-config --libs opencv`
study.cpp: In function ‘int main(int, char**)’:
study.cpp:19: error: ‘SURF’ was not declared in this scope
study.cpp:19: error: expected `;' before ‘calc_surf’
study.cpp:23: error: ‘calc_surf’ was not declared in this scope

な感じで怒られます。
どうやら、ヘッダファイルにSURFがインクルードされていないみたいです。
SURFが定義されているのが、nonfree/nonfree.hpp なのですが、インクルードしたcv.h
を見てみると、nonfree.hppがインクルードされていません

なのでcv.hを以下のように変更します

/*M///////////////////////////////////////////////////////////////////////////////////////
//
//  IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
//
//  By downloading, copying, installing or using the software you agree to this license.
//  If you do not agree to this license, do not download, install,
//  copy or use the software.

~~~中略~~~~
#include "opencv2/core/core_c.h"
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc_c.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/video/tracking.hpp"
#include "opencv2/features2d/features2d.hpp"
#include "opencv2/flann/flann.hpp"
#include "opencv2/calib3d/calib3d.hpp"
#include "opencv2/objdetect/objdetect.hpp"
#include "opencv2/legacy/compat.hpp"
#include "opencv2/nonfree/nonfree.hpp" // <-- ここを追加
~~~以下略~~~

多分これでうまく行くと思います。
nonfreeっていう名前から、ただでは使えないみたいですね。

2012年6月5日火曜日

Instagramの画像処理3 -色味変換-

前々回のポスト のトーンカーブによる画像変換の続きで、トーンカーブを求めます。

トーンカーブなので、ガンマ曲線をフィッティングしてもいいんですが、

  • そこまで厳密に求める必要がない
  • 8bit 256階調なので、その入出力をマッピングしても事足りる

ということなので、ある入力値に対して、いくら出力するか、というデータをテーブルで保持しておくルックアップテーブル(LUT)を求めます。

求め方

黒から白へとなだらかに変化する画像を入力画像とし、この画像にInstagramで変換を行ったものを出力画像とする。
入出力画像の各ピクセルの入力と、出力の関係をテーブルに保持して行きます。
同じ入力輝度値の画素がいくつか出てきますが、平均化してならしました。

入力画像

出力画像


実行結果

instagramの結果

LUTの出力結果


なかなかいい感じですね。
この辺のデータはまとまってgithubにあがってます

https://github.com/ginrou/OpenCV-Sample/blob/master/src/instagram_hack.c
computeLookUpTableという関数が、このLUTを求める関数です。


次は

  • チルトによるぼけ効果
  • ケラレ

あたりをキャリブレーションするかもしれません。


前回の補足

色変換行列を、ランダムカラーの画像を使って推定してみましたが、あまりうまく行きませんでした。
推定に使った画像
Instagramへの入力
Instagramで変換
色変換行列で変換

色味が変わってるのは分かりますが、じゃあちゃんと変わってるかと言うと、そういうわけではないです。




2012年6月3日日曜日

Instagramの画像処理2

色変換行列で試してみよう。



色変換行列の推定

色変換行列が
[r g b 1 ]' = A [r g b 1]
のような4×4の行列で表現されているとして、この変換行列を推定しました。
変換前後の画像から、最小二乗法を用いてパラメータを推定しました。
変換前
変換後

推定した行列はこんな感じです。

0.373982 0.373982 0.373982 -1.927580
0.376430 0.376430 0.376430 0.711464
0.372265 0.372265 0.372265 13.394824
0.000000 0.000000 0.000000 1.000000

この変換行列でやってみたのがこんな感じ
入力
この行列で変換
本来はこうなるはず
それっぽいと言えばそれっぽいけど。。。
っていうか、白黒画像だとうまいこと変換行列を推定できないですよね。
もう少し入力データをいじって実験してみます。

2012年5月30日水曜日

Instagramの画像処理

おそらくトーンカーブいじりながら、線形変換してるんだろうなーという予想をたててる。
とりあえず、

  • 0-255の間で変化する画像
  • 0-255のランダム画像
を使ってとりあえずデータを取ってみた

やり方

  1. 上で書いた画像を作る
  2. Instagramで変換(フィルタはBrannan)
  3. パソコンに持ってくる
  4. 入力を作った画像、出力を変換した画像としてプロット

入出力画像

ランダム-入力

ランダム-出力

グレー出力

グレー入力
画像の端の方が暗くなる現象(ケラレ)があるけど、今回は無視して色変換のみで

プロット出力










所感

ひとまず、同じような変換をしているみたいで一安心。中身を解釈して変換を変えてたらなかなかに面倒だったなと思う。
トーンカーブがあるように見えるけど、線形というように言われてもそうだと思えるしなー。
次は色変換行列を作ってみて、調べてみます。



2012年4月26日木曜日

ウィナーデコンボリューション

ウィナーデコンボリューションとは、ぼけてしまった画像をキレイな、シャープな画像に復元する画像処理アルゴリズムです。

こんな画像が

こうなります


画像の中の細かい部分(髪の毛とか目の周りとか)が復元されているのが見ていただけるでしょうか?

ソースコードはgistにアップロードしてあります。
べた書きのCとOpenCVを用いています。
https://gist.github.com/2424856

ご自由にお使いください。

ぼけのモデル

画像にあらわれる、ぼけてつぶれてしまう現象は一般に、ぼけのない画像とぼけ方の畳み込みで表現されます。

ぼけのモデル

畳み込みは、ぼけた画像の1画素がぼけてない画像の何画素かとぼけ方を掛けて足し合わせていく処理のことです
畳み込み
ここでの問題は、ぼけを含んだ画像と、どのようにぼけたかという情報を用いて、ぼけのない画像を逆演算で求めることです。



フーリエ変換

この畳み込みは、逆演算をストレートに求めることが難しいです。
しかし、畳み込みはフーリエ変換を用いることで乗算に変換できるので逆演算が簡単になります。




アルゴリズム


  1. ぼけを含んだ画像 y とぼけ方 f を入力として受け取る
  2. yとfをフーリエ変換
  3. ぼけのない画像をフーリエ変換領域で求める
  4. 以下の数式に従って計算
  5. 逆フーリエ変換
で作ることができます。


結果

結果は上の通りです。実行時間ははかってませんが、10秒はかからず、1〜2秒くらいだと思う。

before
after
 画像の縁の方に縞模様ができていますが、これはリンギングという現象で、
離散フーリエ変換が周期関数を仮定しているんですが、実際の画像では周期関数でないために上式に従わないために発生します。
窓関数をかけると、抑制することができます。





2012年2月19日日曜日

TwitterFrameworkの利用


4 - Twitter Frameworkの利用

iOS5になってから、iOS上で簡単にTwitterのAPIを利用することができるようになりました。
OAuth認証をしなくていいので、とても便利です。

そこで今回はこのTwitterFrameworkを利用して自分のタイムラインを取得したり、投稿してみたりします。

準備しておくもの:Twitterのアカウント

今回の流れ
  • プロジェクトの準備
  • タイムラインの取得
  • タイムラインをUITableViewに表示
  • Twitterへ投稿
です。
ちなみに次回は、この辺をこまごまと改良して、よりそれっぽいクライアントを作っていく予定です。



プロジェクトの準備
それではまず、プロジェクトの準備です。
いつも通り、NewProjectから進み、今回はMaster-Detail Applicationをテンプレートとして選択します。
Device FamilyはiPhoneを
Use Automatic Reference Countingを今回はオンにしましょう。

プロジェクトの設定画面が出て来たら、次はフレームワークを追加します。
前回と同様にLinked Frameworks and Librariesから
  • AccountFramework
  • TwitterFramework
の二つを追加します。

多分こんな画面になってると思います。
これでプロジェクトの準備は完了です。

TwitterFrameworkはiOSでTwitterを使うときに用いるライブラリ、
AccountFrameworkは、iOSがTwitterのアカウントとかNotification Centerの制御をしてますが、それを一元管理・利用するFrameworkです。多分。

MasterViewController.hでクラス変数の追加などを行います。
まず、二つのヘッダファイル
  • Accounts/Accounts.h
  • Twitter/Twitter/h
を追加します。




今回のクラス変数は
  • つぶやきのユーザー名とその内容を保存しておくための配列、
  • アカウントに関する情報にまつわるもの
を追加します。
さらに、クラスメソッドとして、タイムラインを読み込んで配列にデータを追加するメソッド loadTimeline を追加します。

MasterViewController.h
は次のようになります。



タイムラインの取得
それでは実際にTwitterのAPIを叩いてタイムラインを取得しましょう。
viewDidLoadメソッドを以下のように書き換えます。
- (void)viewDidLoad
{
    [super viewDidLoad];

accountStore = [[ACAccountStore alloc] init];
accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
userNameArray = [[NSMutableArray alloc] initWithCapacity:0];
tweetTextArray = [[NSMutableArray alloc] initWithCapacity:0];
[self loadTimeline];
}


ここではクラス変数の初期化を行っています。本来はinitメソッドでやるべきだとは思います。
その後に、loadTimelineでタイムラインを読みに行っています。


ではそのloadTimelineを実装しましょう


- (void)loadTimeline{
 
 [accountStore requestAccessToAccountsWithType:accountType
       withCompletionHandler:^(BOOL granted, NSError *error) {
        if (granted) {
         
         if (account == nil) {
          NSArray *accountArray = [accountStore accountsWithAccountType:accountType];
          account = [accountArray objectAtIndex:0];
         }
        
         if (account != nil) {
          NSURL *url = [NSURL URLWithString:@"http://api.twitter.com/1/statuses/home_timeline.json"];
          NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
          [params setObject:@"20" forKey:@"count"];
          [params setObject:@"1" forKey:@"include_entities"];
          [params setObject:@"1" forKey:@"include_rts"];
          
          TWRequest *request = [[TWRequest alloc] initWithURL:url
                     parameters:params
                     requestMethod:TWRequestMethodGET];
          [request setAccount:account];
          [request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
           
           if (responseData) {
            NSError *jsonError;
            NSArray *timeline = [NSJSONSerialization JSONObjectWithData:responseData
                         options:NSJSONReadingMutableLeaves error:&jsonError];
            NSLog(@"%@", timeline);
           }
           
          }];
          
          
         }
         
         
        }
       }];
 
}


長いし、変な形してるしで大変ですが、
まずは

[accountStore requestAccessToAccountsWithType:accountType
withCompletionHandler:^(BOOL granted, NSError *error) {
     /*   コード  */
}];

では、アカウントストアの機能を使ってTwitterへリクエストを実行できるように認証を行ってます。
認証で、実行するのに時間を要するので、ここで処理を止めると他の機能へ支障が出てしまうかもしれません。
なので、ここではメソッドを実行して認証待ちを行い、その結果を受けた処理は別スレッドで行います。
その処理内容が 
withCompletionHandler:^(BOOL granted, NSError *error) {
/*   コード  */
}];
の部分です。


if (account == nil) {
NSArray *accountArray = [accountStore accountsWithAccountType:accountType];
account = [accountArray objectAtIndex:0];
}

この部分は利用するアカウントを取得しています。
accountArrayには、利用可能なアカウントが配列として格納されていて、今回はその中の一つ目のアカウントを利用します。

APIコールを行うURLとそのパラメータを設定します。
NSURL *url = [NSURL URLWithString:@"http://api.twitter.com/1/statuses/home_timeline.json"];
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
[params setObject:@"20" forKey:@"count"];
[params setObject:@"1" forKey:@"include_entities"];
[params setObject:@"1" forKey:@"include_rts"];
TWRequest *request = [[TWRequest alloc] initWithURL:url
  parameters:params
requestMethod:TWRequestMethodGET];
[request setAccount:account];


WebアプリケーションのAPIはHTTPを利用して情報の投稿と取得を行います。
そのリクエストをHTTPリクエストと呼びます。HTTPリクエストにはいくつかの種類やパラメータがありますが、
基本的にはurlとメソッドを定義してリクエストを投げます。
今回用いるURLは
http://api.twitter.com/1/statuses/home_timeline.json
です。
このURLにブラウザでアクセスしても多分アクセスできないと思います。
今回はURLで自分のタイムラインを取得するようにしています。

次にパラメータを設定します。ここで用いているNSMutableDictionary型は
いわゆるハッシュや連想配列とよばれているもので、キーを指定してオブジェクトを追加して行きます。


がその追加の例です。キーのcountはタイムラインで取得する件数のことで、今回は20件としています。
他の条件については
で確認できます。

TWRequest *request = [[TWRequest alloc] initWithURL:url
  parameters:params
requestMethod:TWRequestMethodGET];

では、リクエストを生成しています。
さらにその後でアカウントをセットしてリクエストの生成は完了です。


[request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if (responseData) {
NSError *jsonError;
NSArray *timeline = [NSJSONSerialization JSONObjectWithData:responseData
options:NSJSONReadingMutableLeaves error:&jsonError];
NSLog(@"%@", timeline);
}
}];

でリクエストを実行し、タイムラインを取得します。
perfomeRequestWithHandlerメソッドで、リクエストを実行します。
その結果はresponceDataに入っています。
この形式はJSONと呼ばれる形式で取得しているので、アクセスのしやすい形に整形します。
これを自動的にやってくれるメソッドが
NSJSONSerialization JSONObjectWithData: options: error:

です。

この結果をNSLogで出力すると、多分自分のタイムラインがずらーっと出力されます。







タイムラインをUITableViewに表示
このままだと扱いにくいので、データをクラス変数に突っ込んで行きます。
NSLogで出力した部分を次のように変更します。

for (NSDictionary *tweet in timeline) {
 [tweetTextArray addObject:[tweet objectForKey:@"text"]];
 NSDictionary *user = [tweet objectForKey:@"user"];
 [userNameArray addObject:[user objectForKey:@"screen_name"]];
}
[self.tableView reloadData];

先ほどのJSONObjectWithDataメソッドの返り値のデータ構造は、配列の各要素にツイートの内容が納められています。
この結果を一つ一つ取り出して、tweetTextArrayとuserNameArrayに入れて行きます。
見慣れないfor文は、Objetcive-Cにある文法の一つで、配列の各要素にたいして処理を行うことのできるものです。

ツイートの入っている配列の各要素は中身がNSDictionary型です。
ツイートの内容はキーがtextでNSString型で取り出すことができます。
ユーザー情報はuserでNSDictionary型で取り出し、さらにキー値をscreen_nameで取り出すことができます。

すべてのデータを取り出した後は、テーブルビューのデータを再読み込みして表示します。


データを突っ込んだ後は、テーブルビューに表示する内容を作りましょう。

// Customize the number of sections in the table view.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
 return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
 return [userNameArray count];
}

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];

    }

 // Configure the cell.
 cell.textLabel.text = [userNameArray objectAtIndex:indexPath.row];
 cell.detailTextLabel.text = [tweetTextArray objectAtIndex:indexPath.row];
 
    return cell;
}


numberOfSectionsInTableView
ではテーブルビューのセクション数を返します。今回は1で。

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
では何個セルがあるかを返します。今回はツイートの数だけなので、userNameArrayかtweetTextArrayの数を返します。
countメソッドかプロパティで値を返すことができます


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
では結果を表示して行きます。
cellのスタイルはUITableViewCellStyleSubtitleとします。これはメイン、サブ二種類のテキストを貼付けることができます。
貼付けは以下のように行います。

cell.textLabel.text = [userNameArray objectAtIndex:indexPath.row];
cell.detailTextLabel.text = [tweetTextArray objectAtIndex:indexPath.row];



これで実行すると次のようにタイムラインを表示することができます。



Twitterへ投稿
これは今回の宿題としましょう。
updateTimelineと同じように投稿できます。もちろん部分部分は異なりますが。
TWRequestMethodGETをTWRequestMethodPOSTに変えたりで出来ます。



詳しくはここを参考にどうぞ!
https://dev.twitter.com/docs/ios/posting-images-using-twrequest


2012年2月15日水曜日

iPhoneで画像処理 - 2


iPhoneで画像処理

続いて、画素単位へのアクセスを行います。
案外簡単な作業だと思うんですが、この処理を行うにはいろいろと手続きを踏まないと行けません。

UIImage型から画素単位までアクセスし、再びUIImageに戻す手続きは次のデータ型を経ます。

UIImage → CGImageRef → CFDataRef → CGImageRef →UIImage

実に面倒です。おまけに、CGImageRefからCFDataRefへ変換する際にはmallocが発生するので、
処理時間もかかってしまいます。

では具体的にコードを書いて行きます。

CGImageRef cg = inputImage.image.CGImage;
size_t height = CGImageGetHeight(cg);
size_t width = CGImageGetWidth(cg);
size_t bytesPerRow = CGImageGetBytesPerRow(cg);
size_t bitsPerPixel = CGImageGetBitsPerPixel(cg);
CFDataRef dataRef = CGDataProviderCopyData(CGImageGetDataProvider(cg));
UInt8* pixel = (UInt8*)CFDataGetBytePtr(dataRef);

// 処理内容
for( int h = 0; h < height; ++h){
for (int w = 0; w < width; ++w) {
UInt8* buf = pixel + h * bytesPerRow + w * bitsPerPixel / 8;
UInt8 r, g, b;
// rgb値取得
r = *(buf + 0);
g = *(buf + 1);
b = *(buf + 2);
// rgbを入れ替えて値出力
*(buf+0) = b;
*(buf+1) = g;
*(buf+2) = r;
}
}

CFDataRef dst = CFDataCreate(NULL, pixel, CFDataGetLength(dataRef));
CGDataProviderRef dstDataProvider = CGDataProviderCreateWithCFData(dst);
CGImageRef dstCGImage = CGImageCreate(width, height, CGImageGetBitsPerComponent(cg),
  bitsPerPixel, bytesPerRow,CGImageGetColorSpace(cg),
  CGImageGetBitmapInfo(cg),dstDataProvider, NULL,
  CGImageGetShouldInterpolate(cg),CGImageGetRenderingIntent(cg));
UIImage *dstUIImage = [UIImage imageWithCGImage:dstCGImage];
outputImage.image = dstUIImage;

CGImageRelease(dstCGImage);
CFRelease(dst);
CFRelease(dataRef);
CFRelease(dstDataProvider);


UIImageからCGImageRefへの変換、アクセスは
CGImageRef cg = inputImage.image.CGImage;

で可能です。
その後、画像の幅、高さ、一行当たりのバイト数、一画素当たりのビット数などを取得します。
CFDataRef dataRef = CGDataProviderCopyData(CGImageGetDataProvider(cg));
UInt8* pixel = (UInt8*)CFDataGetBytePtr(dataRef);

では、CGImage型からCFDataRef型へ変換を行います。CFDataは多分汎用的に扱えるデータ型です。
画像以外にも文章、音声でも可能です。(多分)
CFDataGetBytePtrでその先頭アドレスを取得します。

処理はよくやるforループを二重にまわせば出来ます。
画素(h,w)のアドレスにアクセスするには

UInt8* buf = pixel + h * bytesPerRow + w * bitsPerPixel / 8;


でできると思います。最後の /8 はビット→バイトの変換です。あんまりマジックナンバーは使わない方がいいんだろうけど。

CFDataからUIImageへの変換は以下の手続きを踏んで行います。

CFDataRef dst = CFDataCreate(NULL, pixel, CFDataGetLength(dataRef));
CGDataProviderRef dstDataProvider = CGDataProviderCreateWithCFData(dst);
CGImageRef dstCGImage = CGImageCreate(width, height, CGImageGetBitsPerComponent(cg),
  bitsPerPixel, bytesPerRow,CGImageGetColorSpace(cg),
  CGImageGetBitmapInfo(cg),dstDataProvider, NULL,
  CGImageGetShouldInterpolate(cg),CGImageGetRenderingIntent(cg));
UIImage *dstUIImage = [UIImage imageWithCGImage:dstCGImage];

CGImageCreate関数が実に引数が多くてどうにかならんのかと思いますが、それは仕方ないんでしょうね。
それぞれの行の変換の流れを追うと、
  1. ポインタpixelから始まるメモリのCFDataRefを作る
  2. CFDataからCGDataProviderRefを作る
  3. CGDataProviderRefからCGImageRefを作る
  4. CGImageからUIImageを作る

という流れです。もっと簡便にしてくれたらいいのにと思いますが。。。


残りの部分では,UIImageViewに貼付けたり、後処理としてリリースをしたりしています。
CFなんとか、CGなんとかで始まる関数や型はC言語ベースで、自動的にリリースされないのでご注意ください。

微分フィルタを作ってみたらこんな感じでした。




わかりづらかったから微分値を4倍してます。

2012年2月11日土曜日

iPhoneで画像処理 - 1


今回はiOS上で画像処理を行います。
iPhoneやiPadにはカメラがついてたりして、いろんな画像処理を施したカメラアプリが登場しています。
今回はその入門編です。

大きく分けて
  • CoreImageを使ったフィルタリング
  • 画素単位へのアクセス

をやって行きたいと思います。



CoreImageを使ったフィルタリング
CoreImageとは,iOSやMacの画像クラスの一つです。CoreFilterと組み合わせて簡単なフィルタリングを行うことが出来ます.
UIImageとの違いは、UIImageはViewとして表示するのに適したクラスで、このCoreImageは画像の変換とか処理に特化している印象です。

ではまずはプロジェクトを作って行きましょう
テンプレートはSingleViewApplicationを選びます。次にライブラリを追加します。
左カラムよりプロジェクトを選択、その中の Linked Frameworks and Libraries の項から+マークをクリック、
CoreImage.frameworkをAddします。



つぎは、UIを編集します。
.xibファイルを選んで、UIImageViewを二つとスライダーを設置します。
UIImageViewには処理前後の画像を、スライダーはフィルタのパラメータ調整に使います。


.hファイルを選んで、クラスを作って行きます。
今回は、UIImageView二つに加え、CIContextとCIFilterの変数も追加します
メソッドはスライダーの変更をトリガーに呼ばれるメソッドを一つ作ります。

@interface SENSViewController : UIViewController{
 IBOutlet UIImageView *originalImage;
 IBOutlet UIImageView *filteredImage;
 CIContext *myCIContext;
 CIFilter *myCIFilter;
}

- (IBAction)sliderMoved:(id)sender;

それぞれのパーツをxibと接続します。
こんな感じに。
あと何か画像を追加しておいてください。できればカラーのものがいいと思います。
では、処理の追加に移りましょう
viewDidLoadを次のように変更します
- (void)viewDidLoad
{
    [super viewDidLoad];
 // Do any additional setup after loading the view, typically from a nib.
 
 
 originalImage.image = [UIImage imageNamed:@"dambo.jpg"];

 myCIContext = [CIContext contextWithOptions:nil];
 [myCIContext retain];

 myCIFilter = [CIFilter filterWithName:@"CIExposureAdjust"];
 [myCIFilter setDefaults];
 CIImage *srcImage = [CIImage imageWithCGImage:originalImage.image.CGImage];
 [myCIFilter setValue:srcImage forKey:@"inputImage"];
 [myCIFilter retain];

}

CIContextは処理画像をUIImageViewに貼付ける際に一度挟む中間処理的なものというイメージです。

CIFilterはフィルタリング処理を行ってくれるクラスです。フィルタの名前を指定して、初期化を行います。
今回は露光を擬似的に調整するフィルタ、CIExposureAdjustを用います

その後、フィルタのパラメータを与える必要があるんですが、
[myCIFilter setDefaults];
とすることで、デフォルトの値で初期化を行うことが出来ます。

さらに、その後で
[myCIFilter setValue:srcImage forKey:@"inputImage"];
では、入力パラメータとして処理対象の画像を指定しています。




retainについて
何度かメソッドが呼ばれています。これは、このインスタンスをまだ解放しないでねというメッセージを送っています。
これがなかった場合、viewDidLoadが終了するとmyCIContextやmyCIImageなどは解放されてしまいます。
では、いつまで解放されないかというと、releaseメソッドを呼ぶまで解放されません。
具体的には、Objective-Cのメモリ管理機構の話になるんで詳細は避けますが、インスタンスはそれぞれ、何個のインスタンスから
参照されているかというretainCounterというものを持っています。

  • retainをするとこのカウンタが+1され、
  • releaseをするとこのカウンタが-1されます。
カウンタが0となったときにオブジェクトは解放されます。



では、次はスライダーの値を変更したときに呼ばれるメソッドの実装を行います。

-(IBAction)sliderMoved:(id)sender{
[myCIFilter setValue:[NSNumber numberWithFloat:[(UISlider *)sender value]]
  forKey:@"inputEV"];
CIImage *dstImage = myCIFilter.outputImage;
CGImageRef myCGImage = [myCIContext createCGImage:dstImage
fromRect:dstImage.extent];
filteredImage.image = [UIImage imageWithCGImage:myCGImage];
CGImageRelease(myCGImage);
}


まず、最初にフィルタのパラメータの一つであるinputEVを設定しています。


NSNumber型は、整数型から浮動小数点型までをラップしてくれる便利なクラスです。今回は引数としてfloat型を与えるのでnumberWithFloatとしています。

フィルタ処理の結果は、フィルタのoutputImageというプロパティです。
多分このプロパティが呼ばれるときにフィルタリング処理を行っています。

その後、CIImage型からUIImage型までの変換を行っています。順番は

CIImage → CGImageRef → UIImage

という順番です。CGImage型はCoreGraphicsImageの略(多分)で、たしかハードウェアよりのなんかだったような…。

これを実行すると多分こんな感じになります。


どうしても一度CGImageRefをメモリ確保しているので、処理が遅くなってしまいますよね。
プロの方々はどうされてるんでしょうか…。






他のフィルタを使ってみる。

今回はフィルタとして露光量を擬似調整するCIExposureを使いましたが、他にはどういったフィルタがあるんでしょうか。
実装されているフィルタは以下のコードで確認することが出来ます。

 
 NSArray *options = [CIFilter filterNamesInCategory:kCICategoryBuiltIn];
 NSLog(@"options of filter");
 for (NSString *str in options) {
  CIFilter *filter = [CIFilter filterWithName:str];
  NSLog(@"%@", filter.attributes);
 }
名前だけ出力した結果が次のようになります。
  • CIAdditionCompositing
  • CIAffineTransform
  • CICheckerboardGenerator
  • CIColorBlendMode
  • CIColorBurnBlendMode
  • CIColorControls
  • CIColorCube
  • CIColorDodgeBlendMode
  • CIColorInvert
  • CIColorMatrix
  • CIColorMonochrome
  • CIConstantColorGenerator
  • CICrop
  • CIDarkenBlendMode
  • CIDifferenceBlendMode
  • CIExclusionBlendMode
  • CIExposureAdjust
  • CIFalseColor
  • CIGammaAdjust
  • CIGaussianGradient
  • CIHardLightBlendMode
  • CIHighlightShadowAdjust
  • CIHueAdjust
  • CIHueBlendMode
  • CILightenBlendMode
  • CILinearGradient
  • CILuminosityBlendMode
  • CIMaximumCompositing
  • CIMinimumCompositing
  • CIMultiplyBlendMode
  • CIMultiplyCompositing
  • CIOverlayBlendMode
  • CIRadialGradient
  • CISaturationBlendMode
  • CIScreenBlendMode
  • CISepiaTone
  • CISoftLightBlendMode
  • CISourceAtopCompositing
  • CISourceInCompositing
  • CISourceOutCompositing
  • CISourceOverCompositing
  • CIStraightenFilter
  • CIStripesGenerator
  • CITemperatureAndTint
  • CIToneCurve
  • CIVibrance
  • CIVignette
  • CIWhitePointAdjust

CIToneCurveは多分色調変換するフィルタですね。
CISepiaToneは色味をセピア調に変換してくれます。
他にも二つの色を合成するCIOverlayBlendModeなどがあります。

フィルタの詳細とそのパラメータはAppleのデベロッパー用サイトから確認できます。

例えばCISepiaToneは

CISepiaTone

Maps the colors of an image to various shades of brown.
Parameters
inputImage
A CIImage class whose display name is Image.
inputIntensity
An NSNumber class whose attribute type is CIAttributeTypeScalar and whose display name is Intensity.
Default value: 1.00 Minimum: 0.00 Maximum: 0.00 Slider minimum: 0.00 Slider maximum: 1.00 Identity: 0.00
Member of
CICategoryBuiltIn, CICategoryStillImage, CICategoryNonSquarePixels, CICategoryInterlaced, CICategoryVideo,CICategoryColorEffect
Localized Display Name
Sepia Tone
と書いてあります。
パラメータは二つで、
入力画像のinputImage、どれくらいセピア感を出すかというinputIntensityです。
inputImageはCIImage型
inputIntensityはNUNumber型と書いてあります。

この辺をどうやって入力するか、ですが、
inputImageは先ほどの例と同じで

[myCIFilter setValue:srcImage forKey:@"inputImage"];

とすればOK.
inputIntensityは先ほどの"inputEV" と同じように

[myCIFilter setValue:[NSNumber numberWithFloat:(何らかの値)]
  forKey:@"inputIntensity"];

で設定できます。何らかの値はスライダーの値を引っ張ってくるなり、予め与えておくなりで行けます。

2012年2月9日木曜日

UIView を使いこなそう


今回の目次
  • UIViewの種類
  • UITableViewとUINavigationContoller
  • UIImageViewとUIScrollView
iOSの画面の構成は、UIViewに溢れています。

なので、UIViewを理解することは即ち、iOSの画面構成を理解する!といっても過言ではありません。
今回は、そんなUIViewについて説明して行きます。


UIViewの種類
UIViewの種類と一口に言っても、いろいろな種類があります。
例えば、
  • 画像を保持するUIImageView
  • 文字列を表示するUILabel (前回使いました)
  • 画面のスクロールを管理するUIScrollView
  • 表を出力するUITableViewと各セルを表すUITableViewCell
などなどです。

そしてiOSの画面は、このUIViewの積み重ねで出来ています。
例えば、でかいビューがあって、その上にUIImageViewとUILabelが乗っかってて、
さらに、UIScrollViewがラップしてる、などいろんなViewが積み重なっています。
どういうViewがどうやって積み重なっているかの順序関係で時々はまったりするので、抑えておくことをお勧めします。

・UINavigationControllerによる画面遷移
iOSの画面遷移にはいろんな方法がありますが、一般的なのは、テーブルビューから横にヒュイッと移動するのが割と多いと思います。
(最近はもっと洗練された奴がでてきてたりしますが)
これは、UINavigationControllerというクラスが、管理しています。
pushViewControllerというメソッドを呼び出すと次の画面に移ります。
前の画面に戻るのは popViewControllerというメソッドです。
自分で呼び出すこともできますし、NavigationControllerも勝手にボタンを作ってくれます。



UITableViewとUINavigationController
それでは、前回と同じようにXcodeを起動して、プロジェクトを作る所から始めましょう。
今回はひな形に Master-Detail Applicationを使います。その際に、Device familyはiphoneを、Use Storyboardと Use Automatic Reference Countingのチェックボックスは外しておいてください

多分、起動するとこんな画面になると思います。


Detailをクリックすると、こんな画面に行って、


Masterを押すと元に戻ると思います。

まずは、新しいViewを追加しましょう
New → file → NewFile として、新しいファイルを追加します。
クラステンプレートはUIViewController subclass
Target for iPadのチェックは外す
With XIB for user Interfaceのチェックはオン
としてファイルを作ります。ファイル名はなんでもいいですが、ImageViewControllerとしました。

次にMasterViewController.hを修正します。

まずクラス変数として、ImageViewController型の変数を一つ追加します。
そしてメソッドを定義する所に
@property (strong, nontoxic) ImageViewController *imgViewController;
と入れます。これは、自動的にアクセサを作ってくれるおまじないです。

では、次は.mファイルへうつります
@implementationの下に
@synthesize imgViewController;
と入れておいてください。

まずは、
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
とします。これは、テーブルが何個セルがあるかを返すメソッドです。
テンプレートファイルでは初期状態で1個あり、今から自分たちで追加するので2を返します。

次に、メソッド
(UITableViewCell *)tableView:tableView cellForRowAtIndexPath:indexPath
を修正します。if文以降を次のように変更し、次のようにします。
このメソッドは、各テーブルの一つ一つに何を表示するかを与えて返します。テーブルのセルはUITableViewCell型で、自分でいろいろといじれますが、今回は割愛します。
indexPath にどのグループの何番目のセルかという情報が入っています。
今回は0番目の列にはテンプレートのものを
1番目の列には、自分の作るものを書きました。


そしてメソッド
- tableView:(UITableView *)tableView didSelectRowAtIndexPath:indexPath

は次のように変更します。

このメソッドは、indexPathのセルが選択されたときに、何をするかを書きます。
今回は選択したら、次の画面へ遷移するようにします。
if (!self.imgViewController) {
    self.imgViewController = [[[ImageViewController alloc] initWithNibName:@"ImageViewController" bundle:nil] autorelease];
   }

の部分は、メンバ変数 imgViewControllerの初期化を行っています。
initWithNibNameは,@"".xib の仕組みを使って初期化しますよというメソッドです。

[self.navigationController pushViewController:self.imgViewController animated:YES];
が画面遷移を行うメソッドです。

以上を書いて実行すると、次のような画面になると思います。






UIImageViewとUIScrollView
では、このViewをいじって行きましょう.今回はUIImageViewを用いた画像の表示を行います。

まずは、プロジェクトに画像を追加します。
画像をプロジェクトにドラッグして行くと勝手に追加してくれます。簡単ですね。



ではまず、Viewの見た目と接続を行います
  1. .xibファイルの右タブのアイテムから、UIImageViewと書かれたアイテムをドラッグして、Viewにドロップします
  2. ImageViewController.hに移り,クラスImageViewControllerにメンバ変数として IBOutlet UIImageView型の変数を追加します。名前は何でも構いませんが、今回はimgViewとしました。
  3. もう一回.xibファイルに戻って,imgViewと配置したimageViewとを接続します.

これは前回とほぼ同じ内容なので簡単ですね。

では次に,コードを書いて画像を表示させましょう。
imageViewController.mに移ります。
そして,viewDidLoadメソッドを以下の行を追加します。

imgView.image = [UIImage imageNamed:@"LENNA.bmp"];

viewDidLoadは画面が呼び出されたときに呼ばれるメソッドで、
ここで画面に貼る画像を呼び出して貼付けます。
UIImageとは、画像を簡単に扱ってくれるクラスです。残念なことに低レベルな処理(画素単位の修正)などは出来ませんが。
imageNamedメソッドは@""のファイル名のファイルを読み込んでUIImageで初期化してくれるメソッドです.
読み込んだUIImageをimgViewの持つ画像のプロパティ imgView.image にセットすることでimageViewへの画像の読み込みが完了します。
めっちゃ簡単ですね。

実行するとこんな感じになると思います。



画像のアスペクト比がおかしいことになったりするので、そういった場合は
貼付ける前に
imgView.contentMode = UIViewContentModeScaleAspectFit;
とすると、画像のアスペクト比を保ったまま、ちょうど目一杯に入るように画像を調整してくれます。





ではiPhoneらしいUIの一つ、スクロールに挑みましょう
まず、.xibファイルより先ほど追加したUIImageViewを削除します。
次にScrollViewを引っ張って来て、Viewに貼付けます。


ImageViewController.hを変更しましょう
@interface ImageViewController : UIViewController
<
UIScrollViewDelegate
>
{
 IBOutlet UIImageView *imgView;
 IBOutlet UIScrollView *scrollView;
}

@end

まず,UIViewControllerの後に 
< UIScrollViewDelegate > 
を追加して、メンバ変数に UIScrollView型を追加します。

そして再び.xibファイルに戻って、変数と配置したパーツとを繋ぎます。

ではImageViewController.mを変更して行きます。
まず, viewDidLoadメソッドを次のように変更します。


今回imgViewはUIパーツの方に入っていないので、自分で初期化する必要があります。
UIImageView initWithImageメソッドは,UIImageを用いて初期化するメソッドです.

その下のscrollViewに関する設定は、
まずcontentSizeで表示するコンテンツの大きさを設定しました。今回のコンテンツは画像の大きさとしました。
そのあと,maximumZoomScaleとminimumZoomScaleでズームの範囲を設定しています.
delegateについては後ほど述べます.
そして addSubViewでスクロールビューの下にサブビューを貼付けます.


そして,次のメソッドを追加します。
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
 return imgView;
}

これは,スクロールビューでズームが行われたときに,どのビューを拡大縮小しますかというのを返すメソッドです.
今回は,imgViewを返します.
本来,このメソッドhaUIScrollViewに定義されていて,UIScrollViewが実装するはずです.
しかし,objective-Cでは定義だけして,あとはサブクラスとか利用するクラスが代わりに実装して!という宣言の仕方が
可能で,その仕組みをdelegate(デリゲート)と呼びます.

どれをデリゲートしないといけないかは,ドキュメントなどに書いてあります.


実行すると,こんな感じでスクロールできると思います.
シミュレータ上での二本指シミュレーションはoptionキーを押しながらドラッグです。




2012年2月6日月曜日

Hello World


UIのパーツを触ってみる

今日のメニュー
  1. UIの配置
  2. アクションとその反映
  3. 他のUIパーツ

プログラミングのHello Worldまでが今日の目標です。


UIの配置

iOSにはUIのパーツが何種類かあり、どこに何を配置しているかという情報は拡張子が.xibのファイルに保存されています。
左のカラムからそのファイルをクリックすると、次のような画面になると思います。


んで、右上のViewと書かれたセグメントの右のボタンをクリックすると、右からにゅいっとビューが出てきます。

そこからLabelをドラッグして貼付けます。
次はその下の項目のRound Rect Buttonも同じようにドラッグします。



⌘+Sで一回保存しておきましょう。

これでUIの配置はひとまず終了です。


アクションとその反映
次は配置したパーツをソースコードに反映させます。
MPAViewController.hを左カラムから選びます。
んで、次のようにコードを書いてください。

※一部は多分自動的に書いてあります。

では簡単に仕組みを説明して行きます。
iOSはObjective-Cという言語で動いています。そしてObjective-CはCやC++をオブジェクト指向に拡張したものという風になっています。
ソースは主に二種類で、
  • クラスやメソッドの宣言をする.hファイル
  • メソッドの中身を書く.mファイル
です。

.hファイルを一行ずつ解説しましょう。

一行目:
@interface MPAViewController : UIViewController

@interfaceはクラスを宣言するときの最初の言葉です。続いて、MPAViewControllerがクラス名、
そのあとのUIViewControllerは親クラスです。
親クラスとはなんぞやとか、UIViewControllerとは何ぞやとかを説明しだすとキリがないので、
今から自分たちが作って行くものは、基本的にiOS SDKに最初から入っている機能を拡張して行くものだと考えてください。

二行目:
IBOutlet UILabel *myLabel;

ここでは、クラス変数を定義します。UILabelがクラス名、*myLabelが変数名です。
IBOutletは、今のところあまり気にしないでください。
今回はUILabel型の myLabelというラベルにhello worldを表示させます。先ほど作ったラベルと
アクションの接続はその後行います。


五行目:
-(IBAction)myPushButton:(id)sender;

この行はメソッドの定義を行っています。
-(IBAction)はメソッドの戻り値の型
myPushButton:はメソッド名
(id)sender; は引数です。

id型はC言語におけるvoid型のような物で、どのようなオブジェクトにも型を姿を変えます。
使うときは適宜キャストする必要があります。
今回、このメソッドはボタンを押されたときに呼び出されるように次で接続を行います。


UIパーツとメソッド、変数の接続

さきほど作ったxibファイルと.hファイルを接続しましょう。
再び左カラムより.xibファイルを選択します。
左から二つ目のカラムにPlaceholdersという欄があると思います。その欄のFile's Ownerを左クリックすると
下のような画面が出てきます。


myLabelの右側の丸い部分をにゅいっとドラッグして、Labelまで持ってきて離す。すると配置したLabelと.hファイルで定義したmyLabelが接続されます。


次にmyPushButtonも同様に、今度は配置したボタンまでドラッグします。離すといくつかの選択肢が出てくると思うので、
ButtonTouchDownを選択します。

これで接続は終了です。多分下の画像みたいになってると思います。
保存するのをお忘れなきよう。



メソッドの実装
それではボタンをクリックしてhello worldと表示するソースを実装しましょう。

MPAViewController.mを開きます。
そして次のソースコードを追加します。

これは、自分で定義したmyLabelというインスタンスに、 Hello Worldという文字列をセットしなさい
というメソッドを呼び出しています。
Objective-Cのメソッド呼び出しは
[インスタンス名 メソッド名:引数, ….];

で行います。見た目が気持ち悪いですが。

それでは、実行してみましょう。
ボタンを押して、以下のようになれば成功です。


Hello Worldがちゃんと表示されてないですが、
.xibファイルから、ラベルの大きさをいじるときちんと表示できます。



他のUIパーツ

今から書く