キャンバス描画(3)・オーバーサンプリング

縮小を高品質化する
前回は拡大縮小方法として「ニアレストネイバー」をやりました。
これは、拡大表示や速度重視の描画としての使い道はありますが、縮小表示としては画質が悪く、あまり使えません。

今回は縮小表示時にもう少し画質を良くする方法を説明します。

拡大縮小の方法としては、ニアレストネイバーの次点として「バイリニア」という方法が存在するのですが、これは縮小表示用としては中途半端な画質となるので、今回は扱いません。

今回やるのは、「オーバーサンプリング」です。
オーバーサンプリング
オーバーサンプリング」とは、1ピクセルを仮想的に複数のサブピクセルに分解して処理を行うやり方です。

例えば、1ピクセルを仮想的に4x4に分解したとします。
そうすると、各ピクセルの x, y それぞれで、「0.0、0.25、0.5、0.75」の位置が仮想的に存在することになります。
分解した「4x4=16マス」それぞれの位置で処理を行い、その結果の平均値を取ることで、色を得ます。

16マスのうち1マスだけに点がある、ということであれば、そこの1ピクセルは、本来の色の 1/16 の濃度しかない、ということになります。

なお、サブピクセルは、あくまで計算上のみに存在する仮想的なものです。
実際に1ピクセル=4x4のイメージを作ると、16倍の容量のイメージが必要になってしまいます。

オーバーサンプリングは基本的にアンチエイリアス描画を行う時に使われるので、本来は拡大縮小用というわけではないのですが、ある程度速度を重視して縮小描画を行うのに適切な方法なので、紹介します。
スクリーンショット


縮小表示が、ニアレストネイバーより滑らかになりました。
ソースコード
使用画像: bitmap1.bmp

#include <stdio.h>
#include "sptk.h"

#define SCRW      16
#define CANVAS_W  250
#define CANVAS_H  250
#define SUBPIXEL  4

SPTK_WIDGET *scrh,*scrv;
SPTK_IMAGE *winimg;
SPTK_IMAGE32 *srcimg;

int scrx = 0,scry = 0,zoom = 100;
int update_canvas = 0;

void draw_canvas()
{
    int ix,iy,jx,jy,pitchd,sfx,sfy,sfx_left,incfxy;
    int sx,sy,r,g,b,sfx_j,sfy_j,incfxy_j;
    uint8_t *pdst;
    SPTK_PIX_RGBA *psrc;
        
    pdst = winimg->pixbuftop;
    pitchd = winimg->pitch_dir - CANVAS_W * winimg->bpp;
    
    incfxy   = (1 << 18) * 100 / zoom;
    incfxy_j = incfxy / SUBPIXEL;
    
    sfx_left = scrx * incfxy;
    
    sfy = scry * incfxy;
    
    for(iy = 0; iy < CANVAS_H; iy++)
    {
        sfx = sfx_left;

        for(ix = 0; ix < CANVAS_W; ix++, sfx += incfxy, pdst += winimg->bpp)
        {
            /* 範囲外 */
            
            sx = sfx >> 18;
            sy = sfy >> 18;
            
            if(sx < 0 || sx >= srcimg->w || sy < 0 || sy >= srcimg->h)
            {
                sptk_image_setpixel_buf_rgb(winimg, pdst, 0xcc, 0xcc, 0xcc);
                continue;
            }
        
            /* オーバーサンプリング */
            
            r = g = b = 0;
        
            for(jy = 0, sfy_j = sfy; jy < SUBPIXEL; jy++, sfy_j += incfxy_j)
            {
                for(jx = 0, sfx_j = sfx; jx < SUBPIXEL; jx++, sfx_j += incfxy_j)
                {
                    sx = sfx_j >> 18;
                    sy = sfy_j >> 18;
                    
                    if(sx < 0) sx = 0;
                    else if(sx >= srcimg->w) sx = srcimg->w - 1;
                    
                    if(sy < 0) sy = 0;
                    else if(sy >= srcimg->h) sy = srcimg->h - 1;
                
                    psrc = srcimg->pixbuf + sy * srcimg->w + sx;
                    
                    r += psrc->r;
                    g += psrc->g;
                    b += psrc->b;
                }
            }
            
            r /= SUBPIXEL * SUBPIXEL;
            g /= SUBPIXEL * SUBPIXEL;
            b /= SUBPIXEL * SUBPIXEL;
        
            sptk_image_setpixel_buf_rgb(winimg, pdst, r, g, b);
        }
        
        sfy += incfxy;
        pdst += pitchd;
    }
}

void update_handle()
{
    char m[16];
    
    if(update_canvas)
    {
        draw_canvas();
        
        sprintf(m, "%d", zoom);
        sptk_image_text(winimg, 4, 4, m, -1, 0xffffff); 
        sptk_image_text(winimg, 3, 3, m, -1, 0);
        
        update_canvas = 0;
    }
}

void update_screen(int time)
{
    update_canvas = 1;

    sptk_update(NULL, 0, 0, CANVAS_W, CANVAS_H, time);
}

void change_zoom()
{
    /* スクロール最大値変更 */

    sptk_scrollbar_set_status(scrh, 0, srcimg->w * zoom / 100, CANVAS_W);
    sptk_scrollbar_set_status(scrv, 0, srcimg->h * zoom / 100, CANVAS_H);
    
    /* 位置を再取得 */
    
    scrx = sptk_scrollbar_get_pos(scrh);
    scry = sptk_scrollbar_get_pos(scrv);
}

void winhandle(SPTK_EVENT *ev)
{
    switch(ev->type)
    {
        case SPTK_EVENT_WINDOW_KEYDOWN:
            if(ev->key.code == 'U')
            {
                if(zoom < 100)
                    zoom += 10;
                else if(zoom != 1000)
                    zoom += 100;
                
                change_zoom();
                update_screen(0);
            }
            else if(ev->key.code == 'D')
            {
                if(zoom > 100)
                    zoom -= 100;
                else if(zoom != 10)
                    zoom -= 10;
                
                change_zoom();
                update_screen(0);
            }
            break;
        case SPTK_EVENT_WINDOW_CLOSE:
            sptk_quit();
            break;
    }
}

void scroll_handle(SPTK_WIDGET *wg,int type,int pos)
{
    if(wg->id == 0)
        scrx = pos;
    else
        scry = pos;
    
    update_screen(5);
}

int main()
{
    sptk_init("test", CANVAS_W + SCRW, CANVAS_H + SCRW);
    
    sptk_window_set_handle(winhandle);
    sptk_set_update_handle(update_handle);
    
    winimg = sptk_window_get_image();
    
    srcimg = sptk_image32_load_bitmap("bitmap1.bmp");
    if(!srcimg) sptk_errexit("cannot load bitmap file");
        
    scrh = sptk_widget_scrollbar_create(0, 0, CANVAS_H, CANVAS_W, SCRW, 0, scroll_handle);
    scrv = sptk_widget_scrollbar_create(1, CANVAS_W, 0, SCRW, CANVAS_H, 1, scroll_handle);

    change_zoom();
    update_screen(-1);
    
    sptk_run();
    
    sptk_image32_free(srcimg);

    return 0;
}
解説
サブピクセル数は SUBPIXEL マクロで定義してあります。
値はにしてありますが、好きな値に変更して試すことができます。
値が大きいほど画質が良くなりますが、その分処理も重くなります。
更新について
今回のキャンバス描画処理は結構重いので、スクロールの位置が少しでも変更されたらすぐ描画、とやっていたのでは、描画が追いつかずにスクロール動作にもたつきが出る場合があります。

そのため、今回は sptk_set_update_handle() で更新用のハンドラをセットし、実際にウィンドウが更新される時のタイミングで、一緒にキャンバス描画を行うようにしています。

スクロールの位置が変更された場合、sptk_update() で更新する範囲だけを送って、実際の更新はタイマーで 5 ms 以内に確実に行われるようにします。
そして、処理するイベントがなくなったか、5 ms の時間が経った時、SPTK 内部のウィンドウの更新処理 (ウィンドウ用イメージを実際のウィンドウに転送する) の前に update_handle() を呼び出させ、そこでキャンバス描画を行います。

これで、スクロール位置が変更されても、すぐにはキャンバス描画は行われません。

本当は、キャンバス側でタイマーなどを使って描画を遅延させた方がいいのですが、ここではテスト用ということで、処理を簡潔にするため、この方法にしました。
オーバーサンプリング・基本の考え方
オーバーサンプリングでは、1ピクセルを複数のサブピクセルに分解します。

しかし、ここでの 1px というのは「拡大縮小後の 1px」ですから、この 1px を分解するということは、拡大縮小後の座標をサブピクセルの数だけ増やすということになります。

ということは、ここでは、ix, iy が拡大縮小後の座標ですから、この座標にサブピクセルの座標を加えて、それを元にソース座標を計算することになりますね。

for(iy = 0; iy < CANVAS_H; iy++)
{
    for(ix = 0; ix < CANVAS_W; ix++)
    {
        /* オーバーサンプリング */

        for(jy = 0; jy < SUBPIXEL; jy++)
        {
            for(jx = 0; jx < SUBPIXEL; jx++)
            {
                dx = ix + jx * (1.0 / SUBPIXEL);
                dy = iy + jy * (1.0 / SUBPIXEL);
                
                /* ソース画像座標計算 */
            }
        }
    }
}

1px を「SUBPIXEL x SUBPIXEL」に分解するということは、サブピクセルの1単位の座標値は「1 / SUBPIXEL」ということになります。
元の座標 ix, iy に、各サブピクセルの位置 (浮動小数点) を足して、その各位置で処理を行います。
考え方を変える
しかし、上記の通りにサブピクセル毎に浮動小数点で計算処理をしていたら、重くなります。
ここでは、高速化のために、考え方を変える必要があります。

まずは、拡大縮小後の座標を基準として考えるのではなく、ソース画像の座標を基準として考えてみましょう。

今回はすでに、高速化の一つとして、固定小数点数を使ってソース画像の座標をあらかじめ計算してあります。
ループ内部では、拡大縮小後の座標からソース画像座標を求めるための計算処理は行わず、結果となる座標を小数付きで保持しておき、変化分を加算していくやり方になっています。

オーバーサンプリングの座標も、このやり方に合うように考えてみれば、高速化できそうです。

拡大縮小後のサブピクセルの1単位は「1 / SUBPIXEL」ですが、これを、ソース画像を基準とした単位に変換すれば、拡大縮小後のサブピクセルが +1 した時には、ソース画像の座標はその単位分移動するということになります。

以下が、それに適応したコードです。

subf = (1 << 18) * 100 / zoom / SUBPIXEL;

for(iy = 0; iy < CANVAS_H; iy++)
{
    for(ix = 0; ix < CANVAS_W; ix++)
    {
        /* オーバーサンプリング */

        for(jy = 0; jy < SUBPIXEL; jy++)
        {
            for(jx = 0; jx < SUBPIXEL; jx++)
            {
                sx = (sfx + jx * subf) >> 18;
                sy = (sfy + jy * subf) >> 18;
            }
        }
    }
}

拡大縮小後における「1 / SUBPIXEL」は、ソース画像においては「(100 / zoom) / SUBPIXEL」に相当します。
これを固定小数点数にするため、(1 << 18) を掛けています。

現在のソース画像の座標は sfx, sfy に入っているので、その座標にサブピクセル分を加算すれば、拡大縮小後の各サブピクセルに対するソース画像座標を得ることができます。

後は、求めた sx, sy のソース画像の座標から色を得ます。
実際のコード
オーバーサンプリング部分の実際のコードは以下のようになっています。

r = g = b = 0;

for(jy = 0, sfy_j = sfy; jy < SUBPIXEL; jy++, sfy_j += incfxy_j)
{
    for(jx = 0, sfx_j = sfx; jx < SUBPIXEL; jx++, sfx_j += incfxy_j)
    {
        sx = sfx_j >> 18;
        sy = sfy_j >> 18;
        
        if(sx < 0) sx = 0;
        else if(sx >= srcimg->w) sx = srcimg->w - 1;
        
        if(sy < 0) sy = 0;
        else if(sy >= srcimg->h) sy = srcimg->h - 1;
    
        psrc = srcimg->pixbuf + sy * srcimg->w + sx;
        
        r += psrc->r;
        g += psrc->g;
        b += psrc->b;
    }
}

r /= SUBPIXEL * SUBPIXEL;
g /= SUBPIXEL * SUBPIXEL;
b /= SUBPIXEL * SUBPIXEL;

求めたソース画像の座標 sx, sy を範囲内に調整した後、各サブピクセル上の RGB 値を加算していき、最後に「SUBPIXEL * SUBPIXEL」で割っています。

これは、各サブピクセルの色の平均色を計算しています。
ソース画像の複数のピクセルの平均色を求めることで、色を滑らかにすることができます。

サブピクセル数が「2 の n 乗」の場合は、割り算の部分をシフト演算で置き換えることができるので、高速化するなら、サブピクセル数は「2,4,8...」といった値にしておくべきです。
キャンバス描画用なら、くらいで十分かと思います。

なお、サブピクセルの処理内で、ソース画像の座標が範囲外の値になる場合があるので、その場合は範囲内に調整した値を使うことにしています。
(左上サブピクセルの位置が範囲外なら、そのピクセルは範囲外とする)

サブピクセル内すべてが範囲外の値であれば、そのピクセルは範囲外の色ということになりますが、一部分でも範囲外の位置が含まれていれば範囲外にするのか、それとも、一部分が範囲外ならそれは無視するのか、など、サブピクセル内における範囲外の位置の処理はそれぞれ好きなように行ってください。
まとめ
オーバーサンプリングによるキャンバス描画は、基本的に縮小表示用です。
ですが、拡大時はドットの境が滑らかになるので、拡大表示用としても使えないことはありません。

なお、オーバーサンプリングによる拡大縮小は、あくまで、画質と速度をバランスよく重視して行うためのやり方なので、今回のようなキャンバス描画や、画質を考慮しないちょっとした拡大縮小を行いたい場合に使います。

画像処理としてちゃんとした拡大縮小を行う場合は、他の方法を使ってください。