2013年11月20日

UIImage(CGImage)の画像に1ピクセルずつ効果を加えたい場合

http://d.hatena.ne.jp/waochi/20110405
ここらへんを参考にしてたのだけど、シミュレータではちゃんと動くのに実機(iOS6のiPad mini,iOS7のTouch 5G)だと表示が変になる。
いろいろ調べるとCGBitmapInfoが実機だと8194とか8198とかいう値になってる。(シミュレーターだと2とか6とかで上位ビットは0だった)
(AlphaInfo(BitmapInfoの下位の5bit)はBitmapInfoをkCGBitmapAlphaInfoMaskでマスクするかちゃんとCGImageGetAlphaInfoで取得した方がいいみたいです ByteOrderがDefault(上位ビットが0)ならBitmapInfo=AlphaInfoだけど)

CGBitmapInfoは
enum {
kCGBitmapAlphaInfoMask = 0x1F,
kCGBitmapFloatComponents = (1 << 8),
kCGBitmapByteOrderMask = 0x7000,
kCGBitmapByteOrderDefault = (0 << 12),
kCGBitmapByteOrder16Little = (1 << 12),
kCGBitmapByteOrder32Little = (2 << 12),
kCGBitmapByteOrder16Big = (3 << 12),
kCGBitmapByteOrder32Big = (4 << 12)
};
なので、実機はkCGBitmapByteOrder32Littleでリトルエンディアンのよう(8198=0x2006)(シミュレータはkCGBitmapByteOrderDefault(32Big))

ちなみに、CGImageAlphaInfo(下位5ビット)は
CGImageAlphaInfo {
kCGImageAlphaNone,
kCGImageAlphaPremultipliedLast,
kCGImageAlphaPremultipliedFirst,
kCGImageAlphaLast,
kCGImageAlphaFirst,
kCGImageAlphaNoneSkipLast,
kCGImageAlphaNoneSkipFirst
};
(順に0から6)

昔、設計をやってた頃にビッグエンディアンのCPUにリトルエンディアンのIOをつなげて、散々みんなから「そんなことできるの?」と言われた経験からエンディアンにはちょっとうるさい。
「エンディアン嘘つかない」とくだらないダジャレを言う余裕すらある。
(エンディアンについては http://www.ertl.jp/~takayuki/readings/info/no05.html とかwikipediaを参照)

で、以下のように直したら一応実機でもシミュレーターでも同じように動きました。
たぶん、はまる人も多いと思うのでご参考まで。
(ほぼ丸一日がかりで調べたので)

--
CGImageRef cgImage = originalImage.CGImage;
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
size_t bitsPerComponent = CGImageGetBitsPerComponent(cgImage);
size_t bitsPerPixel = CGImageGetBitsPerPixel(cgImage);
size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
bool shouldInterpolate = CGImageGetShouldInterpolate(cgImage);
CGColorRenderingIntent intent = CGImageGetRenderingIntent(cgImage);

CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
BOOL premulti=FALSE;
BOOL little=FALSE;

if (bitmapInfo & kCGBitmapByteOrder32Little) // これがミソ
little = TRUE;
if (alphaInfo == kCGImageAlphaPremultipliedFirst || alphaInfo == kCGImageAlphaPremultipliedLast)
premulti = TRUE; //あまり使うことはないと思うけど

// alphaInfoにあわせてビットのオフセットを設定する 無い時は-1
int bitOffsetRed;
int bitOffsetGreen;
int bitOffsetBlue;
int bitOffsetAlpha;

if(alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaLast
){
if (little)
{
bitOffsetRed = 3;
bitOffsetGreen = 2;
bitOffsetBlue = 1;
bitOffsetAlpha = 0;
}
else
{
bitOffsetRed = 0;
bitOffsetGreen = 1;
bitOffsetBlue = 2;
bitOffsetAlpha = 3;
}
} else if (alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaFirst){
if (little)
{
bitOffsetRed = 2;
bitOffsetGreen = 1;
bitOffsetBlue = 0;
bitOffsetAlpha = 3;
}
else
{
bitOffsetRed = 1;
bitOffsetGreen = 2;
bitOffsetBlue = 3;
bitOffsetAlpha = 0;
}
} else if (alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipLast){
if (little)
{
bitOffsetRed = 3;
bitOffsetGreen = 2;
bitOffsetBlue = 1;
bitOffsetAlpha = -1;
}
else
{
bitOffsetRed = 0;
bitOffsetGreen = 1;
bitOffsetBlue = 2;
bitOffsetAlpha = -1;
}
} else if (alphaInfo == kCGImageAlphaNoneSkipFirst){
if (little)
{
bitOffsetRed = 2;
bitOffsetGreen = 1;
bitOffsetBlue = 0;
bitOffsetAlpha = -1;
}
else
{
bitOffsetRed = 1;
bitOffsetGreen = 2;
bitOffsetBlue = 3;
bitOffsetAlpha = -1;
}
} else if (alphaInfo == kCGImageAlphaOnly){ //AlphaOnlyって何?
if (little)
{
bitOffsetRed = -1;
bitOffsetGreen = -1;
bitOffsetBlue = -1;
bitOffsetAlpha = 3;
}
else
{
bitOffsetRed = -1;
bitOffsetGreen = -1;
bitOffsetBlue = -1;
bitOffsetAlpha = 0;
}
} else { //どれでもない時は適当にBigEndianのAlphaLastにしてしまえ
bitOffsetRed = 0;
bitOffsetGreen = 1;
bitOffsetBlue = 2;
bitOffsetAlpha = 3;
}

CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);

//(Received memory warningに注意)※下記2行を1行にまとめると実機でReceived memory warningで悩むことになる 理由はわからない 教えて偉い人
CFDataRef baseData=CGDataProviderCopyData(dataProvider);
CFMutableDataRef data=CFDataCreateMutableCopy(NULL,0,baseData);

UInt8* buffer = (UInt8*)CFDataGetMutableBytePtr(data);

for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
// ピクセルのアドレスを計算する
UInt8* tmp = buffer + j * bytesPerRow + i * 4;

// RGBの値を取得する
double r = 0;
if (bitOffsetRed != -1) {
r = *(tmp + bitOffsetRed);
}

double g = 0;
if (bitOffsetGreen != -1) {
g = *(tmp + bitOffsetGreen);
}

double b = 0;
if (bitOffsetBlue != -1) {
b = *(tmp + bitOffsetBlue);
}

double a = 255;
if (bitOffsetAlpha != -1) {
a = *(tmp + bitOffsetAlpha);
}

:
この間でr,g,bをいじる
:

double R,G,B;
// なんでdoubleにしてるかというと、いろいろ演算してるから
     // 演算しないならUInt8でいいです

R=r;
G=g;
B=b;

// RGBを書き戻す

*(tmp + bitOffsetRed) = (UInt8)R;
*(tmp + bitOffsetGreen) = (UInt8)G;
*(tmp + bitOffsetBlue) = (UInt8)B;

// 解放する
CFRelease(data);
CFRelease(baseData);
--
AlphaPremultipliedの処理の仕方がよくわからない。
何もしなくていいみたいだけど…
(動きからみるとRGBの値にAlphaを演算済みという印だけで扱いはAlphaNoneと同じでいいみたい ドキュメントちゃんと読んでないけど)

※この資料、CGImageのビットマップ操作について日本語で書かれた非公式の資料では2013年11月時点で一番詳しいと思いますよ。(いろいろ探した私が言うのだから間違いない)

※kCGBitmapFloatComponentsなんていう定義があるのをみると今はRGBA各1バイト(Uint8)だけど将来的にはFloatになるのかもしれないですね。
その時は大幅な見直しが必要になります。
まぁ、5年や10年は大丈夫だと思うけど。
ラベル:アプリ製作
posted by one-hand-engineer at 14:27| Comment(2) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
知りたかった情報が分かりやすくまとまっていて助かりました。ありがとうございます。
Posted by Seyama at 2014年09月11日 13:31
私もわからなくて1日つぶしたので書いたかいがありました。
実機とiOSシミュレータでエンディアンが逆というのが知らないとはまりますよね。
まぁ上っ面をいじってる分には全然困らないのですけど。
Posted by ka-ta at 2014年09月11日 13:50
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:


この記事へのトラックバック