学习完Bresenham直线算法,就又轮到了Bresenham画圆算法了,其实画圆算法与画直线算法的原理差不多,但是整体添加了一些别的功能,下面是我自己学习的一些笔记,该文章只是个人理解以及算法的简单实现,同时我在实现这个算法的时候并没有很好的考虑到算法的复杂度等条件,因此可能我自己算法的代码会相当的愚蠢,具体有什么可以改进以及错误,请各位大佬们指出。
在开始前我们依旧讲讲屏幕显示的原理:这里我们还是复制我上一篇文章的解释
屏幕像素原理:
就像上图中的,屏幕中的像素是像一个表格一样,每个像素都是有指定大小的像素格子,而每个格子中都有相应的像素值,这个像素值是有关RGB三种颜色的具体表示数值,我们通过这些格子以及格子中的像素值,就能显示各种图像
那么我们可以发现,在屏幕上画一个圆会比纸张上面画一个圆难一点,而Bresenham算法则能很好的让圆显示在屏幕的像素中
圆的标准方程
圆可以使用标准方程来表示,其中为圆心坐标,为圆的半径,而则是在圆上的点的坐标,通过这个方程我们可以表达圆上的任意一个点
而我们需要的是通过输入一个横轴X的整数值来获取对应的圆上的一个点的纵轴y的值,因此我们需要对标准方程变形为:
通过这个方程式,我们可以通过横轴的坐标来获取对应的圆的纵轴的坐标:
如下图
我们可以通过一个两点间距为无穷小的横轴 x 坐标来获取对应的圆的纵轴 y 坐标,从而画出一个(黑线)圆
算法在干啥
为了讲清楚算法到底在干什么,我们先将上面的图改一下,最后如下图所示:
在这幅图中,我们做出了粗黑色线的方框,这些方框可以看作是指定大小的屏幕点,我们还使用黑线表示圆心为(150, 150),半径为10的圆,而使用红线表示最后我们的算法所选择的像素点的值
在黑线中,我们将圆心以及半径带入到标准方程中,最终得:
然后我们选择求横坐标为153的圆的纵坐标值,最后求得在该横坐标下,y的值为159.54
这时我们有两种选择,一种就是选择上图中的像素1用于显示圆,另一种就是选择像素2用于显示圆,而由于y的值是159.54,更接近于160,因此最终算法将会选择像素1用于显示圆,后面的横坐标为154的圆也是同样的原理。
通过这个例子其实我们就能总结Bresenham画圆算法其实也是和直线算法相似,都是通过对比对应的y的值,选择距离该值最近的整数值
算法如何画圆
但是相比直线算法,Bresenham算法最大的区别就是不同的y值的计算方法,Bresenham通过采用将整个圆分成8块扇形的形式来画一个完整的圆,整体分割方式如下图:
而我们可以只计算其中一段扇形的所有整数y值,然后通过圆的对称性这一定理来计算出剩余的7份,最终画出一个完整的圆
而这里我的想法就是: 先计算出扇形1的所有横坐标x,然后再带入上面我们计算好的公式,就可以求出所有的纵坐标y,而这里我就使用了求圆弧上点的坐标的公式:
这两条公式中为圆的半径,为圆弧的弧长
而由于弧长又与圆弧的夹角度数有关,因此我们需要使用到角度转弧长公式:
这里的angle为圆弧的夹角角度,我们通常使用3.14进行计算
而这里我们需要的是扇形1号的所有的横坐标x的值,并且我们将圆分成了8份,因此每个扇形的夹角角度都为45度,因此我们带入进行计算求得上图中A点的纵坐标x,在算出所有的横坐标x过后就可以带入到我们上面求纵坐标公式中去计算该扇形的所有y的值,并通过y的值来求出所有用于显示圆的坐标点
因此个人想法的Bresenham画圆算法的整体处理为:通过计算1/8份圆的所有横坐标的值,进而计算出所有的纵坐标的值,最后再使用与直线算法相似的做法去选择像素值,最后通过圆的对称性将其他的7/8的圆的部分画出来
python程序实现算法
"""
BresenHam圆形算法
"""
import numpy as np
import matplotlib.pyplot as plt
def draw_circle(func):
def wrapper(usX, usY, usRadis):
res_dict = func(usX, usY, usRadis) # 这里用于接收
####################################这部分可以替换为具体的任务操作,例如下面的画出圆######################################
if usX > usY:
med_value = usX + usRadis
else:
med_value = usY + usRadis
array = np.zeros((med_value+10, med_value+10))
for key, values in res_dict.items():
print(f"{int(key)}, {int(values)}")
array[usY+values, int(key)] = 255 # 1号扇形部分
array[int(key), int(usY+values)] = 255 # 2号扇形部分
array[usY-values, int(key)] = 255 # 3号扇形部分
array[usX-(int(key)-usX), usY+values] = 255 # 4号扇形部分
array[usY-values, usX-(int(key)-usX)] = 255 # 5号扇形部分
array[usX-(int(key)-usX), usY-values] = 255 # 6号扇形部分
array[usY+int(key)-usY, int(usY-values)] = 255 # 7号扇形部分
array[int(usY+values), usX-(int(key)-usX)] = 255 # 8号扇形部分
plt.xticks([])
plt.yticks([])
plt.xlim(0, med_value)
plt.ylim(0, med_value)
plt.imshow(array, cmap="gray")
plt.show()
####################################这部分可以替换为具体的任务操作,例如下面的画出圆######################################
return wrapper
res_dict = dict()
@draw_circle
def BresenHam_circle(usX, usY, usRadis):
for x in range(usX, usX+int(usRadis * np.cos((45*np.pi)/180))+1):
y = np.sqrt(usRadis**2-(x-usX)**2)+usY
# print(f"{x}, {y}")
y_space = y - usY # 计算圆上的某个点的纵坐标与圆心的纵坐标的差值
if ( x == usX ): # 如果处于的是点是在圆心相同高度的圆的两边的点
res_dict[f"{x}"] = int(usRadis) # 直接赋予为圆心的同等纵坐标值
else: # 如果不是这两边的点
if ((y) % 1 > 0.5): # 并且如果圆上的点离上一个的像素点更近,则使用上一个像素点进行显示
res_dict[f"{x}"] = int(y_space)+1 # 这里就是圆对应于x的像素位置值与原型之间的差值
else: # 如果不是,则直接使用回最近的点
res_dict[f"{x}"] = int(y_space)
return res_dict
if __name__ == "__main__":
BresenHam_circle(150, 150, 10)
最后我们画出了:
但是需要注意的是,我们上面程序计算的圆弧的具体坐标是以圆心为(0,0)为基础的,而我们需要画的圆是圆心为(150, 150)的,因此我们在进行迭代输入算式的时候应该加上圆心的横坐标值
C++实现算法
/**
* @brief 计算出像素屏幕中指定圆心以及半径的圆所在的像素位置
* @param x: 圆心的横坐标
* @param y: 圆心的纵坐标
* @param radis: 圆的半径
*/
#include <cmath>
#include <math.h>
#define pi acos(-1)
void Draw_Circle(float x, float y, float radis)
{
float usX = 0;
float end_x = 0;
end_x = x + (int)(radis * cos((45 * pi) / 180));
for (float usX = x; usX <= end_x; usX++)
{
float usY_space = sqrt(powf(radis, 2) - powf((usX - x), 2)); // 这里计算出的是以(0,0)为圆点的编号1扇形的横坐标对应的y,因此后面需要加上圆心的y
float usY = usY_space + y; // 计算出指定圆心、半径以及指定的横坐标在圆上的点的纵坐标
int res_y = (fmodf(usY, 1) > 0.5) ? (int)usY + 1 : (int)usY;
/*************************************这部分可以替换成具体的任务操作************************************/
std::cout << "the x is: " << usX << "\t";
std::cout << "the y is: " << res_y << std::endl;
/*************************************这部分可以替换成具体的任务操作************************************/
}
}
int main(void)
{
Draw_Circle(150.0, 150.0, 10.0);
}
最后的输出与python程序实现的算法结果一致
C实现算法
#include <stdio.h>
#include <math.h>
#define pi acos(-1)
void Draw_Circle(float x, float y, float radis)
{
float end_x = x + (int)(radis * cos((45 * pi) / 180)); // 公式预测的45度角的横坐标
//printf("%f, %f", x, end_x);
for (float usX = x; usX <= end_x; usX++) // 遍历每一个整数横坐标
{
float y_space = sqrt(powf(radis, 2) - powf((usX - x), 2)); // 计算出指定圆心、半径以及指定的横坐标在圆上的点的纵坐标
//printf("the res_usY:%f\n", y+y_space);
int res_x_space = usX - x;
int res_y_space = (fmodf(y_space, 1) > 0.5) ? (int)y_space + 1 : (int)y_space;
/**************************************这里我们可以替换为具体的操作代码,用于画出圆****************************************/
printf("the x:%d\t", (int)(usX));
printf("the y:%d\n", (int)(y+res_y_space));
// /* 根据指定的x以及计算出来的y值我们使用相应的像素点进行显示 */
// ILI9806G_SetPointPixel((uint16_t)(x+res_x_space), (uint16_t)(y+res_y_space)); // 第一扇形
// ILI9806G_SetPointPixel((uint16_t)(y+res_y_space), (uint16_t)(x+res_x_space)); // 第二扇形
// ILI9806G_SetPointPixel((uint16_t)(y+res_y_space), (uint16_t)(x-res_x_space)); // 第三扇形
// ILI9806G_SetPointPixel((uint16_t)(x+res_x_space), (uint16_t)(y-res_y_space)); // 第四扇形
// ILI9806G_SetPointPixel((uint16_t)(x-res_x_space), (uint16_t)(y-res_y_space)); // 第五扇形
// ILI9806G_SetPointPixel((uint16_t)(y-res_y_space), (uint16_t)(x-res_x_space)); // 第六扇形
// ILI9806G_SetPointPixel((uint16_t)(y-res_y_space), (uint16_t)(x+res_x_space)); // 第七扇形
// ILI9806G_SetPointPixel((uint16_t)(x-res_x_space), (uint16_t)(y+res_y_space)); // 第八扇形
/**************************************这里我们可以替换为具体的操作代码,用于画出圆****************************************/
}
}
int main(void)
{
Draw_Circle(150, 150, 10);
}
我们将上面的代码烧录到单片机中看看结果,并且修改一下添加实心或空心选项
实验结果:
实心模式: 空心模式: