iPhoneでお手軽画像変形

概要

斜めの方向から撮影してしまったけど、後から正面から撮った画像が欲しくなった事はありませんか?
例えば、以下の写真ような場合です。
f:id:cflat-inc:20131215100700p:plain

実はこれを正面から撮ったように画像を変形する事が出来ます。
そう。ホモグラフィならね。

こんな感じに出来ます。
f:id:cflat-inc:20131215100708p:plain


モグラフィとは射影変換を用いてある平面から別の平面と射影する変換です。
似たものにアフィン変換がありますが、こちらは平行なものは平行(長方形は長方形)に変換されます。
モグラフィでは平行性は保存されないので、台形から長方形へと変換する事が出来ます。

画像処理向けライブラリのopencvを使えば、iPhoneで簡単に実装出来ます。
早速実装してみましょう。

環境

プロジェクトの作成

プロジェクトの新規作成からSingle View Applicatioを選択し、プロジェクト名はHomographyとします。
f:id:cflat-inc:20131215100714p:plain

また、storyboard上でviewcontrollerにUIImageViewと2つのボタン(camera, decide)を設定しておきます。
f:id:cflat-inc:20131215100653p:plain

opencvの設定

公式ドキュメントに従ってレポジトリから落としてきてビルドします。

cd ~/
git clone https://github.com/Itseez/opencv.git
cd /
sudo ln -s /Applications/Xcode.app/Contents/Developer Developer
cd ~/
python opencv/platforms/ios/build_framework.py ios

ただし、これだと私の環境ではビルドエラーが発生したので、

git checkout 2.4.6

として再度ビルドを実行したら成功しました。

続いてxcodeopencvの設定を行います。
pchファイルにopencvのimportを追加します。

#import <Availability.h>

#ifndef __IPHONE_5_0
#warning "This project uses features only available in iOS SDK 5.0 and later."
#endif

#ifdef __cplusplus
#import <opencv2/opencv.hpp>
#endif

#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
#endif

またLink Binary With Librariesでopencv2.frameworkを追加します。
f:id:cflat-inc:20131215100721p:plain

実装

C++ソースコードを使うのでViewController.mの拡張子を変更し、ViewController.mmに変更します。

実際にホモグラフィを算出する部分の抜粋です。

// ホモグラフィ算出
cv::Mat status;
cv::Mat H = cv::findHomography(cv::Mat(m_selected_pnts), cv::Mat(dst_pnts), status);

cv::Mat warpImg;
cv::warpPerspective(inputImg, warpImg, H, inputImg.size());

cv::findHomographyでホモグラフィを求めて、cv::warpPerspectiveで画像を変換します。
この他に4隅を指定したりするためのコード等を合わせた全体のコードが以下のようになります。

Viewcontroller.h

#import <UIKit/UIKit.h>
@interface ViewController : UIViewController<UINavigationControllerDelegate, UIImagePickerControllerDelegate>
@end


Viewcontroller.mm

#import "ViewController.h"
#include <opencv2/core/core_c.h>
#import "opencv2/highgui/ios.h"
#include <vector>

@interface ViewController ()
{
    std::vector<cv::Point2f> m_selected_pnts;// 左上から順に時計回り
}

@property (nonatomic, strong) UIImagePickerController *picker;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (nonatomic, weak) UIView *view1;
@property (nonatomic, weak) UIView *view2;
@property (nonatomic, weak) UIView *view3;
@property (nonatomic, weak) UIView *view4;

@end
@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    self.imageView.image = nil;
    self.imageView.contentMode =     UIViewContentModeScaleToFill;
    m_selected_pnts.clear();
    
    self.view1 = [self viewWithRect:CGRectMake(40, 40, 60, 60) withColor:[UIColor colorWithRed:0 green:0 blue:1 alpha:0.5]];
    self.view2 = [self viewWithRect:CGRectMake(40, 300, 60, 60) withColor:[UIColor colorWithRed:0 green:1 blue:0 alpha:0.5]];
    self.view3 = [self viewWithRect:CGRectMake(200, 300, 60, 60) withColor:[UIColor colorWithRed:1 green:0 blue:0 alpha:0.5]];
    self.view4 = [self viewWithRect:CGRectMake(200, 40, 60, 60) withColor:[UIColor colorWithRed:1 green:1 blue:0 alpha:0.5]];
}

-(void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [self.view1 removeFromSuperview];
    [self.view2 removeFromSuperview];
    [self.view3 removeFromSuperview];
    [self.view4 removeFromSuperview];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
- (UIView *)viewWithRect:(CGRect)rect withColor:(UIColor *)color
{
    UIView *view = [[UIView alloc] initWithFrame:rect];
    view.backgroundColor = color;
    [self.imageView addSubview:view];
    
    CGFloat len = 4;
    UIView *p = [[UIView alloc] initWithFrame:CGRectMake(0.5*(rect.size.width - len), 0.5*(rect.size.height - len), len, len)];
    p.backgroundColor = [UIColor blackColor];
    [view addSubview:p];
    
    //gesture recognizerを設定
    UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dragView:)];
    [view addGestureRecognizer:panGestureRecognizer];
    return view;
}

//ドラッグ時にコールされるメソッド
-(void)dragView:(UIPanGestureRecognizer *)sender
{
    UIView *targetView = sender.view;
    CGPoint p = [sender translationInView:targetView];
    CGPoint movedPoint = CGPointMake(targetView.center.x + p.x, targetView.center.y + p.y);
    targetView.center = movedPoint;
    [sender setTranslation:CGPointZero inView:targetView];
}

- (IBAction)ButtonPushed:(id)sender {
    self.picker = [[UIImagePickerController alloc] init];
    self.picker.sourceType = UIImagePickerControllerSourceTypeCamera;
    self.picker.delegate = self;
    [self presentViewController:self.picker animated:YES completion:nil];
}

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    [self.picker dismissViewControllerAnimated:YES completion:nil];
    self.imageView.image = [info objectForKey:UIImagePickerControllerOriginalImage];
}
// 入力画像を射影変換する
- (UIImage *)projectiveTransForm:(UIImage *)image
{
    // 対象となる領域
    CGFloat min_x = FLT_MAX;
    CGFloat max_x = FLT_MIN;
    CGFloat min_y = FLT_MAX;
    CGFloat max_y = FLT_MIN;
    for(auto it = m_selected_pnts.begin(); it != m_selected_pnts.end(); ++it){
        min_x = MIN(min_x, it->x);
        max_x = MAX(max_x, it->x);
        min_y = MIN(min_y, it->y);
        max_y = MAX(max_y, it->y);
    }
    std::vector<cv::Point2f> dst_pnts;
    dst_pnts.push_back(cv::Point2f(min_x, min_y));// 左上
    dst_pnts.push_back(cv::Point2f(min_x, max_y));// 左下
    dst_pnts.push_back(cv::Point2f(max_x, max_y));// 右下
    dst_pnts.push_back(cv::Point2f(max_x, min_y));// 右上
    
    // ホモグラフィ算出
    cv::Mat status;
    cv::Mat H = cv::findHomography(cv::Mat(m_selected_pnts), cv::Mat(dst_pnts), status);
    
    // 画像が回転しているので修正
    UIGraphicsBeginImageContext(image.size);
    [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    cv::Mat inputImg;
    UIImageToMat(image, inputImg);
    
    cv::Mat warpImg;
    cv::warpPerspective(inputImg, warpImg, H, inputImg.size());
    return MatToUIImage(warpImg);
}
-(CGPoint)coordinateForCGImage:(UIImageView*)imageView cordinate:(CGPoint)coordinate
{
    CGImageRef cgImage = imageView.image.CGImage;
    CGFloat imageWidth = CGImageGetWidth(cgImage);
    CGFloat imageHeight = CGImageGetHeight(cgImage);
    float scaleY = imageWidth / imageView.frame.size.height;
    float scaleX = imageHeight  / imageView.frame.size.width;
    return CGPointMake(coordinate.x * scaleX, coordinate.y * scaleY);
}
- (IBAction)decideButtonPushed:(id)sender {
    CGPoint p1 = [self coordinateForCGImage:self.imageView cordinate:self.view1.center];
    CGPoint p2 = [self coordinateForCGImage:self.imageView cordinate:self.view2.center];
    CGPoint p3 = [self coordinateForCGImage:self.imageView cordinate:self.view3.center];
    CGPoint p4 = [self coordinateForCGImage:self.imageView cordinate:self.view4.center];
    
    m_selected_pnts.push_back(cv::Point2f(p1.x, p1.y));
    m_selected_pnts.push_back(cv::Point2f(p2.x, p2.y));
    m_selected_pnts.push_back(cv::Point2f(p3.x, p3.y));
    m_selected_pnts.push_back(cv::Point2f(p4.x, p4.y));
    
    self.imageView.image = [self projectiveTransForm:self.imageView.image];
    m_selected_pnts.clear();
}
@end


※今回のコードはiphoneを縦向きで撮影した場合にしか対応していません。

もう少し手を加えれば色々な事に応用出来そうですね。