ByteArrayで画像処理入門

前回は画像ヘッダ(ppmフォーマット)について扱いました。
これで前準備が終わり、実際の画像処理に取りかかることができます。
ただその前に、画像に関する情報はまとめて管理しておくと便利かと思います。
ByteArrayに画像ヘッダ以外のデータ、つまり画像データ本体を格納することになるんですが、その画像データにアクセスする際にいくつかの画像に関する情報が必要です。なのでそれらの情報をどういう風にまとめようかと考えたんですが、ここではOpenCVで利用されているIplImage構造体に倣います。

・IplImage構造体 (OpenCV/cxcore/include/cxtypes.h 内に定義されている)

typedef struct _IplImage
{
    int  nSize;         /* sizeof(IplImage) */
    int  ID;            /* version (=0)*/
    int  nChannels;     /* Most of OpenCV functions support 1,2,3 or 4 channels */
    int  alphaChannel;  /* ignored by OpenCV */
    int  depth;         /* pixel depth in bits: IPL_DEPTH_8U, IPL_DEPTH_8S, IPL_DEPTH_16S,
                           IPL_DEPTH_32S, IPL_DEPTH_32F and IPL_DEPTH_64F are supported */
    char colorModel[4]; /* ignored by OpenCV */
    char channelSeq[4]; /* ditto */
    int  dataOrder;     /* 0 - interleaved color channels, 1 - separate color channels.
                           cvCreateImage can only create interleaved images */
    int  origin;        /* 0 - top-left origin,
                           1 - bottom-left origin (Windows bitmaps style) */
    int  align;         /* Alignment of image rows (4 or 8).
                           OpenCV ignores it and uses widthStep instead */
    int  width;         /* image width in pixels */
    int  height;        /* image height in pixels */
    struct _IplROI *roi;/* image ROI. if NULL, the whole image is selected */
    struct _IplImage *maskROI; /* must be NULL */
    void  *imageId;     /* ditto */
    struct _IplTileInfo *tileInfo; /* ditto */
    int  imageSize;     /* image data size in bytes
                           (==image->height*image->widthStep
                           in case of interleaved data)*/
    char *imageData;  /* pointer to aligned image data */
    int  widthStep;   /* size of aligned image row in bytes */
    int  BorderMode[4]; /* ignored by OpenCV */
    int  BorderConst[4]; /* ditto */
    char *imageDataOrigin; /* pointer to very origin of image data
                              (not necessarily aligned) -
                              needed for correct deallocation */
}
IplImage;

// ROI
typedef struct _IplROI
{
    int  coi; /* 0 - no COI (all channels are selected), 1 - 0th channel is selected ...*/
    int  xOffset;
    int  yOffset;
    int  width;
    int  height;
}
IplROI;

これを、あまり使わないメンバを除いてActionScriptで書くと、

package {
 import flash.geom.Rectangle;
 import flash.utils.ByteArray;
 
 public class IplImage {
  public var nChannels:int;  //チャンネル数
  public var depth:int;      //ビット深度
  public var origin:int;     //左上原点なら0, 左下原点なら1(Windows Bitmap形式)
  public var width:int;      //画像の幅(画素数)
  public var height:int;     //画像の高さ(画素数)
  public var roi:Rectangle;  //着目領域(Region Of Interest)
  public var imageSize:int;  //画像のサイズ(バイト)
  public var imageData:ByteArray;  //ピクセルデータ
  public var widthStep:int;  //画像の横一行のバイト数
  
  public function IplImage(src:String) {
   var stream:URLStream = new URLStream();
   stream.load(new URLRequest(src));
   stream.addEventListener(Event.COMPLETE, loadImage);
   stream.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
  }
・・・
・デコード処理
・読み込みエラー処理
・・・
 }
}

// こんな感じで使えるように
var img:IplImage = new IplImage("./assets/src.raw");
trace("width: " + img.width + "px");

// trace例
width: 200px

実際に使う情報はこれくらいで、この中のimageDataに生のピクセルデータが入ることになります。
元が構造体なのでメンバも全てpublic、メソッドは最小限の数で留めておきます。
loadImage()でrawまたはppm画像をデコードするんですが、Flashで使う場合はBitmapData経由でByteArrayを利用したいユーザーが多いと思うので、今回はそっちのことについても少し調べました。
どうやらBitmapData.getPixels()というメソッドで、ピクセルデータが全て入ったByteArrayを返してくれるようです。
ただし、ARGB各8ビット整数のデータが返ってくるらしく自由度は高くありません。
OpenCVで言うなら、IPL_DEPTH_8U固定ということです。

入力をBitmapDataにするとコンストラクタは、

public function IplImage(src:BitmapData) {
 nChannels = 4;
 depth = 8;
 origin = 0;
 width = src.width;
 height = src.height;
 widthStep = width*(depth/8)*nChannels;
 imageSize = height*widthStep;
 imageData = new ByteArray();
 imageData = src.getPixels(src.rect);
}

となり、チャンネル数とビット深度は固定されます。
ということでBitmapData経由前提ならnChannelsとdepthメンバは必要なく、埋め込みでOKです。
getPixel()とgetPixel32()で分かれているのに、getPixels32()がないのはなぜだろうAdobe?

ここで、getPixels()後のpositionはデータの終端を指していることに注意します。
(IplImageの場合はimageSizeと同じ値になる、つまり画像データのバイトサイズ)
取得できるデータの末尾よりも後の部分を読み取ろうとした場合はEOFErrorとなります。
Cのポインタを扱っている感覚に近いですね。範囲外参照にはしっかり注意しておきます。

ここで改めてピクセルデータへのアクセス方法を整理します。
(座標(x,y)の各チャンネルデータにアクセスする場合)

・IplImage (C版:OpenCV) の場合

// cvLoadImage()ではBGR順に格納される

img->imageData[img->widthStep*y + x*3]       // B
img->imageData[img->widthStep*y + x*3 + 1]   // G
img->imageData[img->widthStep*y + x*3 + 2]   // R

・IplImage (AS3.0版:imageDataにByteArrayを利用) の場合

// getPixels()ではARGB順で格納される

img.imageData[img.widthStep*y + x*4]      // A
img.imageData[img.widthStep*y + x*4 + 1]  // R
img.imageData[img.widthStep*y + x*4 + 2]  // G
img.imageData[img.widthStep*y + x*4 + 3]  // B

ARGB固定なのは納得いかないですが、OpenCVのIplImageとほぼ同じ書式でアクセスできます。
(Cの場合、IplImageはポインタで宣言するのでアロー演算子を使うことになる)

ここで、ピクセルデータへの直接アクセス IplImage(opencv.jpのOpenCVサンプルコード)をASで書いてみると、
・R(赤)チャンネル値を0に,B(青)チャンネル値を約0.7倍する

[Embed(source = './assets/dna_768x768.png')]
var EmbedImage:Class; 
var srcBmp:Bitmap = new EmbedImage() as Bitmap;
var srcBmpData:BitmapData = srcBmp.bitmapData;

var img:IplImage = new IplImage(srcBmpData);
      
var p:Array = [];
var dstBmpData:BitmapData = new BitmapData(srcBmp.width, srcBmp.height);

// (1)ピクセルデータ(R,G,B)を順次取得し,変更する
for(var y:int=0;y<img.height;y++){
 for(var x:int=0;x<img.width;x++){
  p[0] = img.imageData[img.widthStep*y + x*4 + 3];
  p[1] = img.imageData[img.widthStep*y + x*4 + 2];
  p[2] = img.imageData[img.widthStep*y + x*4 + 1];
  img.imageData[img.widthStep*y + x*4 + 3] = Math.round(p[0]*0.7 + 10);
  img.imageData[img.widthStep*y + x*4 + 2] = Math.round(p[1]*1.0);
  img.imageData[img.widthStep*y + x*4 + 1] = Math.round(p[2]*0.0); 
 }
}
img.imageData.position = 0;
dstBmpData.setPixels(dstBmpData.rect, img.imageData);
addChild(new Bitmap(dstBmpData));

・出力結果 (左:入力、右:出力)

とりあえず同じ結果が得られました。
(1)の所のコードを見比べるとわかりますが、本家にかなり近い書き方で処理できていることがわかります。

ここで、ByteArrayにはread~()というデータ読み込み用メソッドがたくさん用意されていますが、総じて速度が遅く、事前にposition操作も必要なので、直にインデックスでアクセスするより1ステップ手間が増えてしまいます。
なので、
・1バイトずつ読み取る場合、readByte()を使うのは避けてインデックスアクセス。
・一気に4バイト以上読み取る場合はreadInt()やreadDouble()等を使う。
という感じで使い分けるといいんじゃないかなと思います。
ちなみに、readUnsignedInt()は符号なし32ビット整数を読み取るのでBitmapData.getPixel32()と同じです。
つまり、

img.getPixel32(x,y);  //imgはBitmapData
と
img.imageData.readUnsignedInt();  //img.imageDataはByteArray (imgはIplImage)
と
img.imageData[img.widthStep*y + x*4] << 24 | img.imageData[img.widthStep*y + x*4 + 1] << 16 |
img.imageData[img.widthStep*y + x*4 + 2] << 8 | img.imageData[img.widthStep*y + x*4 + 3];

この3つは同じ値になります。
そしてどうやらreadUnsignedInt()はgetPixel32()より遅いみたいです。
(50万回以上ループを回して50ms程度の差ですが)

ByteArrayを使うメリットはバイトレベルでデータを操作できること。
つまり、画像処理の場合はチャンネルレベルの操作をする時です。

・R(赤)チャンネルの値を取得する

// BitmapDataの場合 (imgはBitmapData)
img.getPixel(x,y) >> 16 & 0xff; 

 //ByteArrayの場合 (img.imageDataはByteArray)
img.imageData[img.widthStep*y + x*4 + 1];

この場合はByteArrayの方が20~25%ほど速いようです。
また、いったんByteArrayにデータを格納した後はARGB固定である必要はありません。
ピクセルデータをHSV色空間に変換して、

img.imageData[img.widthStep*y + x*4 + 1];  //座標(x,y)のH(色相)値を取得
img.imageData[img.widthStep*y + x*4 + 2];  //座標(x,y)のS(彩度)値を取得
img.imageData[img.widthStep*y + x*4 + 3];  //座標(x,y)のV(輝度)値を取得

のようにそれぞれの成分を単独で取得できるようにしておけば利用しやすいです。

ByteArrayを使ったピクセルデータへのアクセスは、確かにBitmapDataのそれよりは複雑かもしれません。
ただ、ここでByteArray版getPixel()などのメソッドを安易に作ってしまわないようにしたい所。
ピクセルデータへのアクセスは何度も繰り返すものなので、関数呼び出しのオーバーヘッドが無視できません。
OpenCVでもcvGet2D()というgetPixel()と同じ関数がありますが、使うのは非推奨とされていますね。

最近、C言語しか知らない周りの人達にFlashでデモを作って見せると、「へ?Flashってそんなことまで出来るんですねぇ。」みたいに興味を持つ人がちらほら。加えてAlchemyのおかげでC/C++開発者のFlash界参入が期待できそうですね。ストイックな人が多いでしょうから、高度なFlashをどんどん量産していきそうで少し怖いですけど;

関連記事:
ByteArrayで画像処理入門以前 2
ByteArrayで画像処理入門以前

あわせて読む:

3 Thoughts

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です