No.007 - 09-1 円描画(1) |
ミッチェナー(Michener)、Bresenham(ブレゼンハム)等のアルゴリズムを用いた円描画の紹介と解説です。 古くからあるアルゴリズムらしいのですが、あんまり解説してるサイトがないので当サイトで一通りやります。 |
1.円の基本 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
円の一般的な公式は ですが、 ミッチェナー(Michener)のアルゴリズムでは の状態にして使います。 ピタゴラスって天才だな〜と思う今日この頃。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2.デジタル円の描画を行う前に |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
最初に言っておきますが デジタル円はアスペクト比の関係上、真円には近づきません。どうあがいても。 ピクセル自体が縦長なので円もちょっと縦長です。 アスペクト比の修正は各アプリケーションで行うほうがいいので、 今回はR * R ピクセルの正方形と考えてアルゴリズムを組みます。 ご了承ください。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3.いろいろなデジタル円の描画 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
デジタル円の描画のアルゴリズムは2、3種類があります。 ブレゼンハム(Bresenham)、ミッチェナー(Michener)が一般的です。 OpenFMIというサイトの中のページで "Comparing Circle Drawing Algorithms"という C のソース http://openfmi.net/snippet/detail.php?type=snippet&id=8 が紹介されています。 同一コード内にブレゼンハム、ミッチェナー等の関数も用意されており、ソースとしては完璧です。 ちなみにこのソースの中にダブル・サブストラクション(2重減算)というアルゴリズムもありますが、 これは、ブレゼンハムの中点を用いた円描画のようです。 ("second-order differences"や"midpoint circle algorithm"といった名前で洋サイト検索すると吉です。) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4.基本的なデジタル円の描画の考え方 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
これが考え方の基本です。 ただホントに毎回2乗して比較してたら高速演算にならないので各アルゴリズムで工夫がなされています。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
5.旧版のブレゼンハム(Bresenham)の円描画 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6.ミッチェナー(Michener)の円描画 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
海外のサイトではこの「ミッチェナー」のアルゴリズムが「ブレゼンハム」として紹介しているところがあります。 筆者も「信頼のできる文献」をもってないので詳細は不明です。 仕方が無いので、とりあえずここは「ミッチェナー」で通します。
ここでDH、DLのそれぞれのDとの差分を考えます。 DH = D + 〜 DL = D + 〜 とすれば演算が高速になるからです。 DH - D = 4x +6 DL - D = 4x - 4y +10
Dの初期値は、スタート地点が(0, r) なので
より、 D0 = a+b = 2T + 4x - 2y +3 = 0 + 4*0 -2*r +3 = 3 -2*r です。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
7.ブレゼンハム(Bresenham)の中点分岐を用いた円描画 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
D <0 なら、M<r で r は真ん中(==M)より高 い位置にあるので、P点を選択、 D >=0 なら M>=<r で r は真ん中(= =M)より低い位置にあるので、Q点を選択します
このへんのやり方はミッチェナーと同じです。 Dの初期値は、スタート地点が(0, r) なので
より、 D0 = T +2x -y +1+/4 = 0 + 2*0 -r +1+1/4 = 1 -r +1/4 です。 ルーチン上、x, y, r は全て整数で、 比較演算、代入演算においても全て整数で間に合っており 初期値 D0 の小数部分である 1/4 は結果にまったく影響しないので、 1/4 は切り捨ててしまいます。 したがって 初期値 D0 は D0 = 1 -r |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
さてこれで一応メインループの完成です。
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
今度はD の増分に注目です。
それぞれの増分を囘h、囘l とすると 囘h = 2x +3 囘l = 2x -2y +5
囘h は x が1増えると 2増えます。y の増減は関係しません。 囘h = 2(x+1) +3 // ・・・x が1増えたとき 囘l は x が1増えると 2増えます。y が1減っても 2増えます。 囘l = 2(x+1) -2(y-1) +5 // ・・・x が1増えて、y が1減ったとき = 2x +2 -2y +2 +5 = 2x -2y +9 また、囘h、囘l それぞれの初期値は、スタート地点(0, r) より、 囘h = 2x +3 = 3 囘l = 2x -2y +5 = 5 -2r |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
さあメインループの完成です。
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
で実際の関数がこれ。
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
8.まとめ |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
今回はアルゴリズムの紹介だけで終わりますが、 実は上記のブレゼンハム とミッチェナー、実は少し問題があります。 1.引数に半径を指定しているので、奇数の直径を指定出来ない。 大きな円ならかまわないのですが、デジタル円は小さな円の場合が少なくないので。 2.実は精度が良くない。 アルゴリズムの見かけ上は「完璧」なんですが、実際きちんと
続きは次回。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
下記サンプルは "gt_img_09.cpp" ファ イル1つだけです。 単純に円描画アルゴリズムを比較するためのソースです。 OpenFMIというサイトの"Comparing Circle Drawing Algorithms"という C のソースを 参考にしています。 日本語訳後、修正して、さらにウチのオリジナル関数も加えています。 関数解説 1.ReallySimpleCircle() はシンプル円描画(小数切捨て)です。 2.ReallySimpleCircle2() はシンプル円描画(値のより近いピクセルを選択する方式) 3.Win32APIGDICircle() Win32API GDI 円描画 4.BresenhamCircle() はブレゼンハム(Bresenham)の円描画 (旧版) 5.MiechenerCircle() はミッチェナー(Miechener) の円描画 6.MidpointCircle() はブレゼンハム(Bresenham)の中点分岐の円描画 6つの関数のうち 1.ReallySimpleCircle() シンプル円描画(小数切捨て) 5.MiechenerCircle() ミッチェナー 6.MidpointCircle() ブレゼンハム(中点) の3つの関数が同一円を描きます。 ミッチェナーやブレゼンハムがなぜ、小数切捨てのシンプル円になるのか筆者にはわかりません。 2.ReallySimpleCircle2() は当サイト・オリジナル関数です。 基本に忠実なので、他より少しだけ精度が高いんですが、実行速度の遅い関数です。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
gt_img_09_1.cpp |
#include <windows.h> #include <math.h> // シンプル円描画(小数切捨て方式) void ReallySimpleCircle (HDC hdc, LONG radius, POINT center, COLORREF col){ LONG cx = 0, cy; double xLimit = sqrt ((double) (radius * radius) / 2); // r*r*√2 → 45° while (cx++ <= xLimit){ cy = (LONG) floor (sqrt (radius * radius - cx * cx)); SetPixel (hdc, cx + center.x, cy + center.y, col); // 45-90 度の間 SetPixel (hdc, cx + center.x, -cy + center.y, col); // 270-315 度の間 SetPixel (hdc, -cx + center.x, cy + center.y, col); // 90-135 度の間 SetPixel (hdc, -cx + center.x, -cy + center.y, col); // 225-270 度の間 SetPixel (hdc, cy + center.x, cx + center.y, col); // 0-45 度の間 SetPixel (hdc, cy + center.x, -cx + center.y, col); // 315-360 度の間 SetPixel (hdc, -cy + center.x, cx + center.y, col); // 135-180 度の間 SetPixel (hdc, -cy + center.x, -cx + center.y, col); // 180-225 度の間 } } // シンプル円描画2(値のより近いピクセルを選択する方式) void ReallySimpleCircle2 (HDC hdc, LONG radius, POINT center, COLORREF col){ LONG cx = 0, cy=radius; double xLimit = sqrt ((double) (radius * radius) / 2); // 45度→r*√2 double d1, d2; for (cx=0; cx <= xLimit ; cx++) { d1 = (cx * cx+cy*cy) - radius * radius; d2 = (cx * cx+(cy-1)*(cy-1)) - radius * radius; if (abs(d1)>abs(d2)) cy--; SetPixel (hdc, cx + center.x, cy + center.y, col); // 45-90 度の間 SetPixel (hdc, cx + center.x, -cy + center.y, col); // 270-315 度の間 SetPixel (hdc, -cx + center.x, cy + center.y, col); // 90-135 度の間 SetPixel (hdc, -cx + center.x, -cy + center.y, col); // 225-270 度の間 SetPixel (hdc, cy + center.x, cx + center.y, col); // 0-45 度の間 SetPixel (hdc, cy + center.x, -cx + center.y, col); // 315-360 度の間 SetPixel (hdc, -cy + center.x, cx + center.y, col); // 135-180 度の間 SetPixel (hdc, -cy + center.x, -cx + center.y, col); // 180-225 度の間 } } // Win32API GDI 円描画 void Win32APIGDICircle (HDC hdc, LONG radius, POINT center){ SelectObject (hdc, GetStockObject (NULL_BRUSH)); Ellipse (hdc, center.x - radius, center.y + radius, center.x + radius, center.y - radius); } // ブレゼンハム(Bresenham)の円描画 (旧版) void BresenhamCircle (HDC hdc, LONG radius, POINT center, COLORREF col){ LONG cx, cy, d; cx = 0; cy = radius; d = 2 - 2 * radius; // 初期座標をあらかじめ描画する SetPixel (hdc, cx + center.x, cy + center.y, col); // 座標 (0, R); SetPixel (hdc, cx + center.x, -cy + center.y, col); // 座標 (0, -R); SetPixel (hdc, cy + center.x, cx + center.y, col); // 座標 (R, 0); SetPixel (hdc, -cy + center.x, cx + center.y, col); // 座標 (-R, 0); while (1){ if (d > -cy){ --cy; d += 1 - 2 * cy; } if (d <= cx){ ++cx; d += 1 + 2 * cx; } if (!cy) return; // 描画終了 // 描画 SetPixel (hdc, cx + center.x, cy + center.y, col); // 0-90 度の間 SetPixel (hdc, -cx + center.x, cy + center.y, col); // 90-180 度の間 SetPixel (hdc, -cx + center.x, -cy + center.y, col); // 180-270 度の間 SetPixel (hdc, cx + center.x, -cy + center.y, col); // 270-360 度の間 } } // ミッチェナー(Miechener) の円描画 void MiechenerCircle (HDC hdc, LONG radius, POINT center, COLORREF col){ LONG cx, cy, d; d = 3 - 2 * radius; cy = radius; // 初期座標をあらかじめ描画する SetPixel (hdc, center.x, radius + center.y, col); // 座標 (0, R); SetPixel (hdc, center.x, -radius + center.y, col); // 座標 (0, -R); SetPixel (hdc, radius + center.x, center.y, col); // 座標 (R, 0); SetPixel (hdc, -radius + center.x, center.y, col); // 座標 (-R, 0); for (cx = 0; cx <= cy; cx++){ if (d >= 0) { d += 10 + 4 * cx - 4 * cy; cy--; } else { d += 6 + 4 * cx; } // 描画 SetPixel (hdc, cy + center.x, cx + center.y, col); // 0-45 度の間 SetPixel (hdc, cx + center.x, cy + center.y, col); // 45-90 度の間 SetPixel (hdc, -cx + center.x, cy + center.y, col); // 90-135 度の間 SetPixel (hdc, -cy + center.x, cx + center.y, col); // 135-180 度の間 SetPixel (hdc, -cy + center.x, -cx + center.y, col); // 180-225 度の間 SetPixel (hdc, -cx + center.x, -cy + center.y, col); // 225-270 度の間 SetPixel (hdc, cx + center.x, -cy + center.y, col); // 270-315 度の間 SetPixel (hdc, cy + center.x, -cx + center.y, col); // 315-360 度の間 } } // ブレゼンハム(Bresenham)の中点分岐の円描画 void MidpointCircle (HDC hdc, LONG radius, POINT center, COLORREF col){ LONG cx, cy, d, dH, dD; d = 1 - radius; dH = 3; dD = 5 - 2 * radius; cy = radius; // drawing the circle: for (cx = 0; cx <= cy; cx++){ if (d < 0){ d += dH; dH += 2; dD += 2; } else{ d += dD; dH += 2; dD += 4; --cy; } // 描画 SetPixel (hdc, cy + center.x, cx + center.y, col); // 0-45 度の間 SetPixel (hdc, cx + center.x, cy + center.y, col); // 45-90 度の間 SetPixel (hdc, -cx + center.x, cy + center.y, col); // 90-135 度の間 SetPixel (hdc, -cy + center.x, cx + center.y, col); // 135-180 度の間 SetPixel (hdc, -cy + center.x, -cx + center.y, col); // 180-225 度の間 SetPixel (hdc, -cx + center.x, -cy + center.y, col); // 225-270 度の間 SetPixel (hdc, cx + center.x, -cy + center.y, col); // 270-315 度の間 SetPixel (hdc, cy + center.x, -cx + center.y, col); // 315-360 度の間 } } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){ RECT rc; static POINT center; HDC hdc; PAINTSTRUCT ps; int RADIUS = 100; switch (message){ case WM_SIZE: InvalidateRect(hwnd, NULL, TRUE); break; case WM_PAINT: GetClientRect(hwnd, &rc); center.x = rc.right/ 2; center.y = rc.bottom/ 2; hdc = BeginPaint (hwnd, &ps); // 中心線描画(水平) MoveToEx (hdc, 0, center.y, NULL); LineTo (hdc, rc.right, center.y); // 中心線描画(垂直) MoveToEx (hdc, center.x, 0, NULL); LineTo (hdc, center.x, rc.bottom); // シンプル円描画(小数切捨て) ReallySimpleCircle (hdc, RADIUS, center, RGB(255, 0, 0)); // シンプル円描画(値のより近いピクセルを選択する方式) center.x += 20; ReallySimpleCircle2 (hdc, RADIUS, center, RGB(0, 255, 0)); // Win32API GDI 円描画 center.x += 20; Win32APIGDICircle (hdc, RADIUS, center); // ブレゼンハム(Bresenham)の円描画 (旧版) center.x += 20; BresenhamCircle (hdc, RADIUS, center, RGB(255, 0, 255)); // ミッチェナー(Miechener) の円描画 center.x += 20; MiechenerCircle (hdc, RADIUS, center, RGB(0, 0, 255)); // ブレゼンハム(Bresenham)の中点分岐の円描画 center.x += 20; MidpointCircle (hdc, RADIUS, center, RGB(0, 100, 100)); EndPaint (hwnd, &ps); break; case WM_DESTROY: PostQuitMessage (0); break; } return DefWindowProc (hwnd, message, wParam, lParam); } int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow){ static TCHAR szAppName[] = TEXT ("CircleDemoApp"); HWND hwnd; MSG msg; WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor (NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = szAppName; if (!RegisterClass (&wndclass)){ MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT("円描画の比較(Comparing Circle Drawings)"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 500, 300, NULL, NULL, hInstance, NULL); ShowWindow (hwnd, iCmdShow); UpdateWindow (hwnd); while (GetMessage (&msg, NULL, 0, 0)){ TranslateMessage (&msg); DispatchMessage (&msg); } return msg.wParam; } |