影にまつわる深い事情

前回までに、shadowコマンドで影を自在にコントロールできるようになったと喜んでいたが、実はまだ甘かった。まだまだ、なんちゃって影使いのレベル。立派な影使いになるためには、最低でも以下のことにも気を配る必要がありそう。

トリミング時のピクセルのずれ

  • ピクセルレベルで確認したら、トリミングの範囲が1ピクセルずれていた。
  • まずはその修正をして、コーナーを歪みのない丸い形状にしてみる。

輪郭線を描く

  • shadowコマンドで影付けしたウィンドウって、OSXの影付きウィンドウと比べると、どうもぼやけた印象を受ける。
  • それが何なのか分からなかったが、ピクセル単位で拡大してみて、ようやく気付いた。
OSXスクリーンショット shadowコマンド
20121115145858 20121116060542

なんと、OSXスクリーンショットには輪郭がある!

  • ウィンドウの1ピクセル外側を、影よりも1レベル濃い色の灰色ラインが囲っているではないか!
  • だからOSXスクリーンショットは、ウィンドウ枠のクッッキリした印象が際立っているのだ。
  • shadowコマンドは影をコントロールするのだけど、その過程で気付かぬうちに輪郭まで削除していたのだ...。
  • 輪郭を復活させたい。しばし考えて、影を2回描画する作戦を思いついた。
    • 1回目に輪郭のために画像の外側に1ピクセルはみ出る影を描く。
    • 2回目は今まで通りにオプション指定された通りの影を描く。
  • 1回目の輪郭用の影に、2回目の影が重ね塗りされるので、周囲の1ピクセル分が輪郭のように色が濃くなる、という作戦。
  • その仕組みを追加してみた。といっても、影が1ピクセルになるようにして、今までの描画コードを2回繰り返しているだけ。


// 影付きイメージを描画して返す
NSImage* dropshadowImage(NSImage *image, float blurRadius, float alphaValue)
{
float margin = blurRadius * 1.25;
//スケールを取得する
CGFloat scale = [[NSScreen mainScreen] backingScaleFactor];
//Retina環境に応じたポイントサイズを取得する
NSBitmapImageRep* imageRep = [NSBitmapImageRep imageRepWithData:[image TIFFRepresentation]];
NSSize dotSize = NSMakeSize(imageRep.pixelsWide/scale, imageRep.pixelsHigh/scale);
//描画する場所を準備
NSRect newRect = NSZeroRect;
newRect.size.width = dotSize.width + margin*2;
newRect.size.height = dotSize.height + margin*2;
NSImage *newImage = [[NSImage alloc] initWithSize:newRect.size];
//描画する場所=newImageに狙いを定める、描画環境を保存しておく
[newImage lockFocus];
[NSGraphicsContext saveGraphicsState];
//拡大・縮小した時の補間品質の指定
[[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
//影の設定(輪郭線のため)
NSShadow *shadow = [[NSShadow alloc] init];
[shadow setShadowOffset:NSZeroSize];
[shadow setShadowBlurRadius:1.0];
[shadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:alphaValue]];
[shadow set];
//描画する(輪郭線のため)
NSRect drawRect;
drawRect.origin = NSMakePoint(margin, margin);
drawRect.size = dotSize;
[image drawInRect:drawRect
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
//影の設定
[shadow setShadowOffset:NSMakeSize(0.0, -blurRadius * 0.25)];
[shadow setShadowBlurRadius:blurRadius];
[shadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:alphaValue]];
[shadow set];
//描画する
drawRect.origin = NSMakePoint(margin, margin);
drawRect.size = dotSize;
[image drawInRect:drawRect
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
//描画環境を元に戻す、描画する場所=newImageから狙いを外す
[NSGraphicsContext restoreGraphicsState];
[newImage unlockFocus];

return newImage;
}

そして、輪郭はできた!以前よりもくっきり感が増した印象だ。満足。

OSXスクリーンショット shadowコマンド
20121115145858 20121115153024

アンチエイリアス処理との戦い

  • 輪郭には満足できたが、今度はコーナーの形状に注目してみる。
  • なぜshadowコマンドのアンチエイリアス処理が消えてしまうかというと、影の部分を透明化した時の弊害である。
  • 少しでも透明度が入っていたら影と見なして、アルファ値0の完全な透明にすることで、影を消しているのだ。
  • アンチエリアス処理にも透明度が混ざっているので、影の透明化とともに消えてしまったのである。
  • もし影の部分の透明化をしなかったら、以下のようになる。
  • ピクセルレベルでは若干の差は見られるが、通常の倍率で見ればその違いに気付かないレベル。
OSXスクリーンショット shadowコマンド
20121115145858 20121115165316
  • 本来、影なしのスクリーンショットなら影の透明化は不要だ。
  • しかし、現状では影のある画像・ない画像に関係なく、一律で影の透明化を処理してしまっている。
  • 不要な処理をして、元画像の品質を無駄に落としてしまうのは勿体ないことである。修正してみた。
  • トリミング調査したrectの範囲が元画像と同じなら、影なし画像と判定して、無駄なトリミング・影の透明化はしない。


for(i = optind; i < argc; i++){
// ファイルパスをパースしておく
// NSString *aPath = @"~/a/b/c.d.e";
NSString *aPath = [NSString stringWithUTF8String:argv[i]];
NSString *fPath = [aPath stringByStandardizingPath]; // /Users/HOME/a/b/c.d.e
// NSString *fDir = [fPath stringByDeletingLastPathComponent]; // /Users/HOME/a/b
// NSString *fNameExt = [fPath lastPathComponent]; // c.d.e
NSString *fExt = [fPath pathExtension]; // e
NSString *fDirName = [fPath stringByDeletingPathExtension]; // /Users/HOME/a/b/c.d
// NSLog(@"name.ext=%@ ext=%@ dir=%@ dir/name=%@", fNameExt, fExt, fDir, fDirName);
NSString *outputPath = [NSString stringWithFormat:@"%@-shadow%@.%@", fDirName, commandText, fExt];

NSImage *image = [[NSImage alloc] initWithContentsOfFile:fPath];
NSRect imageRect = NSMakeRect(0, 0, image.size.width, image.size.height);
NSRect trimRect = trimRectFromImageByAlphaValue(image);
if (!NSEqualRects(trimRect, imageRect)) {
// 影の領域を削除した画像にする
image = trimImageByRect(image, trimRect);
// 影の部分を透明にする
image = transparentImageByAlphaValue(image);
}
// 影付きイメージを生成する
image = dropshadowImage(image, blurRadius, alphaValue);
// PNG画像として保存する
saveImageByPNG(image, outputPath);
// 画像情報を出力する
NSLog(@"%@ %@", outputPath, NSStringFromRect(imageRect));
}
}
return 0;
}

これで、影なし画像に対しては、高品質なアンチエイリアスを保持できるようになった!

ファイル名に入力コマンドを付加する

  • これは画質には関係ないが、出力するファイル名に入力したコマンドとオプション設定そのままを付加するようにしてみた。
  • 出力された画像が、どのような設定でこうなったのか、違いを比較する時にきっと便利である。


int main(int argc, char * argv[])
{

@autoreleasepool {
int opt, i;
float blurRadius = 8.0;
float alphaValue = 0.5;
NSString *commandText = @""; //<--------追記1

//opterr = 0;/* エラーメッセージを非表示にする */

while((opt = getopt(argc, argv, "a:b:")) != -1){
commandText = [commandText stringByAppendingString:[NSString stringWithFormat:@"-%c%s", opt, optarg]]; //<--------追記2
switch(opt){
case 'a':
sscanf(optarg, "%f", &alphaValue);
printf(" Option -%c = %f
"
, opt, alphaValue);
break;

case 'b':
sscanf(optarg, "%f", &blurRadius);
printf(" Option -%c = %f
"
, opt, blurRadius);
break;

// 解析できないオプションが見つかった場合は「?」を返す
// オプション引数が不足している場合も「?」を返す
case '?':
printf("Usage: dropshadow [-a ALPAH_VALUE(0-1)] [-b BLUR_RADIUS(0<)] FILE ...
"
);
printf("Example:
"
);
printf(" shadow test.png -> Default shadow(= shadow -a0.5 -b8 test.png)
"
);
printf(" shadow -b4 test.png -> Nano shadow
"
);
printf(" shadow -b2 test.png -> Line shadow
"
);
printf(" shadow -b0 -a0 test.png -> None shadow
"
);
return 1;
}
}

for(i = optind; i < argc; i++){
// ファイルパスをパースしておく
// NSString *aPath = @"~/a/b/c.d.e";
NSString *aPath = [NSString stringWithUTF8String:argv[i]];
NSString *fPath = [aPath stringByStandardizingPath]; // /Users/HOME/a/b/c.d.e
// NSString *fDir = [fPath stringByDeletingLastPathComponent]; // /Users/HOME/a/b
// NSString *fNameExt = [fPath lastPathComponent]; // c.d.e
NSString *fExt = [fPath pathExtension]; // e
NSString *fDirName = [fPath stringByDeletingPathExtension]; // /Users/HOME/a/b/c.d
// NSLog(@"name.ext=%@ ext=%@ dir=%@ dir/name=%@", fNameExt, fExt, fDir, fDirName);
NSString *outputPath = [NSString stringWithFormat:@"%@-shadow%@.%@", fDirName, commandText, fExt]; //<--------追記3

お勧めの設定

  • 以上の修正を加えて、shadowコマンドの性能は以下のようになった。
画像 コマンド 影の種類
20121116084151 OSXスクリーンショット 元画像
20121115181608 -shadow デフォルト
20121115181607 -shadow-b4 Nano
20121115181606 -shadow-b2 Line
20121115181605 -shadow-b0-a0 None

所感

  • ようやく一段落付いた感じ。
  • コーナーのカーブにアンチエイリアス処理を復活できないのが心残り。
  • ピクセルレベルで観察すると、描画に関する様々な事情が見えてくる。