キャンバス描画(1)・ニアレストネイバー

拡大縮小表示
ペイントソフトでは、キャンバス上でイメージを拡大または縮小して表示したい場合があります。
ドット絵などの小さい画像では拡大表示して編集した方が描画しやすいですし、大きい画像では縮小表示して全体を把握しながら編集できた方が便利です。

ここからは、イメージを拡大縮小してキャンバスに描画する方法を説明していきます。

なお、今後は、ウィンドウに表示されるイメージ部分を「キャンバス」と称することにします。
「キャンバス = ウィンドウ用のイメージ」だと思ってください。
画像処理の基本
まず、画像処理においては、

ソース画像の各点を元に描画先の座標を得て描画を行う

というやり方ではなく、

描画先の各点ごとに、ソース画像の内容を参照して色を得る

という考え方で描画を行っていきます。
元画像を基準にして考えるのではなく、処理後の画像を基準にして計算していきます。

例えば、「(x,y) の回転後の位置は (x1,y1)」という計算を使うのではなく、「回転後の位置 (x1,y1) の回転前位置は (x,y)」というように、処理後の位置から逆算して元の位置を計算します。
ニアレストネイバー
まずは、一番単純な拡大縮小方法、「ニアレストネイバー (Nearest Neighbor)」を説明します。

ニアレストネイバーは、描画先における描画元の位置の色を、単純に 1 点だけ拾って描画します。
画質は考慮しない、速度重視の方法です。

速度重視と言っても、拡大時にはドットが拡大された状態になるので、ドット絵編集用として使えます。
また、キャンバスのスクロール時など、キャンバス全体を更新する必要がある場合に、移動中は処理を軽くするためニアレストネイバーを使う、といった使い道もあります。
スクリーンショット


U キーで一段階拡大、D キーで一段階縮小。
100% 以下の場合は 20 ずつ、100% 以上の場合は 100 ずつ増減します。
スクロールバーでスクロールもできます。

現在の倍率は左上に表示されます。
ソースコード
使用画像: bitmap1.bmp

014_nearest.c
#include <stdio.h>
#include "sptk.h"

#define SCRW   16
#define CANVAS_W  250
#define CANVAS_H  250

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

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

void draw_canvas()
{
    int ix,iy,pitchd,sx,sy;
    uint8_t *pdst;
    SPTK_PIX_RGBA *psrc;
        
    pdst = winimg->pixbuftop;
    pitchd = winimg->pitch_dir - CANVAS_W * winimg->bpp;
    
    for(iy = 0; iy < CANVAS_H; iy++)
    {
        for(ix = 0; ix < CANVAS_W; ix++)
        {
            sx = (ix + scrx) * 100 / zoom;
            sy = (iy + scry) * 100 / zoom;
        
            psrc = sptk_image32_getptbuf(srcimg, sx, sy);
            
            if(psrc)
                sptk_image_setpixel_buf_rgb(winimg, pdst, psrc->r, psrc->g, psrc->b);
            else
                /* ソース範囲外 */
                sptk_image_setpixel_buf_rgb(winimg, pdst, 0xcc, 0xcc, 0xcc);
        
            pdst += winimg->bpp;
        }
        
        pdst += pitchd;
    }
}    

void update_screen(int time)
{
    char m[16];

    draw_canvas();
    
    sprintf(m, "%d", zoom);
    sptk_image_text(winimg, 4, 4, m, -1, 0xffffff); 
    sptk_image_text(winimg, 3, 3, m, -1, 0); 
    
    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 += 20;
                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 != 20)
                    zoom -= 20;
                
                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);
    
    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;
}
解説
"bitmap1.bmp" の画像を元画像として、ウィンドウイメージに直接拡大縮小して描画していきます。
zoom に現在の倍率 (等倍 = 100) が入っています。
スクロール
今回は、スクロールも出来るようにしていますが、スクロールの最大値は表示倍率によって変化するので、倍率が変化した時は change_zoom() でスクロールバーの情報を変更しています。

倍率が 200% なら、スクロール可能範囲は、「0〜画像サイズ×2」です。
画像幅(高さ) × 倍率 / 100」で、スクロール最大値を計算します。

なお、スクロールの範囲が変更された場合、内部のスクロール位置もそれに合わせて調整されている場合があるので、スクロール位置を再取得する必要があります。
sptk_scrollbar_get_pos() で、現在位置を再取得します。
キャンバス描画
draw_canvas() が、srcimg のイメージを元にウィンドウイメージに拡大縮小して描画する関数です。

イメージのバッファ操作
拡大縮小処理を説明する前に、まずは、SPTK_IMAGE のバッファ操作を説明します。

pdst = winimg->pixbuftop;
pitchd = winimg->pitch_dir - CANVAS_W * winimg->bpp;

pdst は、ウィンドウイメージ上の (0,0) の位置のポインタ、
pitchd は、X 方向処理後にポインタ位置を Y + 1 するのに必要な移動バイト数。

SPTK_IMAGEpixbuftop には、イメージの左上位置のポインタが入っています。
ちなみに、バッファ自体の先頭は pixbuf です。

なぜバッファの先頭とイメージの左上位置で別の変数が用意されているかというと、ウィンドウ表示用のイメージは、OS などによってイメージのバッファ構成が異なるためです
Windows では SPTK 内部で DIB イメージが使われていますが、このイメージは、Y が下から上へ向かう「ボトムアップ型」なので、バッファの先頭は (0, h - 1) の位置となっています。
そのため、Windows の場合は、(0,0) の位置である pixbuftop には「pixbuf + pitch * (h - 1)」の位置が入っています。

pitch_dir は、バッファ位置を Y + 1 するために必要な移動バイト数。
Windows のように、Y + 1 が上方向になる場合は、負の値が入っています。
bpp は、1px のバイト数です。

X 方向の処理が終わった後、pdst のポインタを pitchd 分移動しています。
Y を下方向に +1 移動するには、pitch_dir 分を加算するのですが、X 方向の処理としてすでに CANVAS_W * bpp 分右に移動しているので、pitch_dir からその分を引いた値を差分として加算します。

for(iy = 0; iy < CANVAS_H; iy++)
{
    for(ix = 0; ix < CANVAS_W; ix++)
    {
        sx = (ix + scrx) * 100 / zoom;
        sy = (iy + scry) * 100 / zoom;
    
        psrc = sptk_image32_getptbuf(srcimg, sx, sy);
        
        if(psrc)
            sptk_image_setpixel_buf_rgb(winimg, pdst, psrc->r, psrc->g, psrc->b);
        else
            /* ソース範囲外 */
            sptk_image_setpixel_buf_rgb(winimg, pdst, 0xcc, 0xcc, 0xcc);
    
        pdst += winimg->bpp;
    }
    
    pdst += pitchd;
}

実際にキャンバス描画として処理する範囲は、ウィンドウ上でイメージを表示する範囲です。
ここでは、(0,0) から CANVAS_W x CANVAS_H の範囲です。

拡大縮小の処理後であるキャンバスの座標から、拡大縮小の元画像の座標を計算して、計算した位置の元画像の色をそのまま描画先にセットします。
座標の計算式
実際の座標の計算式を求める前に、まずは、ソース画像の座標から、指定倍率を適用したキャンバス座標を計算する式を考えてみます。

ソース座標→キャンバス座標
表示倍率が 200% なら座標は x2 になるわけですから、以下のような式になりますね。

canvas_x = sx * zoom / 100
canvas_y = sy * zoom / 100

ついでに、スクロールの位置も適用する場合、拡大縮小した後にスクロール分を移動することになるので、以下のようになります。

canvas_x = sx * zoom / 100 - scrx
canvas_y = sy * zoom / 100 - scry

キャンバス座標→ソース座標
では、上の式を元に、キャンバス座標からソース座標を逆算する式を作ってみましょう。

canvas_x = sx * zoom / 100 - scrx
canvas_x + scrx = sx * zoom / 100
(canvas_x + scrx) * 100 = sx * zoom
(canvas_x + scrx) * 100 / zoom = sx

式の左辺/右辺を移動する際は、「+ なら -」、「* なら /」、というように逆の演算になります。
各演算を左辺に移動していくと、右辺には sx だけが残りました。
これで、キャンバス座標からソース座標を計算する式の出来上がりです。

sx = (canvas_x + scrx) * 100 / zoom
sy = (canvas_y + scry) * 100 / zoom
色をセット
キャンバス座標からソース座標を計算した後は、そのソース座標の色を描画先にセットします。

psrc = sptk_image32_getptbuf(srcimg, sx, sy);

if(psrc)
    sptk_image_setpixel_buf_rgb(winimg, pdst, psrc->r, psrc->g, psrc->b);
else
    /* ソース範囲外 */
    sptk_image_setpixel_buf_rgb(winimg, pdst, 0xcc, 0xcc, 0xcc);

ただし、拡大縮小後のイメージがキャンバスサイズより小さい場合などは、計算後のソース座標が、ソース画像全体の範囲外になる場合があるので、その場合は範囲外の色として適当な色を置きます。

sptk_image32_getptbuf() は、指定位置のピクセルバッファのポインタを取得します。
NULL が返った場合は、範囲外の座標です。

sptk_image_setpixel_buf_rgb() は、SPTK_IMAGE の指定ポインタ位置に、RGB 値を指定して色をセットします。
※ SPTK_IMAGE のピクセルフォーマットは、OS やディスプレイカラーによって異なるので、直接値をセットしてはいけません。