ラベル イメージを描画する の投稿を表示しています。 すべての投稿を表示
ラベル イメージを描画する の投稿を表示しています。 すべての投稿を表示

2011年7月15日金曜日

イメージを描画する(11.2)

インターフェイスと bpf デバイスファイルのアタッチ

めも
///// bpf デバイスファイルのオープン /////
    NSArray *devices;
    NSString *deviceFile = @"/dev/bpf";
    int bpf;

    [deviceFile completePathIntoString:nil
                         caseSensitive:YES
                      matchesIntoArray:&devices
                           filterTypes:nil];
      
    for (NSString *device in devices) {
        bpf = open([device UTF8String],O_RDONLY,0);
        
        if (bpf != -1 ) {
            break;
        }
    }

// 使用するネットワークインターフェイスを用意する e.g "en0"
    struct ifreq ifr;
    bzero(ifr.ifr_name, sizeof(char) * IFNAMSIZ);
    strncpy(ifr.ifr_name, argv[1], IFNAMSIZ);

// デバイスファイルとネットワークインターフェイスを接続、設定する
    u_int isImmediately = 1;
    u_int isIO = 0;             // Input only;
    u_int bufferLength = (u_int)([[NSString stringWithCString:argv[2]
                                            encoding:NSUTF8StringEncoding] intValue]

    ioctl(bpf, BIOCSBLEN, &bufferLength); // 長さ
    ioctl(bpf, BIOCSETIF, &ifr);  // 接続
    ioctl(bpf, BIOCIMMEDIATE, &isImmediately); // すぐに書出す
    ioctl(bpf, BIOCSSEESENT, &isIO); // インプットのみ
あとは read(2) でデバイスファイルから読込む。
また読込みは bpf(4) Mac OS X Man page より bpf_hdr について。
The following structure is prepended to each packet returned by read(2):
"以下の構造体(bpf_hdr)はread(2) によって戻される各パケットの先頭に追加されます。"

Additionally, individual packets are padded so that each starts on a word boundary.  This requires that an application has some knowledge of how to get from packet to packet.  The macro BPF_WORDALIGN is defined in to facilitate this process.  It rounds up its argument to the nearest word aligned value (where a word is BPF_ALIGNMENT bytes wide).
p = (char *)p + BPF_WORDALIGN(p->bh_hdrlen + p->bh_caplen)

考え中。

// kernel -> bpf デバイスファイルへのデータコピーの状態
        struct bpf_stat status;
        ioctl(bpf, BIOCGSTATS,&status);
        NSLog(@"receive:%d, drop:%d",status.bs_recv, status.bs_drop);

bs_drop: パケットトラフィックがついていっていないとカーネルがドロップする。その数。

2011年7月10日日曜日

イメージを描画する(11.1)

ネットワークインターフェイスの一覧を取得したい。
めも

/////////////// ネットワークインターフェイスの取得 ///////////////
     int socketFD,length, lastLength;
    char *buffer;
    struct ifconf ifc;
    
    socketFD = socket(AF_INET, SOCK_DGRAM, 0);
    lastLength = 0;
    length = sizeof(struct ifreq) * 100;
    NSLog(@"length:%d",length);
    
    for (; ;) {
        
        buffer = (char *)calloc(length, sizeof(char));
        ifc.ifc_len = length;
        ifc.ifc_ifcu.ifcu_buf = buffer;
        
        if ( ioctl(socketFD, SIOCGIFCONF, &ifc) < 0) {
            int erroNumber = errno;
            NSLog(@"%s",strerror(erroNumber));
        } 
        else {
            
            if (ifc.ifc_len == lastLength) {
                break;
            }
            
            lastLength = ifc.ifc_len;
        }
        
        length += sizeof(struct ifreq) * 10;
        free(buffer);
    }
    
    struct ifreq *ifr = (struct ifreq *)buffer;
    int next = 0;
    /////////////// 表示してみる ///////////////
    while (ifr < (struct ifreq *)(buffer + lastLength)) {
        
        ifr = (struct ifreq *)(buffer + next);
        NSLog(@"interface:%s",ifr->ifr_name);
        next += IFNAMSIZ + ifr->ifr_ifru.ifru_addr.sa_len;
        
    }
    free(buffer);
    close(socketFD);
同じのがいっぱい出た。

2011年6月28日火曜日

イメージを描画する(10)

あなたの瞳の色が僕の中でどんな色で見えているのか伝えたい、写真家にもそんなときってあると思います。というわけでこんなものを作ります。


このイメージを
こう表示する。



-- 途中です。改訂します。 --

/* めも
RGB のカラー・ガモット(color gamut)の各頂点を情報(三刺激値?)を取得する。
プロファイルのデータから rXYZ, gXYZ, bXYZ を得る。

Specification ICC.1:2004-10-10 (Profile Version 4.2.0.0)
redMatrixColumnTag, greenMatrixColumnTag and blueMatrixColumnTag は XYZType.
     -- XYZType Encoding --
     0-3    4byte   'XYZ ' type sigunature.
     4-7    4Byte   reserved, must be set to 0.
     8-end    -     an array of XYZ number. XYZNumber
    
     -- XYZ Number --
     0-3    4Byte   CIE X   s15Fixed16Number
     4-7    4Byte   CIE Y   s15Fixed16Number
     8-11   4Byte   CIE Z   s15Fixed16Number
    
     -- s15Fixed16Number --
     Number                 Encoding
     -32768.0               80000000h
     0                      00000000h
     1.0                    00010000h
     32767 + (65535/65536)  7FFFFFFFh
前半分が符号付き整数、後ろ半分が小数だけど、整数値を65536で割った値。つまり
iiiiiiiiiiiiiiii + ffffffffffffffff / 65536

7 Profile requirements
a)    All profile data shall be encoded as big-endian,

ColorSync Manager API 
CMGetPartialProfileElement //ColorSyncDeprecated.h 非推奨
 elementData の値がリトルエンディアンで戻ったので EndianS32_BtoN にかけた。

*/

2011年6月20日月曜日

イメージを描画する(9)

あなたの瞳は世界をどんなふうに見るの?奇麗な瞳をみられたら写真家にもそんな気持ちになるときがあると思います。というわけで、こんなものをつくります。


Photoshop(CS3)では ”色の校正” プレビュー(5.0.3)では ”プロファイルを使ってソフトプルーフ” 、つまりソフトプルーフ機能を単独で実装します。

NSBitmapImageRep
表示だけのシステムなので、なにかないか探してみますとこんなものがありました。

- (NSBitmapImageRep *)bitmapImageRepByConvertingToColorSpace:(NSColorSpace *)targetSpace
                                                           renderingIntent:(NSColorRenderingIntent)renderingIntent

すてき。長いです。画像を描画意図(rendering intent)で別の色空間に変換してくれます。ここに色校正に使うディスプレイ・プロファイルとアウトプット・プロファイルからカラースペースを作って渡してやればよさそうです。なお 10.6 以降です。

NSColorSpace
NSColorSpace にはプロファイルのクラスごとにカラースペースを生成してくれるメソッドがないので

- (id)initWithColorSyncProfile:(void *)prof

を使ってColorSync プロファイルから作成してやれば良さそうです。引数が (void *)prof となっていますが、リファレンスは CMProfileRef わたすべしとなっています。なるほど。

ColorSync Manager API
目的は CMProfileRef を取得することです。

CMError CMOpenProfile (
   CMProfileRef *prof,
   const CMProfileLocation *theProfile
);

第1引数は戻ったときにプロファイルがはいります。なので第2引数の CMProfileLocation を取得して渡してやればよいですが、長旅になりそうな予感!

少し寄り道 CMIterateColorSyncFolder
インストースされているプロファイルの情報を提供してくれる API はないものでしょうか。そんな僕のために CMIterateColorSyncFolder 関数がありました。すべての利用可能なプロファイルからプロファイルごとの情報を確認できます。

CMError CMIterateColorSyncFolder (
   CMProfileIterateUPP proc,
   UInt32 *seed,
   UInt32 *count,
   void *refCon
);

第1引数は CMIterateColorSyncFolder ルーチンのなかで呼出されるコールバック関数のポインタになります。

第2引数はキャッシュの設定です。CMIterateColorSyncFolder 初めて呼出すときは0をセットしたポインタを渡せとあります。内部の seed と一致しなかったな場合はプロファイルごとに一度のコールバックを呼出すとあります。

第3引数は戻ってきたときに利用可能なプロファイルの数が入れられます。

第4引数は任意のデータへのポインタを指定します。この値は第1引数のコールバックが呼出されるたびに渡されます。なのでコールバック内で参照する必要のあるデータがある場合は設定します。

今度は CMProfileIterateUPP!
CMIterateColorSyncFolder の第1引数 CMProfileIterateUPP は

typedef CMProfileIterateProcPtr CMProfileIterateUPP;

となっていて CMProfileIterateProcPtr は

typedef OSErr (*CMProfileIterateProcPtr)
(
   CMProfileIterateData * iterateData,
   void * refCon
);

です。第1引数にはプロファイルに関連するデータ構造体です。第2引数にはCMIterateColorSyncFolder の第4引数で設定したデータへのポインタがそのままやってきます。

CMProfileLocation を求めて
CMProfileIterateData はどうなっているのでしょうか。

struct CMProfileIterateData {
   UInt32 dataVersion;
   CM2Header header;
   ScriptCode code;
   Str255 name;
   CMProfileLocation location;
   UniCharCount uniCodeNameCount;
   UniChar * uniCodeName;
   unsigned char * asciiName;
   CMMakeAndModel * makeAndModel;
   CMProfileMD5 * digest;
};
typedef struct CMProfileIterateData CMProfileIterateData;

CMProfileLocation ありました。これを CMOpenProfile に渡してやれば CMProfileRef が取得できます。さらに CM2Header 構造体の

struct CM2Header {
   UInt32 size;
   OSType CMMType;
   UInt32 profileVersion;
   OSType profileClass;
   OSType dataColorSpace;
   OSType profileConnectionSpace;
   CMDateTime dateTime;
   OSType CS2profileSignature;
   OSType platform;
   UInt32 flags;
   OSType deviceManufacturer;
   UInt32 deviceModel;
   UInt32 deviceAttributes[2];
   UInt32 renderingIntent;
   CMFixedXYZColor white;
   OSType creator;
   char reserved[44];
};
typedef struct CM2Header CM2Header;

OSType profileClass でプロファイルのクラスの情報があります。

enum {
   cmInputClass = 'scnr',
   cmDisplayClass = 'mntr',
   cmOutputClass = 'prtr',
   cmLinkClass = 'link',
   cmAbstractClass = 'abst',
   cmColorSpaceClass = 'spac',
   cmNamedColorClass = 'nmcl'
};

cmDisplayClass と cmOutputClass を使ってプロファイルのクラスを選別できそうです。

それでは
戻り値のチェックをしていません。
DrawingImage_9_AppDelegate.h
#import <Cocoa/Cocoa.h>

@class DISourceImageView, DIProofImageView;

@interface DrawingImage_9_AppDelegate : NSObject  {
    NSWindow *window;
    
    DISourceImageView *srcView;
    DIProofImageView *proofView;
    
    NSTextField *srcColorSpaceField;
    
    NSBitmapImageRep *srcImage;
}

#pragma mark Accessor Method
@property (assign) IBOutlet NSWindow *window;
@property (assign) IBOutlet DISourceImageView *srcView;
@property (assign) IBOutlet DIProofImageView *proofView;
@property (assign) IBOutlet NSTextField *srcColorSpaceField;

#pragma mark Action Method
- (IBAction)openImage:(id)sender;
@end

DrawingImage_9_AppDelegate.m
#import "DrawingImage_9_AppDelegate.h"
#import "DISourceImageView.h"
#import "DIProofImageView.h"

@implementation DrawingImage_9_AppDelegate

#pragma mark init & dealloc
- (void) dealloc
{
    [srcImage release];
    [super dealloc];
}

#pragma mark Accessor Method
@synthesize window, srcView, proofView, srcColorSpaceField;

#pragma mark Inner Method
- (void)setViewsWithImage:(NSBitmapImageRep *)bitmapImage
{
    if (bitmapImage) {
        NSColorSpace *srcSpace = [bitmapImage colorSpace];
        [srcColorSpaceField setStringValue:[srcSpace localizedName]];
    }
    else {
        [srcColorSpaceField setStringValue:@""];
    }
    
    [srcView setImage:bitmapImage];
    [proofView setImage:bitmapImage];
}

#pragma mark Action Method
-(IBAction)openImage:(id)sender
{
    NSOpenPanel *opPanel = [NSOpenPanel openPanel];
    
    [opPanel setCanChooseFiles:YES];
    [opPanel setCanChooseDirectories:NO];
    [opPanel setAllowsMultipleSelection:NO];
    
    [opPanel beginSheetModalForWindow:window
                    completionHandler:^(NSInteger result){
                        
                        if (result == NSFileHandlingPanelOKButton) {
                            
                            if (srcImage != nil) {
                                [srcImage release];
                            }
                            
                            NSData *data = [NSData dataWithContentsOfURL:[opPanel URL]];
                            srcImage = [[NSBitmapImageRep alloc] initWithData:data];
                            
                            [self setViewsWithImage:srcImage];
                            
                        }
                        else {
                            // do nothing; 
                        }
                        
                    }];
}
@end

DISourceImageView.h
#import <Cocoa/Cocoa.h>

@interface DISourceImageView : NSView {

    NSBitmapImageRep *image;

}

#pragma mark Accessor Method
- (void)setImage:(NSBitmapImageRep *)bitmapImage;
@end

DISourceImageView.m
#import "DISourceImageView.h"

@implementation DISourceImageView

#pragma mark init & dealloc
- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
    }
    return self;
}

#pragma mark Drawing Method
- (void)drawRect:(NSRect)dirtyRect {
    
    if (image) {
        
        // ビューのサイズを取得
        CGSize viewSize = CGSizeMake([self bounds].size.width,
                                     [self bounds].size.height);
        
        // NSBitmapImageRep から CGImageRef を取得
        CGImageRef imageRep = [image CGImage];
        
        // 現在のグラフィクス・コンテキストを取得、変更するのでグラフィック状態を保存
        CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
        CGContextSaveGState(context);
        
        //描画意図を Absolute Colorimetric(絶対的な色域)に設定
        CGContextSetRenderingIntent(context, kCGRenderingIntentAbsoluteColorimetric);
        
        // 体裁を整えてイメージを描画
        CGFloat imageWidth = CGImageGetWidth(imageRep);
        CGFloat imageHeight = CGImageGetHeight(imageRep);
        
        CGFloat scale = imageHeight / imageWidth;
        
        BOOL isWide = (imageWidth >= imageHeight) ? YES : NO;
        
        if (isWide == YES) {
            
            CGFloat translate;
            translate = (viewSize.height - (viewSize.width * scale)) / 2.0;
            
            CGContextTranslateCTM(context, 0.0, translate);
            CGContextScaleCTM(context, 1.0, scale);
        }
        else {
            CGFloat translate;
            translate = (viewSize.width - (viewSize.height * (1.0 / scale))) / 2.0;
            
            CGContextTranslateCTM(context, translate, 0.0);
            CGContextScaleCTM(context, 1.0 / scale, 1.0);
            
        }
        
        CGContextDrawImage(context, CGRectMake(0.0,0.0,viewSize.width,viewSize.height), imageRep);
        
        // グラフィクス・コンテキストをもとに戻す。
        CGContextRestoreGState(context);
    }
    else {
        // do nothing;
    }
}

#pragma mark Accessor Method
- (void)setImage:(NSBitmapImageRep *)bitmapImage
{
    image = bitmapImage;
    [self setNeedsDisplay:YES];
}


@end

DIProofImageView.h
#import <Cocoa/Cocoa.h>

@interface DIProofImageView : NSView {

    NSPopUpButton *outputSpace;
    NSPopUpButton *intent;
    
    NSBitmapImageRep *image;
}

#pragma mark Accessor Method
@property (assign) IBOutlet NSPopUpButton *outputSpace;
@property (assign) IBOutlet NSPopUpButton *intent;
- (void)setImage:(NSBitmapImageRep *)bitmapImage;

#pragma mark Action Method
- (IBAction)selectedColorSpace:(id)sender;
- (IBAction)selectedRenderingIntent:(id)sender;
@end

DIProofImageView.m
#import "DIProofImageView.h"

#pragma mark Profile Iteration Callback Function
OSErr profileIterateProcPtr (CMProfileIterateData *iterateData, void *refCon)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSMutableArray *colorSpaces = refCon;

    // プロファイルのクラスを取得
    OSType profileClass = (*iterateData).header.profileClass;
    
    // クラスがアウトプット、ディスプレイに該当したら追加する。
    if (profileClass == cmOutputClass || profileClass == cmDisplayClass){

        // プロファイルからカラースペースを作成する。
        CMProfileRef prof;
        CMProfileLocation location = iterateData->location;
        
        // プロファイルを開く
        CMOpenProfile(&prof, &location);
        
        // プロファイルからカラースペースを作成
        NSColorSpace *colorSpace = [[NSColorSpace alloc] initWithColorSyncProfile:prof];
        [colorSpaces addObject:colorSpace];
        
        // もういらない。開いたプロファイルは閉じる。
        [colorSpace release];
        CMCloseProfile(prof);
        
    }
    
    [pool drain];
    return noErr;
}
#pragma mark -
@implementation DIProofImageView

#pragma mark init & dealloc
- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
    }
    return self;
}

#pragma mark awakeFromNib
- (void)awakeFromNib
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    // インストールされているプロファイルからカラースペースを作成して、Arrayに追加する。
    NSMutableArray *colorSpaces = [NSMutableArray array];
    CMIterateColorSyncFolder(profileIterateProcPtr, NULL, NULL, colorSpaces);
    
    // メニューを作成してポップアップボタンに設定する。
    NSMenu *outputMenu = [[NSMenu alloc] init];
    
    for (NSColorSpace *colorSpace in colorSpaces) {
        
        NSMenuItem *item = [[NSMenuItem alloc] init];
        
        // メニューアイテムにタイトルとカラースペースを設定、メニューに追加
        [item setTitle:[colorSpace localizedName]];
        [item setRepresentedObject:colorSpace];
        [outputMenu addItem:item];
        
        [item release];
    }
    
    [outputSpace setMenu:outputMenu];
    
    [outputMenu release];
    [colorSpaces removeAllObjects];

    
    // 描画意図(Rendering intent)をポップアップボタンに設定する。
    NSMutableDictionary *intentDic = [NSMutableDictionary dictionary];
    
    [intentDic setValue:[NSNumber numberWithInteger:1] forKey:@"Absolute Colorimetric"];
    [intentDic setValue:[NSNumber numberWithInteger:2] forKey:@"Relative Colorimetric"];
    [intentDic setValue:[NSNumber numberWithInteger:3] forKey:@"Perceptual"];
    [intentDic setValue:[NSNumber numberWithInteger:4] forKey:@"Saturation"];
    
    NSMenu *intentMenu = [[NSMenu alloc] init];
    
    for (NSString *key in [intentDic allKeys]) {
        
        NSMenuItem *item = [[NSMenuItem alloc] init];
        
        // メニューアイテムにタイトルとカラースペースを設定、メニューに追加
        [item setTitle:key];
        [item setRepresentedObject:[intentDic valueForKey:key]];
        [intentMenu addItem:item];
        
        [item release];
    }
    
    [intent setMenu:intentMenu];
    
    [intentMenu release];
    [intentDic removeAllObjects];
    
    [intent selectItemWithTitle:@"Perceptual"];
    
    [pool drain];
}

#pragma mark Drawing Method
- (void)drawRect:(NSRect)dirtyRect {
    
    if (image) {
        
        // 選択されているカラースペースと描画意図からビットマップを変換する
        NSColorSpace *colorSpace;
        NSColorRenderingIntent renderingIntent;
        NSBitmapImageRep *outputImage;
        
        colorSpace = [[outputSpace selectedItem] representedObject];
        renderingIntent = [[[intent selectedItem] representedObject] integerValue];
        outputImage = [image bitmapImageRepByConvertingToColorSpace:colorSpace
                                                    renderingIntent:renderingIntent];
        
        // ビューのサイズを取得
        CGSize viewSize = CGSizeMake([self bounds].size.width,
                                     [self bounds].size.height);
        
        // NSBitmapImageRep から CGImageRef を取得
        CGImageRef imageRep = [image CGImage];
        
        // 現在のグラフィクス・コンテキストを取得、変更するのでグラフィック状態を保存
        CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
        CGContextSaveGState(context);
        
        //描画意図を Absolute Colorimetric(絶対的な色域)に設定
        CGContextSetRenderingIntent(context, kCGRenderingIntentAbsoluteColorimetric);
        
        // 体裁を整えてイメージを描画
        CGFloat imageWidth = CGImageGetWidth(imageRep);
        CGFloat imageHeight = CGImageGetHeight(imageRep);
        
        CGFloat scale = imageHeight / imageWidth;
        
        BOOL isWide = (imageWidth >= imageHeight) ? YES : NO;
        
        if (isWide == YES) {
            
            CGFloat translate;
            translate = (viewSize.height - (viewSize.width * scale)) / 2.0;
            
            CGContextTranslateCTM(context, 0.0, translate);
            CGContextScaleCTM(context, 1.0, scale);
        }
        else {
            CGFloat translate;
            translate = (viewSize.width - (viewSize.height * (1.0 / scale))) / 2.0;
            
            CGContextTranslateCTM(context, translate, 0.0);
            CGContextScaleCTM(context, 1.0 / scale, 1.0);
            
        }
        
        [outputImage drawInRect:[self bounds]];
        
        // グラフィクス・コンテキストをもとに戻す。
        CGContextRestoreGState(context);
    }
    else {
        // do nothing;
    }
}

#pragma mark Accessor Method
@synthesize outputSpace, intent;

- (void)setImage:(NSBitmapImageRep *)bitmapImage
{
    image = bitmapImage;
    [self setNeedsDisplay:YES];
}

#pragma mark Action Method
- (IBAction)selectedColorSpace:(id)sender
{
    [self setNeedsDisplay:YES];
}
- (IBAction)selectedRenderingIntent:(id)sender
{
    [self setNeedsDisplay:YES];
}
@end

Interface Builder での作業



その他
指定したカラースペースに変換したビットマップを生成するのに ColorSync Manager API だけでやろうとするとけっこうめんどうです。NCWConcatColorWorld か CWConcatColorWorld を使って CMWorldRef を作成してやります。それを CWMatchBitmap にかけてやってビットマップを作成すれば良いと思います。また、この間に使用するいくつかの構造体を定義してやらなければなりません。

2011年6月10日金曜日

イメージを描画する(8)

Quartz の カラー・マネジメントに関係するところの基礎的な部分です。参照したドキュメントの最終更新は Tiger のとき。本文中の図はドキュメントのものを勝手に転載しています。基本はいまも(たぶん)同じはず。めも。

参照:TN2035 ColorSync on Mac OS X

ColorSync は以前のバージョンでは任意のインストールだったものを Mac OS X から基本機能としてシステムに組込まれるようになりました。Quartz によっても利用されます。

主な ColorSync 2つの基盤
1つは ICC(International Color Consortium)プロファイル。デバイスの色空間から間メディア(intermedia 中間ってこと?)の色空間への変換方法を記述するドキュメント。もう1つは CMM(Color Management Modules)。要は変換モジュール。ICC プロファイルを使って実際に計算してくれるところ。

ColorSync と Quartz のカラー・マネジメント
ColorSync は Quartz が様々な異なる色空間から選択した色空間に変換する合成処理のための作業スペースとして使われる。Quartz のカラー・マネジメントは Quartz によって作成される PDF のカラー・データを処理するためのエンジンとして ColorSync を中心に構築されている。

PDFのカラー・モデル
PDF での色はデバイス、キャリブレートされたもの、ICC ベースのもののどれかで定義される。変換はソースの色空間、変換先の色空間、描画意図(rendering intent: よくある ”知覚的” とかってゆうあれ)の関数で記述できる。

ColorSync on Mac OS X "Figure 3: PDF Color Conversion." より転載

ColorSync / ICC のカラー・モデル
ICC での色は ICC プロファイルで定義される。変換はソースのプロファイルと変換先のプロファイルの間に間メディア・プロファイルをオプションで挿入できる点が PDF と異なる。描画意図の値は PDF のと同じ。

ColorSync on Mac OS X "Figure 4: ICC Color Conversions." より転載

Quartz のカラー・モデル
Quartz は ICC と PDF のカラー・モデルを統合してつくられた。すべての PDF の色空間は Quartz では ICC プロファイルとして表現される。Quartz が PDF から継承した概念の重要なものの1つに色空間の等価性があり、暗黙のルールとして、ソースの色空間が変換先の色空間と異なる場合のみ必要に応じて色変換がおこなわれる。

ColorSync on Mac OS X "Figure 5: Color space equivalence" より転載
Quartz は1つのソースから1つの変換先へ対応させるという PDF の概念をもっているが、高度なカラー・マネジメントに対応できるよう、3つ以上のプロファイルの複雑な色変換もでき、そのときは ColorSync がそれぞれの色空間がもつプロファイルを1つのプロファイルに連結して PDF に埋め込む。

Quartz のおもな構成
ユーザから見た場合 Quartz の構成は、レベルの高い順に
  1. グラフィック・コンテンツを生成するアプリケーションとしての Quartz
  2. レンダリング・サービスを提供する Quartz
  3. ラスタライズ・データの出力するための Quartz
というの大きく3つの構成要素がある。

例:特殊効果の適用
ColorSync on Mac OS X "Figure 6: Applying Special Effects with Abstract Profile." より転載
データにある効果を適用する場合は Quartz の内部で作業スペースに抽象プロファイルが追加されるというかたちで実現する。また、抽象プロファイル(abstract profile)とは

Abstract profiles allow applications to perform special color effects independent of the devices on which the effects are rendered. For example, an application may choose to implement an abstract profile that increases yellow hue on all devices. Abstract profiles allow users of the application to make subjective color changes to images or graphics objects.

抽象プロファイルはレンダリングされた効果をもたらすデバイスから独立した特殊効果をアプリケーションが実行することを許可します。例えば、関係するすべてのデバイスで黄色の色相を増加する抽象プロファイルを実装するためにアプリケーションは選ぶかもしれません。抽象プロファイルはアプリケーションのユーザに画像やグラフィックの主観的な色を変更することを許可します。


例:ソフト・プルーフ
ColorSync on Mac OS X "Figure 7: Soft-proofing on Primary Display" より転載
同様にプリンタ・プロファイルを作業スペースに追加するというかたちで実現する。プリンタ・プロファイルを定義するすべての色補正は作業スペースに反映されてディスプレイに表示される。


例:自由に変形された色空間
ColorSync on Mac OS X "Figure 8: Free-transform color space used to produce color effects." より転載
複数のプロファイル(multi-profile)からなる色空間の使用はQuartz の内部に限らず、アプリケーションからも使用できる。

という感じです。とりあえずここまで。

2011年6月6日月曜日

イメージを描画する(7)

ことが後先になりますが Core Graphics について。

Core Graphics or Quartz

The Quartz 2D API is part of the Core Graphics framework, so you may see Quartz referred to as Core Graphics or, simply, CG.

といわけで、Quartz(Quartz 2D)と Core Graphics はおおよそ同義で使われます。

何をどうすればいいの?

A graphics context is an opaque data type (CGContextRef) that encapsulates the information Quartz uses to draw images to an output device, such as a PDF file, a bitmap, or a window on a display. The information inside a graphics context includes graphics drawing parameters and a device-specific representation of the paint on the page. All objects in Quartz are drawn to, or contained by, a graphics context.

グラフィクス・コンテキストはQuartz がイメージを PDF ファイル、ビットマップ、ディスプレイ上のウィンドウのような、出力先のデバイスに描画するために使う情報をカプセル化する不透明なデータ型(opaque data type)です。Quartz によるすべてのオブジェクトはグラフィクス・コンテキストに描画され、グラフィクス・コンテキストに含まれます。なるほど。

ところでリファレンスやガイドによく出てくる opaque data type って何でしょうか。わからなかったので、すこし寄り道。

In computer science, an opaque data type is a data type that is incompletely defined in an interface, so that ordinary client programs can only manipulate data of that type by calling procedures that have access to the missing information.

計算機科学において不透明な(opaque)データ型とはインターフェイスで不完全に定義されたデータ型のことです。通常クライアントプログラムは隠蔽されている情報にアクセスをもつプロシージャを呼出すことによってのみデータを操作することができます。

たとえば C でオブジェクト指向を実装しようとした場合に、オブジェクト自体はこの opaque data type にしておいて、プロシージャとともにヘッダで公開する。そして実装はライブラリとかで提供すれば、提供者は利用者に安全に使ってもらえる、利用者は実際のデータ構造が解らなくてもプロシージャを通して便利に使えるってことでしょうか(間違ってたらごめん)。実際に CGContextRef は CGContext.hで

typedef struct CGContext *CGContextRef;

CGContext 構造体を typedef したポインタとしてのみ宣言されていて、その他 CGContextRef を引数にとる CGContext* 関数のプロトタイプが宣言されています。

戻りまして、

When you draw with Quartz, all device-specific characteristics are contained within the specific type of graphics context you use. In other words, you can draw the same image to a different device simply by providing a different graphics context to the same sequence of Quartz drawing routines. You do not need to perform any device-specific calculations; Quartz does it for you.

Quartz を使用して描画するとき、すべてのデバイス固有の特性は使用するグラフィクス・コンテキストの特定の型の中に含まれています。いいかえれば、Quartz の描画ルーチンの同じシーケンスに異なるグラフィクス・コンテキストを提供することによって、異なるデバイスに同じイメージを描くことができます。デバイス固有の計算を実行する必要はありません。Quartz がやってくれます。

つまり、描画先のグラフィクス・コンテキストを取得または作成して、描画ルーチンにコンテキストを渡してやるだけで、あとは Quartz がやってくれるということです。

それでは
同じ描画ルーチンに異なるコンテキストを渡してそれぞれのコンテキストに描画してみます。
  • ビューに描画
  • ビットマップに描画
  • PDF ファイルに描画
Xcode で新規プロジェクトを Cocoa Application オプションは何もチェックせずに作成します。カスタム・ビューを使うので、新規ファイルで subclass of を NSView で追加しました。

ソース

DrawingImage_7_AppDelegate.h
#import <Cocoa/Cocoa.h>

@class CustomView;

@interface DrawingImage_7_AppDelegate : NSObject <NSApplicationDelegate> {
    NSWindow *window;
    CustomView *aView;
}
@property (assign) IBOutlet NSWindow *window;
@property (assign) IBOutlet CustomView *aView;

- (IBAction)createTIFF:(id)sender;
- (IBAction)createPDF:(id)sender;

@end

DrawingImage_7_AppDelegate.m
#import "DrawingImage_7_AppDelegate.h"
#import "CustomView.h"

@implementation DrawingImage_7_AppDelegate

@synthesize window, aView;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
 // Insert code here to initialize your application 
}

- (IBAction)createTIFF:(id)sender
{
    [aView createTIFF];
}
- (IBAction)createPDF:(id)sender
{
    [aView createPDF];
}
@end

CustomView.h
#import <Cocoa/Cocoa.h>

@interface CustomView : NSView {
}
- (void)createTIFF;
- (void)createPDF;
@end

CustomView.m
#import "CustomView.h"


@implementation CustomView

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
    }
    return self;
}

- (void)drawWithContext:(CGContextRef)context
{
    CGContextSaveGState(context);
    
    CGContextSetFillColorSpace(context, [[[self window] colorSpace] CGColorSpace]);
    
    CGRect rect;
    rect.origin = CGPointMake(0.0, 0.0);
    rect.size = CGSizeMake([self visibleRect].size.width, [self visibleRect].size.height);
    
    CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 1.0);
    CGContextFillRect(context, rect);
    
    CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
    CGContextFillRect(context, CGRectMake(0.0, 0.0, 200.0, 100.0));
    
    CGContextSetRGBFillColor(context, 0.0, 0.0, 1.0, 0.5);
    CGContextFillRect(context, CGRectMake(0.0, 0.0, 100.0, 200.0));
    
    CGContextRestoreGState(context);
}

- (void)drawRect:(NSRect)dirtyRect {
    
    CGContextRef viewContext = [[NSGraphicsContext currentContext] graphicsPort];
    
    [self drawWithContext:viewContext];
}

- (void)createTIFF
{
    NSRect viewRect = [self visibleRect];
    size_t width = (size_t)viewRect.size.width;
    size_t height = (size_t)viewRect.size.height;
    
    
    CGContextRef bitmapContext;
    CGColorSpaceRef colorSpace;
    
    colorSpace = [[[self window] colorSpace] CGColorSpace];
    
    bitmapContext = CGBitmapContextCreate(NULL,
                                          width,
                                          height,
                                          8,
                                          width * 4,
                                          colorSpace,
                                          kCGImageAlphaPremultipliedLast);
        
    [self drawWithContext:bitmapContext];
    
    CGImageRef imageRef = CGBitmapContextCreateImage(bitmapContext);
    
    CGContextRelease(bitmapContext);
    
    NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:imageRef];
    
    CGImageRelease(imageRef);
    
    NSData *tiffData = [imageRep TIFFRepresentation];
    
    [tiffData writeToFile:[NSHomeDirectory() stringByAppendingPathComponent:@"Desktop/test.tif"]
               atomically:YES];
    
    [imageRep release];
    
}

- (void)createPDF
{   
    CGRect mediaBox;
    mediaBox.origin = CGPointMake(0.0, 0.0);
    mediaBox.size = CGSizeMake([self visibleRect].size.width, [self visibleRect].size.height);
    
    NSURL *URL;
    CGContextRef PDFContext;
    
    URL = [NSURL fileURLWithPath:[NSHomeDirectory() stringByAppendingPathComponent:@"Desktop/test.pdf"]];
    PDFContext = CGPDFContextCreateWithURL((CFURLRef)URL, &mediaBox, NULL);
    
    CGContextBeginPage(PDFContext, &mediaBox);
    
    [self drawWithContext:PDFContext];
    
    CGContextEndPage(PDFContext);

    CGContextRelease(PDFContext);
}
@end

Pop Up Button のメニューをそれぞれ TIFF と PDF を設定してアクションにそれぞれ接続します。

結果は?
同じルーチンでそれぞれのコンテキスト描画できました。TIFF は問題ありませんでしたが、PDFは色が違いました。Quartz で PDF に描画する場合のカラースペースの設定はどうすればいいのでしょうか。コンテキスト作成時の辞書に NULL を渡しています。ここできちんと情報を渡せばよい気がしますが Auxiliary Dictionary Keys を見て適当に kCGPDFContextOutputIntent や kCGPDFXDestinationOutputProfile を設定して渡してみましたが特に変化せずでしたので保留。


NSRect と CGRect
余談ですが、Release 時のビルドのときに少しはまりましたので。NSSize, NSPoint も同様です。

When building for 64 bit systems, or building 32 bit like 64 bit, NSRect is typedef’d to CGRect.

だそうです。

2011年6月1日水曜日

イメージを描画する(6)

前回の続きで、ヒストグラムの平坦化してみました。ほぼ同じです。データの読込みをCore Graphics でなくて NSBitmapImageRep でおこなっています。また引数を指定してイメージ・データのを指定できるようにしました。いつものように決め打ち、手抜きのだめコードです。

#import <Cocoa/Cocoa.h>
#import <Accelerate/Accelerate.h>

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    if (argc < 3) {
        
        NSLog(@"Too few arguments required for.");
        [pool drain];
        return EXIT_FAILURE;
        
    }
    
    NSMutableDictionary *paths = [NSMutableDictionary dictionary];
    NSFileManager *fManager = [NSFileManager defaultManager];
    
    NSString *path1
        = [[NSString stringWithUTF8String:argv[1]] stringByExpandingTildeInPath];
    
    NSString *path2
        = [[NSString stringWithUTF8String:argv[2]] stringByExpandingTildeInPath];
    
    if ([fManager fileExistsAtPath:path1] == YES) {
        [paths setValue:path1 forKey:@"source"];
        [paths setValue:path2 forKey:@"distination"];
    }
    else {
        NSLog(@"Can not find a source path.");
        [paths removeAllObjects];
        [pool drain];
        return EXIT_FAILURE;
    }
    
    NSData *data = [NSData dataWithContentsOfFile:[paths valueForKey:@"source"]];
    NSBitmapImageRep *bitmapImageRep = [NSBitmapImageRep imageRepWithData:data];
    
    if (!bitmapImageRep) {
        NSLog(@"\"%@ \"format can not be handled."
              ,[[paths valueForKey:@"source"] pathExtension]);
        [paths removeAllObjects];
        [pool drain];
        return EXIT_FAILURE;
    }
    
    NSBitmapFormat alphaFirst, nonPremultiply, floatingPoint;
    NSInteger bpp, bpr, spp, pixelsHigh, pixelsWide;
    BOOL isPlanar, hasAlpha;
    NSColorSpace *colorSpace;
    NSColorSpaceModel colorSpaceModel;

    alphaFirst = [bitmapImageRep bitmapFormat] & NSAlphaFirstBitmapFormat;
    nonPremultiply = [bitmapImageRep bitmapFormat] & NSAlphaNonpremultipliedBitmapFormat;
    floatingPoint = [bitmapImageRep bitmapFormat] & NSFloatingPointSamplesBitmapFormat;
    bpp = [bitmapImageRep bitsPerPixel];
    bpr = [bitmapImageRep bytesPerRow];
    spp = [bitmapImageRep samplesPerPixel];
    pixelsHigh = [bitmapImageRep pixelsHigh];
    pixelsWide = [bitmapImageRep pixelsWide];
    isPlanar = [bitmapImageRep isPlanar];
    hasAlpha = [bitmapImageRep hasAlpha];
    colorSpace = [bitmapImageRep colorSpace];
    colorSpaceModel = [colorSpace colorSpaceModel];
    
    if (colorSpaceModel != NSRGBColorSpaceModel ||
        (bpp/spp) != 8 ||
        (floatingPoint == NSFloatingPointSamplesBitmapFormat) ) {
        
        NSLog(@"A color space is supported 8-bit RGB model only:%@, "
              @"Bits per pixel:%d, "
              @"Samples per pixel:%d, "
              ,[bitmapImageRep colorSpaceName]
              ,bpp
              ,spp);
        
        [paths removeAllObjects];
        [pool drain];
        return EXIT_FAILURE;
    }
    
    // -------------------- vImage_Bufferの作成 --------------------//
    vImage_Buffer org, src, dist;
    vImage_Error error;
    
    org.data = [bitmapImageRep bitmapData];
    org.height = (vImagePixelCount)pixelsHigh;
    org.width = (vImagePixelCount)pixelsWide;
    org.rowBytes = bpr;
    
    src.data = (unsigned char *)calloc(pixelsHigh * pixelsWide * 4, sizeof(unsigned char));
    src.height = (vImagePixelCount)pixelsHigh;
    src.width = (vImagePixelCount)pixelsWide;
    src.rowBytes = pixelsWide * 4;
    
    dist.data = (unsigned char *)calloc(pixelsHigh * pixelsWide * 4, sizeof(unsigned char));
    dist.height = (vImagePixelCount)pixelsHigh;
    dist.width = (vImagePixelCount)pixelsWide;
    dist.rowBytes = pixelsWide * 4;
    

    // RGB,RGBA,ARGB -> ARGB にする。
    if (spp == 3) {
        vImageConvert_RGB888toARGB8888(&org, NULL, 255, &src, FALSE, 0);
    }
    else if (alphaFirst == NSAlphaFirstBitmapFormat){
        
        uint8_t permutMap[4] = {3,0,1,2};
        vImagePermuteChannels_ARGB8888(&org, &src, permutMap, 0);
        
    }
    else {
        
        uint8_t permutMap[4] = {0,1,2,3};
        vImagePermuteChannels_ARGB8888(&org, &src, permutMap, 0);
        
    }
    
    // premultiply されてるものは解除。
    if (nonPremultiply != NSAlphaNonpremultipliedBitmapFormat) {
        vImageUnpremultiplyData_ARGB8888(&src, &src, 0);
    }
    
    // 平坦化
    error = vImageEqualization_ARGB8888(&src, &dist,kvImageLeaveAlphaUnchanged);
    
    // 解除したものをもとに戻す。
    if (nonPremultiply != NSAlphaNonpremultipliedBitmapFormat) {
        vImagePremultiplyData_ARGB8888(&dist, &dist, 0);
    }
    
    if (error != 0) {
        free(src.data);
        free(dist.data);
    }
    
    
    // もういらない
    free(src.data);
    
    //-------------------- 書き出し用の画像を作成 --------------------//
    CGContextRef context
    = CGBitmapContextCreate(dist.data,
                            pixelsWide,
                            pixelsHigh,
                            8,
                            pixelsWide * 4,
                            [colorSpace CGColorSpace],
                            kCGImageAlphaPremultipliedFirst);
    
    CGImageRef cgImage = CGBitmapContextCreateImage(context);
    
    // もういらない
    CGContextRelease(context);
    free(dist.data);
    
    NSBitmapImageRep *imageRep
        = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
    
    // もういらない
    CGImageRelease(cgImage);
    
    NSData *outputData = [imageRep TIFFRepresentation];
    
    [outputData writeToFile:[paths valueForKey:@"distination"]
                 atomically:YES];
    
    
    [paths removeAllObjects];
    [pool drain];
    return 0;
}

結果

Photoshop で確認。左が平坦化前、右が平坦化後のヒストグラムです。扱うイメージ・データの色に偏りがあると大幅に色相が変化します。

Equalization transforms an image so that it has a more uniform histogram. A truly uniform histogram is one in which each intensity level occurs with equal frequency. These functions approximate that histogram.

真に均一なヒストグラムは各強度レベルが同じになるが、vImage のヒストグラム関数は近似する。とあります。なるほど。

2011年5月24日火曜日

イメージを描画する(5)

昨年の出版されたものの話ですが 「photographer's gallery press no.9」 でマイケル・フリード氏のインタビューが掲載されています。聞き手の甲斐氏に答えるかたちで  --前略-- the photographic surface just can never play a really active role.-- と述べています。そこで甲斐氏 -- Do you think that's really true? -- と踏み込み(いいぞ!)ます。このやり取りに興味をおぼえ、氏の著作「 Why Photography Matter s as Art as Never Before」 を読むことにしました。

余談ですが僕の考えは、写真の物質的な表面は真に能動的な役割を常に果たしている、です。

なにするの?
というわけで今日は vImage の畳み込み(Convolution)関数を使って簡単なエンボス・フィルタを作り、写真に擬似的な表面を演出してやろうと思います。vImage はCPUのベクトル演算ユニットを使う命令をはいてくれます。操作は C言語を使います。

Image formats are either planar or interleaved. A planar image format stores image data so that the data for each channel (plane) is in a separate buffer. For example, a typical planar image would have separate buffers for the red, green, blue, and alpha channels. An interleaved image format stores image data so that the data from each pixel alternates: ARGBARGBARGB . . .

vImage で使用できる画像はプレーンで構成されているか、インターリーブされて構成されているもののどちらかになります。また、それぞれ

Data values for images can be integer or floating-point. In vImage, image formats that use integer values represent an intensity level as an 8-bit unsigned value. Values can range from 0 to 255, inclusive, with 255 indicating full intensity and 0 no intensity. Image formats that use floating-point values typically use values in the range of 0.0 (lowest intensity) to 1.0 (full intensity).

0 から 255 までの符号なし整数(unsigned char)か 0.0 から 1.0 までの浮動小数点(float)で構成されている必要があります。そうでないものについては vImage の変換(Conversion)関数を使って変換してやります。

C ばかり
今回はインターリーブ、符号なし整数の決め打ちでいきます。新規プロジェクトを Command Line Tool、Foundation を選択して、適当なところに適当な名前で作成します。個別にフレームワークを読込むのが手間なので Founfdation フレームワークを削除してから、Cocoa, Accelerate フレームワークを追加してそれぞれインポートしました。デスクトップに image.tif というファイルがあることを仮定してます。

#import <Cocoa/Cocoa.h>
#import <Accelerate/Accelerate.h>

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
    //---------- 画像のURLを取得 image.tif がデスクトップにあると仮定します。----------/
    NSString *path
        = [NSHomeDirectory() stringByAppendingPathComponent:@"Desktop/image.tif"];
    NSURL *imageURL = [NSURL fileURLWithPath:path];
    
    // imageURL にある画像からソースを作成。
    CGImageSourceRef sourceRef
        = CGImageSourceCreateWithURL((CFURLRef)imageURL, NULL);
    
    // ソースが作られなければ終了。
    if (sourceRef == NULL) {
        return EXIT_FAILURE;
    }
    
    // ソースからイメージを作成
    CGImageRef imageRef
        = CGImageSourceCreateImageAtIndex(sourceRef,0,NULL);
    
    // もういらない。
    CFRelease(sourceRef);

    // イメージが作成できなければ終了。
    if (imageRef == NULL) {
        
        return EXIT_FAILURE;
    }

    //-------------------- イメージの情報を取得 --------------------//
    size_t bpr                          = CGImageGetBytesPerRow(imageRef);
    size_t height                       = CGImageGetHeight(imageRef);
    size_t width                        = CGImageGetWidth(imageRef);
    CGColorSpaceRef colorSpace          = CGImageGetColorSpace(imageRef);
    CGColorSpaceModel colorSpaceModel   = CGColorSpaceGetModel(colorSpace);
    CGBitmapInfo bitmapInfo             = CGImageGetBitmapInfo(imageRef);
    CGImageAlphaInfo alphaInfo          = bitmapInfo & kCGBitmapAlphaInfoMask;
    
    CGBitmapInfo infoMask = kCGBitmapFloatComponents |
                                                      kCGBitmapByteOrderMask;
    
    // 今回はごめん。
    if ((bitmapInfo & infoMask) > 0 ||
        colorSpaceModel != kCGColorSpaceModelRGB) {
        
        CGImageRelease(imageRef);
        return EXIT_FAILURE;
    }
        
    //-------------------- ビットマップを取得する --------------------//
    CGDataProviderRef provider  = CGImageGetDataProvider(imageRef);
    NSData *tmpData = (NSData *)CGDataProviderCopyData(provider);
    
    // もういらない
    CGImageRelease(imageRef);
    
    unsigned char *bitmapData
        = (unsigned char *)malloc([tmpData length] * sizeof(unsigned char));
    
    [tmpData getBytes:bitmapData length:[tmpData length]];
    
    // もういらない
    [tmpData release];
    
    
    // -------------------- vImage_Bufferの作成 --------------------//
    vImage_Buffer org, src, dist;
    vImage_Error error;
    
    org.data = bitmapData;
    org.height = (vImagePixelCount)height;
    org.width = (vImagePixelCount)width;
    org.rowBytes = bpr;
    
    src.data = (unsigned char *)calloc(width * height * 4, sizeof(unsigned char));
    src.height = (vImagePixelCount)height;
    src.width = (vImagePixelCount)width;
    src.rowBytes = width * 4;
    
    dist.data = (unsigned char *)calloc(width * height * 4, sizeof(unsigned char));
    dist.height = (vImagePixelCount)height;
    dist.width = (vImagePixelCount)width;
    dist.rowBytes = width * 4;

    // RGB,RGBA,ARGB -> ARGB にする。
    if (alphaInfo == kCGImageAlphaNone) {
        vImageConvert_RGB888toARGB8888(&org, NULL, 255, &src, FALSE, 0);
    }
    else if (alphaInfo == kCGImageAlphaLast ||
             alphaInfo == kCGImageAlphaPremultipliedLast ||
             alphaInfo == kCGImageAlphaNoneSkipLast){
        
        uint8_t permutMap[4] = {3,0,1,2};
        vImagePermuteChannels_ARGB8888(&org, &src, permutMap, 0);
        
    }
    else if (alphaInfo == kCGImageAlphaFirst ||
             alphaInfo == kCGImageAlphaPremultipliedFirst ||
             alphaInfo == kCGImageAlphaNoneSkipFirst){
        
        uint8_t permutMap[4] = {0,1,2,3};
        vImagePermuteChannels_ARGB8888(&org, &src, permutMap, 0);
        
    }
    
    // premultiply されてるものは解除。
    if (alphaInfo == kCGImageAlphaPremultipliedFirst ||
        alphaInfo == kCGImageAlphaPremultipliedLast) {
        
        vImageUnpremultiplyData_ARGB8888(&src, &src, 0);

    }
    
    
    // もういらない
    free(bitmapData);
    
    // -------------------- 畳み込みの計算 --------------------//
    // 3 x 3 カーネルを作成
    int16_t kernel[9] = {
        -2, -2,-2,
        -2, 1, 2,
        2, 2, 2
    };
    
    // 除数の作成
    int32_t divisor = 0;
    for (NSInteger i = 0; i < 9; ++i) {
        divisor += kernel[i];
    }
    
    // バックグランド・カラーの作成(使わない)
    Pixel_8888 bgColor = {0,0,0,0};
    
    // 畳み込みの計算
    error = vImageConvolve_ARGB8888(&src,
                                    &dist,
                                    NULL,
                                    0,
                                    0,
                                    kernel,
                                    3,
                                    3,
                                    divisor,
                                    bgColor,
                                    kvImageEdgeExtend +
                                    kvImageLeaveAlphaUnchanged);
    
    
    
    // エラーがでたら終了
    if (error < 0) {
        free(src.data);
        free(dist.data);
        return EXIT_FAILURE;
    }

    // premultiply されてたものは元にもどす。
    if (alphaInfo == kCGImageAlphaPremultipliedFirst ||
        alphaInfo == kCGImageAlphaPremultipliedLast) {
        
        vImagePremultiplyData_ARGB8888(&dist, &dist, 0);

    }

    // もういらない。
    free(src.data);
    
    //-------------------- 書き出し用の画像を作成 --------------------//
    CGContextRef context
        = CGBitmapContextCreate(dist.data,
                                width,
                                height,
                                8,
                                width * 4,
                                colorSpace,
                                kCGImageAlphaPremultipliedFirst);
    
    CGImageRef cgImage = CGBitmapContextCreateImage(context);
    
    // もういらない
    CGContextRelease(context);
    
    NSBitmapImageRep *imageRep
        = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
    
    // もういらない
    CGImageRelease(cgImage);
    free(dist.data);
    
    NSData *outputData = [imageRep TIFFRepresentation];
    
    NSString *pathToWrite
        = [NSHomeDirectory() stringByAppendingPathComponent:@"Desktop/out.tif"];
    
    [outputData writeToFile:pathToWrite
                 atomically:YES];
    
    [imageRep release];
    
    [pool drain];
    return EXIT_SUCCESS;
}

これをビルドして、コマンドラインから実行するとデスクトップに out.tif ができました。エンボスもかかってます。

まず画像情報を取得して 8ビット整数でないもの、RGB でないものは除外しました。それからビットマップ・データを作成してます。CG* 関数をいろいろ使いましたが、NSBitmapImageRep を使っても同様のことができます。今回はなんとなくです。

2011年5月4日水曜日

イメージを描画する(4)

シャッターの巻き上げが壊れました。カメラを修理に出すことにします。

ところで Core Image は OpenGL を基盤とする画像処理技術です。静止画、動画、なにやかにやいろいろとやってくれます。とりあえず Core Image カーネル言語を試してみたいので簡単な方法でやります。

ぼかす
Quartz Composer を ”Basic Composition” で開いてパッチを並べます。[Core Image Filter] のラベルを ”Blur” と変更しています。同じ名前のパッチがありますが、それじゃありません。適当なイメージも用意して [image] にインポートしてあります。


インスペクタ・パネルを表示して [Core Image Filter] にコード記述します。


やったこと
coord_0 〜 coord_8 は現在処理中のピクセルを中心に各座標を取得しています。dist_0 〜 dist_8 は取得した座標をもとにそれぞれのピクセルのサンプルを取得しています。Core Image に渡される色成分はアルファ値が事前にかけ合わ(premultiply)されていることを前提としている(そうでないものについてはアルファ値が1.0だと推定して動作する)ので、それを unpremultiply() で解除してやります。dist で取得したサンプルの平均をとって、アルファ値をかけ合わせた値を返してやります。こうすることで、隣り合うピクセル同士のサンプルの差が小さくなり、結果としてぼけるのです。

Core Image is designed for two types of developers—filter clients and filter creators. If you plan only to use Core Image filters, you are a filter client. If you plan to write your own filter, you are a filter creator..

Core Image はフィルタを使う人と作る人のために設計されているとのことです。使う人は CI* オブジェクトを使ってごにょごにょすればよしです。作りたい人はカーネル・コードを書きます。

Core Image Kernel Language Reference には Core Image カーネル言語固有の部分しか書いてありません。その他リファレンスに書いてある 除外項目を除く GLSL のサブセットを使う事ができます。パッチで書いた vec2() は GLSL のベクトル・コンストラクタで、要素が2つのベクトルを生成します。

その他別件で。パッチ内では if とか for とかが使えませんでしたが、

Statements: continue, break, discard. Other flow control statements (if, for, while, do while) are supported only when the loop condition can be inferred at the time the code compiles.

コードをコンパイルするときにループ条件を推定できればサポートされるよ、とありますので場合によってはいけるのかもです。とりあえずここまで。

2011年5月3日火曜日

イメージを描画する(3)

ふと公園の芝生で寝っころがりながら、ぼんやりと夢想に耽る。写真家にもそんな気分のときってあると思います。とゆうわけで、描いた夢を具体的にするために Cocoa にはこんなクラスがあります。

The NSGraphicsContext class is the programmatic interface to objects that represent graphics contexts. A context can be thought of as a destination to which drawing and graphics state operations are sent for execution. Each graphics context contains its own graphics environment and state.
私家版 描画のおさらい
Cocoa の描画はビューを基本(view-based)に行われます。なのでビューへ描画する場合は通常 NSView のサブクラスを作成して drawRect: をオーバーライドします。そして再描画が必要な場合は(ウィンドウサイズが変更されたりとか)メインスレッドのイベントループのなかで自動に drawRect: を呼出します。drawRect: が呼出されているときは Cocoa がすでにそのビューへの描画環境を整えていてくれているので、

The receiver can assume the focus has been locked and the coordinate transformations of its frame and bounds rectangles have been applied; all it needs to do is invoke rendering client functions.

ビューへフォーカスがロックされていることや、座標系がそのビューであることを仮定できます。そしてこの再描画が終わると描画環境を再びもとに戻して(ありがとう)くれています。

この drawRect: を直接自分で呼出すことはあまりありません。プログラムのタイミングで再描画が必要になった場合には setNeedsDisplay: に YES を渡して再描画が必要なことを知らせてイベントループの中で再描画を要求するか、displayIfNeeded ですぐに再描画をおこないます。その他の要求するタイミングによっていくつかある display〜 メソッドを使い分けます。

イメージを描画する(1)では再描画がオープンパネルから新しいイメージを指定したときにも行われて欲しいので drawImage を drawRect: の外(setBitmapImageRepWithData:)からも呼んでます。なので lockFocus, unlockFocus を呼出して、描画環境を整えてやる必要がありました。対してイメージを描画する(2)では drawRect: 内なのでロックする必要がありません。

詳しい内容につきましては ”Cocoa Drawing Guide”, ”View Programming Guide” をご参照ください。

どうする?
いままでは bitmapData でポインタを取得して、あれやこれやと行っていました。その他に setColor:atX:y: setPixel:atX:y で各ピクセルにアクセスできますが、これも手間はさほどかわりません。いまは描いた夢をいっきに具体的にしたいのです。

それじゃ NSBitmapImageRep をレシーバに lockFocus, unlockFocus を使えばいいんじゃない?そうだよ、写真家さん、そうしちゃいなよ!...残念なことに NSImage では使えますが、NSBitmapImageRep にはそれがありません。でも写真家はそうゆう星のもとに生まれた人間なので(じゃNSImage 使えよ)こんなことではめげません。そこで NSGraphicsContext を使うのです。

冒頭のリファレンスの通り NSGraphicsContext は描画環境(以下グラフィクス・コンテキスト)を表すオブジェクトへのインターフェイスです。ここを通してどうこうしてやれば NSBitmapImageRep に描画できると期待します。

こうする
Graphics contexts are maintained on a stack. You push a graphics context onto the stack by sending it a saveGraphicsState message, and pop it off the stack by sending it a restoreGraphicsState message. By sending restoreGraphicsState to an NSGraphicsContext object you remove it from the stack, and the next graphics context on the stack becomes the current graphics context.

グラフィクス・コンテキストはスタック上に保持されていて、saveGraphicsState でスタックにプッシュされ restoreGraphicsState でポップされる。restoreGraphicsState を NSGraphicsContext に送信してスタックから削除しときなさいよと。そうしとけばスタック上の次のグラフィクス・コンテキストがカレントになりますよ。ということでしょうか。

手順は
現在のグラフィクス・コンテキストをスタック上にポップしておいて、その間に NSBitmapImageRep に描画できるようなコンテキストをカレントに設定する。描画が終わったら restoreGraphicsState を呼んで削除する。削除されたら次のグラフィクス・コンテキストが戻ってくる。

じゃあ NSBitmapImageRep に描画できるようなコンテキストはどうする?

+ (NSGraphicsContext *)graphicsContextWithBitmapImageRep:(NSBitmapImageRep *)bitmapRep

ありました。これでちょちょいのちょいです。
では手順を。
  1. ビューから NSBitmapImageRep のインスタンスを取得する。
  2. 生成したビットマップ・イメージに描画するためのパスを生成する。
  3. ビットマップ・イメージのグラフィクス・コンテキストを取得する。
  4. グラフィクス・コンテキストをスタック上にポップする。
  5. グラフィクス・コンテキストを変更する。
  6. ビットマップ・イメージにパスを描画する。
  7.  グラフィクス・コンテキストを元に戻す。
  8. ビットマップ・イメージをビューに描画する。
例によってウインドウにカスタム・ビューを貼付けてあります。

#import "CustomView.h"

NSBitmapImageRep *bitmapImageRep;

@implementation CustomView

NSBitmapImageRep *bitmapImageRep;

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
 if (self) {
        bitmapImageRep = nil;
    }
    return self;
}

- (void)drawRect:(NSRect)dirtyRect {
 
 if (bitmapImageRep) {
  [bitmapImageRep release];
  bitmapImageRep = nil;
 }
 
 // CustomView からbitmapImageRep を取得する。
 NSRect rect = [self bounds];
 bitmapImageRep = [[self bitmapImageRepForCachingDisplayInRect:rect] retain];
 
 // bitmapImageRep へ描画するためのパスを作成する。
 NSBezierPath *bezierPath = [NSBezierPath bezierPath];
 [bezierPath moveToPoint:NSMakePoint(0.0, cos(2.0 * M_PI) * rect.size.height)];

 NSPoint amplitude;
 double radians = 2.0 * M_PI / rect.size.width;
 double ampAdjuster = rect.size.height / 2.0;
 NSInteger phase;
 
 for (phase = 0; phase < rect.size.width; phase++) {
  amplitude = NSMakePoint(phase, (cos(radians * phase) * ampAdjuster ) + ampAdjuster);
  [bezierPath lineToPoint:amplitude];
 }
 
 // bitmapImageRep のグラフィクス・コンテキスト取得
 NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmapImageRep];
 
 // 取得したコンテキストが nil でなければ描画する。
 if (context) {
  
  // 現在のグラフィクス・コンテキストを保存
  [NSGraphicsContext saveGraphicsState];
  
  // 現在のグラフィクス・コンテキストを bitmapImageRep へのコンテキストへとセットする。
  [NSGraphicsContext setCurrentContext:context];
  
  // 下地を塗る。
  [[NSColor whiteColor] set];
  NSRectFill(rect);
  
  // パスを描く。
  [[NSColor redColor] set];
  [bezierPath stroke];
  
  // 現在のグラフィクス・コンテキストを削除し、元に戻す。
  [NSGraphicsContext restoreGraphicsState];
 }
 
 // ビューに描画する。
 [bitmapImageRep drawInRect:rect];
}

- (void) dealloc
{
 [bitmapImageRep release];
 [super dealloc];
}

@end


Cosine Wave !
今回はビューからビットマップを取得してそれをまたビューに描画しています。写真家はいつも遠回りな人生なのでこうゆうのは慣れっこです。そうです僕が芝生の上で寝っころがりながらみたものは Cosine Wave でした。きれいなコサイン波が描けました。

手順1はメソッド名の通り本来キャッシュのために使用するもののようです。autorelease された NSBitmapImageRep か作成できなければ nil が返ってきます。なので nil をチェックするべきでしょうがしていません。

手順2でコサイン波を NSBezierPath で作成してます。moveToPoint でスタートの位置を決めています。それからループの中で次に線を引くポイントを決め、そこに向けて線を引いて、を繰り返してます。Cocoa の描画モデルは Cocoa Drawing Guide ”The Painter Model” で説明されている通り。いまはまだ図形を作成しただけの状態です。

手順3〜5は前述の通り。”NSGraphicsContext”, ”Cocoa Drawing Gude” にはよく saveGraphicsState と restoreGraphicsState をのバランスを必ずとってね!と書いてあります。

手順6で手順2で作成したパスを実際にグラフィックとして描画しています。パスは適当なグラフィクス・コンテキストのなかで、fill や stroke メッセージを送信してやるとそこに描画されます。ここではグラフィクス・コンテキストが変更されていますので、ビットマップに描画しています。

手順7〜8はグラフィクス・コンテキストを元に戻し、ビューに描画してます。

まだまだ NSGraphicsContext
グラフィクス・コンテキストがもっているグラフィクスの状態(state)をみてみます。

Current
transformation
matrix (CTM)
特定のビューの座標系から描画先のデバイスへの座標系へ変換のために指定する。Cocoa はビューの drawRect: を呼出す前に CTM を変更します。CTM の変更は NSAffineTranceform オブジェクトを使います。原点、尺度、回転の座標系を変更できます。
Clipping area描画されるときに塗りつぶされる描画指定領域を指定する。Cocoa はビューの drawRect: を呼出す前にクリッピング・エリアを可視の領域(visible area)に変更します。NSBezierPath オブジェクトを使って変更できます。
Line widthパスの幅を指定する。デフォルトの幅は1.0です。NSBezierPath オブジェクトを使って変更できます。
Line join style2つの線の結合スタイルを指定する。デフォルトのスタイルは NSMiterLineJoinStyle です。NSBezierPath オブジェクトを使って変更できます。
Line cap Styleパスの線端のスタイルを指定する。デフォルトのスタイルは NSButtLineCapStyle です。NSBezierPath オブジェクトを使って変更できます。
Line dash style破線のパターンを定義する。デフォルトはなく、実線になります。NSBezierPath オブジェクトを使って変更できます。
Line miter limit線の結合スタイルを NSMiterLineJoinStyle に設定したときのみ適用され、その限度を決定する。結合部分の角は線の幅で決まります。限度を超える場合に角が切り取られます。デフォルトの限度は10.0です。NSBezierPath を使って変更できます。
Flatness value曲線が描かれるときの精度を指定する。ピクセル単位で計られる最大許容差範囲です。値が小さくなれば曲線が滑らかに描かれますが、計算コストが高くつきます。同じ値でもデバイスによっては解釈がわずかに異なるかも知れません。デフォルトの値は0.6です。NSBezierPath を使って変更できます。
Stroke color線が描かれるときのカラーを指定する。システムがサポートするカラースペースのカラーが使えます。この値はアルファ値の情報を含んでいます。カラーの情報は NSColor で管理されます。
Fill color特定の範囲を塗りつぶすカラーを指定する。システムがサポートするカラースペースのカラーが使えます。この値はアルファ値の情報を含んでいます。カラーの情報は NSColor で管理されます。
Shadow描かれる内容に適用するシャドウの属性を指定する。設定は NSShadow クラスを使っておこないます。
Rendering intent設定されているカラー・スペースをを現在のカラー・スペースにマッピングする方法を指定する。Cocoa では直接この属性を設定できません。Quartz を使ってください。
Font nameテキストを描画するとき使用するフォントを指定する。フォント情報は NSFont クラスを使って変更できます。
Font sizeテキストを描画するとき使用するフォント・サイズを指定する。フォント情報は NSFont クラスを使って変更できます。
Font character spacingテキストを描画するとき使用する字間(character spacing)を指定する。Cocoa でこの属性は間接的にしかサポートされていません。
Text drawing modeテキストをどのように描画するかを指定する。Cocoa でこの属性は間接的にしかサポートされていません。
Image interpolation quality画像のサイズを変更した場合の補完の処理の仕方を指定する。NSGraphicsContext クラスでこの設定を変更できます。
Compositing operationソースと描画先のイメージのブレンド処理の仕方を指定する。Cocoa でサポートされるブレンド・モードは Quartz と関連がありますが、使用方法と動作が異なります。NSGraphicsContext クラスでデフォルト値を指定できます。
Global alpha透明度を指定する。Cocoa は直接的にこの属性をサポートしてません。値の変更は Quartz の CGContextSetAlpha を使用しなければなりません。
Anti-aliasing settingアンチエイリアシングを指定する。NSGraphicsContext クラスでこの設定を変更できます。

ウィンディングの規則(winding rule:開いたパス線が交わるときに囲む領域の処理の仕方)は現在のグラフィクス・コンテキストには保存されません。NSBezierPath オブジェクトでデフォルト値を設定してください、とのこと。今回使った NSColor の set はStroke Color と Fill Color の両方を設定します。それぞれ別に設定する場合は、setStroke, setFill です。

2011年4月30日土曜日

イメージを描画する(2)

あしびきの山鳥の尾のしだり尾の長々しメソッドをひとりかもせむ、写真家にもそんな気分のときってあると思います。というわけで前回ご紹介した長々しメソッドを使ってみたいと思います。

If a coverage (alpha) plane exists, a bitmap’s color components are premultiplied with it. If you modify the contents of the bitmap, you are therefore responsible for premultiplying the data. For this reason, though, if you want to manipulate the actual data, an NSBitmapImageRep object is not recommended for storage. If you need to work with data that is not premultiplied, you should use Quartz, specifically CGImageCreate with kCGImageAlphaLast.
NSBitmapImageRep Class Reference "Alpha PreMultiplication"

アルファ・プレーンがある場合、ビットマップの色成分は事前にかけ合わされます。なのでビットマップの色成分を変更する場合はデータを手前で事前にかけ合わせろと。だから NSBitmapImageRep はビットマップ・データの操作を前提としたストレージにはむいていない(手間だもんね)。そうゆう場合には Quartz をつかってね。ということでしょうか、Quartz、Cでばりばり。そのうちやりましょう。



プレーン(plane)って?
分からなかったのでメモしときます。ビットマップ画像を構成成分の重ね合わせとしてみることができます。このとき重ね合わされている成分それぞれををプレーンと呼びます。構成成分を色成分でみたとき、カラー・プレーンと呼ばれます。正確な表現ではありませんが、イメージとしては Photoshop の RGB(CMYK)の各チャンネルで表現されているもの、みたいな感じです。その他にビットの深さを基準にみたビット・プレーンもあります。ビット・プレーンを利用した電子透かしの原理など、知りませんでしたので目から鱗が落ちました。


では NSBitmapImageRep はどんなときに使うのでしょうか。リファレンスのメソッド群を眺めますとプログラムからビットマップ・データを作成するというのが一番の使い方のような気がしますが。だれか教えてくださいませ。


下記の説明は画像工学が専門でない僕の理解に基づいているので間違っている可能性があります。その場合ご指摘いただけたら幸いに存じます。?と思ったら NSBitmapImageRep Class Reference を直接ご参照ください。

initWithBitmapDataPlanes:(unsigned char **)planes
                              pixelsWide:(NSInteger)width
                               pixelsHigh:(NSInteger)height
                        bitsPerSample:(NSInteger)bps
                     samplesPerPixel:(NSInteger)spp
                                  hasAlpha:(BOOL)alpha
                                     isPlanar:(BOOl)isPlanar
                    colorSpaceName:(NSString *)colorSpaceName
                         bitmapFormat:(NSBitmapFormat)bitmapFormat
                            bytesPerRow:(NSInteger)rowBytes
                               bitsPerPixel:(NSInteger)pixelBits
(unsigned char **)planes
  • イメージ・データのバッファを指定。プレーン構成(plane configuration)の場合それぞれのバッファに1つのプレーンがあり、ひとつのプレーンには1つの成分があり、
  •  
  • R1R2R3R4R5...G1G2G3G4G5...B1B2B3B4B5...(A1A2A3A4A5...)
  •  
  • の順番になります。
  • アルファ・プレーンが存在する場合は例によって、事前にかけ合わせろと。isPlanar が NO のときはメッシュ構成(meshed configuration)
  •  
  • R1G1B1(A1)R2G2B2(A2)R3G3B3(A3)...
  •  
  • で、先頭のバッファのみが読込まれます。
  •  
  • NULL(または NULL の配列)を設定するとオブジェクトが適当なメモリを確保してくれます。確保されたメモリはオブジェクトに所有され、オブジェクトを解放すれば同時に解放されます。NULL でない場合はオブジェクトはただこのバッファを参照するだけで、変更不可なものとみなします。オブジェクトを解放してもこのバッファは解放されません。この場合は別に自分で解放する必要があります。
(NSInteger)width
  • 作成したい画像の横幅ピクセル
(NSInteger)height
  • 作成したい画像の高さピクセル
(NSInteger)bps
  • 1ピクセル中の1つの成分が何ビットで構成されているかの指定です。サンプルあたりのビット数。すべての成分でサンプルごとに同じビットを持っていると仮定されます。例えば256階調ならば8。
  •  
  • 指定できるビット:1, 2, 4, 8, 12, 16 のどれか。
(NSInteger) spp
  • データの成分の数またはピクセルあたりのサンプル数。意味を持つのは1〜5まで。アルファ値をもったCMYKならば5。RGBならば3。
(BOOL)alpha
  • YES:アルファ値をもっている
  • NO:もっていない
(BOOl)isPlanar
  • YES:プレーン構成
  • NO:メッシュ構成
(NSString *)colorSpaceName
  • 次のうちどれか。bps で 12 を指定している場合はモノクロのカラースペースは指定できません。
  •  
  • NSCalibrateWhiteColorSpace
  • NSCalibrateBlackColorSpace
  • NSCalibrateRGBColorSpace
  • NSDeviceWhiteColorSpace
  • NSDeviceBlackColorSpace
  • NSDeviceRGBColorSpace
  • NSDeviceCMYKColorSpace
  • NSNamedColorSpace
  • NSCustomColorSpace
(NSBitmapFormat)bitmapFormat
  • 次の3つ。Cのビット演算でORして使ってね、とのこと。
  •  
  • NSAlphaFirstBitmapFormat = 1 << 0
  • NSNonPremultipliedBitmapFormat = 1 << 1
  • NSFloatingPointSamplesBitmapFormat = 1 << 2

  • NSAlphaFirstBitmapFormat が0ならば R1G1B1A1... とアルファ値が最後に置かれます。
  • NSNonPremultipliedBitmapFormat が0ならばアルファ値は事前にかけ合わされています。
  • NSFloatingPointSamplesBitmapFormat が0ならば整数です。
(NSInteger)rowBytes
  • width が実際に何バイトで構成されているかの指定。width を使って正確に計算した値でもよいですが、コンピュータはワードごと(16ビットとか32ビットなど CPU に依存します。)の方が読み出しが速いので、画像にしない部分に余分なデータをもたせることがあります。バイト数をあらかじめオブジェクトに教えてやるのはこのためです。0を指定してやりますと、パフォーマンスが最適になるような値を生成してくれます。
(NSInteger)pixelBits
  • 1ピクセルが実際に何ビットで構成されているかの指定。通常はプレーン構成ならば bps 、メッシュ構成ならば "bps * spp" に等しくなります。これもまたパフォーマンス上の理由でそうしない場合があります。例えば RGB で8bps、3pps のメッシュ構成でビット列が構成されていると仮定しますと通常、
  •  
  • R1G1B1R2G2B2R3G3B3...
  •  
  • となっていますが、32ビットごとの読み出しが速いこと(システムに依存します)を考慮して、
  •  
  • R1G1B1Empty1R2G2B2Empty2R3G3B3Empty3...
  •  
  • とする場合があります。この場合は24ビットではなく32ビットになります。
  • 0を指定してやると余分なデータなしで bps と spp の値でピクセルあたりのビット数を解釈します。


前置きがものすごくながくなりましたが、ここからが本番です。
手順は、
  1. ビットマップ・イメージ(99 * 99, 8ビット RGB )を作成する。
  2. 設定した通りにできているかビットマップ・データに何か色を設定する。
  3. ちゃんと設定されたたか View に描画してみてる。
  4. TIFF で書き出してみる。
です。ウィンドウにカスタム・ビューを貼付けてあります。

#import "CustomView.h"

NSBitmapImageRep *bitmapImageRep;

@implementation CustomView

- (id)initWithFrame:(NSRect)frame
{

    self = [super initWithFrame:frame];

    if (self) {

        bitmapImageRep = nil;

    }

    return self;
}
- (void) dealloc
{
 [bitmapImageRep release];
 [super dealloc];
}

- (void)drawImage
{
 if (bitmapImageRep)
  return;  
 
 // 中途半端にして bytesPerRow の最適化をみてみる。
 NSInteger pixelsWide = 99;
 NSInteger pixelsHigh = 99;
 
 // 手順1
 bitmapImageRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
                pixelsWide:pixelsWide
                pixelsHigh:pixelsHigh
                bitsPerSample:8
              samplesPerPixel:3
                  hasAlpha:NO
                  isPlanar:NO
               colorSpaceName:NSCalibratedRGBColorSpace
                 bitmapFormat:0
               bytesPerRow:0
                 bitsPerPixel:0];
 
 // 手順2
 unsigned char* bitmapData = [bitmapImageRep bitmapData];
 NSInteger spp = [bitmapImageRep samplesPerPixel];
 NSInteger bpr = [bitmapImageRep bytesPerRow];
 
 NSInteger width, height;
 for (width = 0; width < pixelsWide; ++width) {
  
  for (height = 0; height < pixelsHigh; ++height) {
   
   // ビットマップ・データに値を設定。緑と青縞模様。
   if ((height & 1)) {
    *(bitmapData + (width * spp) + (height * bpr) + 1) = 255;    
   }
   else {
    *(bitmapData + (width * spp) + (height * bpr) + 2) = 255;    
   }
  }
 }
}

- (void)drawRect:(NSRect)dirtyRect
{

 [self drawImage];
 
 CGFloat pixelsWide, pixelsHigh;
 pixelsWide = (CGFloat)[bitmapImageRep pixelsWide];
 pixelsHigh = (CGFloat)[bitmapImageRep pixelsHigh];
 
 // 手順3
 [bitmapImageRep drawInRect:NSMakeRect(0.0, 0.0, pixelsWide, pixelsHigh)];

}

-(void)saveImage
{

 // 手順4
 NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"Desktop/test.tif"];

 NSData *imageData = [bitmapImageRep TIFFRepresentation];

 [imageData writeToFile:filePath atomically:YES];

}
@end

手順1で作成した ビットマッピ・イメージは僕の環境では bytePerRow が 297バイトではなく 320バイトになっていました。bitsPerPixel で0を渡しているので、サンプルごとのパディングはなく、各行の最後 23バイトは余分なデータだということが分かります。
 
手順2でビットマップデータに直接値を設定しています。

手順3、手順4はそのままなので、特に説明はありません。


initWithBitimapDataPlanes:〜に渡す値は意外に NULL とか 0とか定数なのでらくちんでした。これでがしがしビットマップ・イメージが作れます。

2011年4月28日木曜日

イメージを描画する(1)

たまにはイメージをバイト列でごりごり扱いたい、写真家にもそんな気分のときってあると思います。

The NSBitmapImageRep class renders an image from bitmap data. Bitmap data formats supported include GIF, JPEG, TIFF, PNG, and various permutations of raw bitmap data.

というわけで、そんな僕のために Apple は NSBitmapImageRep を用意してくれていました。生のビットマップ・データをあつかって1ピクセルずつ描画しようというわけです。ふつうはこんな方法でイメージを描画いたしません(描画にものすごく時間がかかります)のであしからず。なのでお急ぎの方はご遠慮くださいませ。

手順は
  1. NSBitmapImageRep からビットマップ・データを抽出。
  2. イメージ・データが持っている情報をもとに1ピクセルのカラーを作成。
  3. 作成したカラーを設定して View へ1ピクセル描画する。
  4. 2と3を繰り返す。
ウィンドウにカスタム・ビューを設定してあります。AppDelegate からオープンパネルを開き、指定したファイルから NSData のインスタンスを作ます。それを setBitmapImageRepWithData: でカスタムビューに送り、それが NSBitmapImageRep で扱うことができる場合に NSBitmapImageRep のインスタンスを生成するようにしてあります。

#import "CustomView.h"

NSBitmapImageRep *bitmapImageRep;

@implementation CustomView

- (id)initWithFrame:(NSRect)frame {
 
    self = [super initWithFrame:frame];
    if (self) {
        bitmapImageRep = nil;
    }
 
    return self;
}

- (void) dealloc
{ 
 [bitmapImageRep release];
 [super dealloc];
}

- (void)drawRect:(NSRect)dirtyRect
{
    [self drawImage];
}

- (void)drawImage
{
 
 if (!bitmapImageRep)
  return;
 
 //手順1
 unsigned char *bitmapData = [bitmapImageRep bitmapData];
// 手順2
 NSInteger bpr = [bitmapImageRep bytesPerRow];
 NSInteger bppPbps = [bitmapImageRep bitsPerPixel] / [bitmapImageRep bitsPerSample];
 
 NSUInteger pixelsWide, pixelsHigh, indexW, indexH, indexC;
 pixelsWide = [bitmapImageRep pixelsWide];
 pixelsHigh = [bitmapImageRep pixelsHigh];

 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
 
 [self lockFocus];
 
 for (indexW = 0; indexW < pixelsWide; indexW++ ) {
  
  for (indexH = 0; indexH < pixelsHigh; indexH++) {
   
   CGFloat *components = calloc(bppPbps, sizeof(CGFloat));
   
   for (indexC = 0; indexC < bppPbps; indexC++) {
    
    *(components + indexC) = (*(bitmapData + (indexW * bppPbps) + (indexH * bpr) + indexC)) / 255.0;
    
   }   
   [[NSColor colorWithColorSpace:[bitmapImageRep colorSpace]
          components:components
         count:bppPbps] set];
   // 手順3
   NSRectFill(NSMakeRect(indexW, (pixelsHigh - indexH), 1, 1));
   
   free(components);
  }
  
 }
 
 [self unlockFocus];
 [pool release];
 
}

- (BOOL)setBitmapImageRepWithData:(NSData *)imageData
{
 if (bitmapImageRep) {
  [bitmapImageRep release];
  bitmapImageRep = nil;
 }
 
 NSString *className = [[NSImageRep imageRepClassForData:imageData] className];
 
 if ([className isEqualToString:@"NSBitmapImageRep"] == YES) {
  
  bitmapImageRep = [[NSBitmapImageRep imageRepWithData:imageData] retain];
  
  [self drawImage];
  return YES;
  
 }
 
 return NO;
 
}
@end


手順1でビットマップ・データのポインタを取得しています。手順2のループのコードから分かるように、データが1次元でのみ構成されていると仮定してます。データが(例えば)カラー・プレーンで構成されている場合は isPlanar の戻り値をチェックして、それに応じて読み込む必要があります。ここはとりあえず決め打ちします。

手順2で bppPbps を計算するのはイメージ・データによっては適当にパディングされ、必ずしも

bitsPerPixel = bitsPerSample * samplesPerPixel

にならないためです。
ループ中の

*(components + indexC) = (*(bitmapData + (indexW * bppPbps) + (indexH * bpr) + indexC)) / 255.0;

は bitmapData からカラー・コンポーネントを取得しています。コンポーネントのフォーマットが整数で8ビットであることを仮定しています。ここも bitmapFormat の戻り値をチェックすると整数か小数か確認できますが決め打っています。NSColor が0〜1でカラー情報をもつので 255.0(8ビットを仮定しない場合はpow(2,[bitmapImageRep bitsPerSample]) - 1)で割ります。colorWithColorSpace:〜でbitmapImageRep と同じカラースペースを指定してやります。

手順3は設定した色を1ピクセルで View に塗りつぶします。 View の座標系が左下が原点となっているので(pixelHeight - indexH)。単に indexH としたい場合は -(BOOL)isFlipped を実装して YES を返すようにすれば左上が原点になります。


NSBitmapImageRep を使って View に普通に描画するには、View にフォーカスをロックしてdrawInRect:(NSRect)rect を使い

NSRect rect = NSMakeRect( 0.0, 0.0, pixelsWide, pixelsHigh);
[bitmapImageRep drawInRect:rect];

としますと、さらっといきます。

余談ですがこの NSBitmapImageRep は Cocoa で最も長いイニシャライザをもってます。

initWithBitmapDataPlanes:(unsigned char **)planes
                              pixelsWide:(NSInteger)width
                               pixelsHigh:(NSInteger)height
                        bitsPerSample:(NSInteger)bps
                     samplesPerPixel:(NSInteger)spp
                                  hasAlpha:(BOOL)alpha
                                     isPlanar:(BOOl)isPlanar
                    colorSpaceName:(NSString *)colorSpaceName
                         bitmapFormat:(NSBitmapFormat)bitmapFormat
                            bytesPerRow:(NSInteger)rowBytes
                               bitsPerPixel:(NSInteger)pixelBits

148文字です。ツイッターでつぶやかれる方ご注意ください。そんな NSBitmapImageRep ラヴ。