Bri*_*erk 5 python algorithm geometry drawing pixel
我想要绘制一个与 Photoshop 的椭圆工具完全相同的实心圆(精确到像素)。
我不想实现任何抗锯齿,所以我的问题可以被视为循环二进制掩码生成问题。实现相同的圆一开始似乎相当简单,但是,我现在已经实现了 5 种以上不同的圆绘制算法,但没有一个能够完全匹配 Photoshop 的圆生成。
我使用 Python 进行算法编程,使用 numpy 进行数组操作,使用 OpenCV 进行圆形绘制实现/洪水填充,使用 Scikit-Image 进行图像保存功能。我将问题表述为一个函数,该函数在给定图像上绘制给定半径和 x,y 位置的圆,并返回一个在其上绘制圆的新图像。
我已经编程/使用了以下圆形绘制实现:
这些算法在两种不同的场景中进行了测试:一个半径为 10 像素的小圆和一个半径为 257 像素的大圆。小圆圈用于比较更明显的视觉差异,而大圆圈用于针对 Photoshop 中生成的相同半径的圆计算误差度量。
以下是这些测试的结果:
上述六种算法被指示在 40x40px 图像的中心绘制一个 10px 半径的圆。为了便于比较,我将所有结果合并起来。

上述六种算法被指示在 2370x1770px 图像的中心绘制一个 257px 半径的圆。将每个算法生成的圆从 Photoshop 中绘制的 257 像素半径圆中减去,以获得差值。
作为参考,在 Photoshop 中生成的圆如下:

朴素毕达哥拉斯学派:

开放式CV:

布雷森纳姆:

中点:

算术圈:
本实施例中对参数进行了优化。

安德烈斯圈子:

在这个阶段,我在使用算术圆算法方面取得了最大的成功,但是,我仍然不 100% 相信调整该算法将允许模拟 Photoshop 的椭圆绘制行为。
在 Photoshop 中绘制圆圈时,有一些边缘情况我没有成功复制。例如:一个 2x2px 的圆被绘制为 2x1 像素的矩形。
另一个奇怪的行为是,特定半径的圆不表现出大多数圆绘制行为中使用的八分圆对称性。我认为这是因为椭圆工具用于绘制仅表现出象限对称性的椭圆。因此,即使正在绘制具有八分圆对称性的圆,仍然会绘制象限,也许吗?
作为最后一点思考,我在 Photoshop 中绘制了半径逐渐减小和颜色交替的重叠圆圈。可以看到星形几何形状与以类似方式绘制的布雷森汉姆圆非常相似。这让我想知道 Photoshop 是否使用了修改后的 Bresenham 圆?

无论如何,感谢您阅读到目前为止!如果能获得一些关于我可以采取哪些措施来使其更加准确的意见,那就太好了。
谢谢你!
上述测试/实现的代码如下:
import math
from skimage import io
import cv2
import numpy as np
def draw_octant_half_integer(circle_img, centre_x, centre_y, x, y):
circle_img[centre_x + x, centre_y + y] = 255
circle_img[centre_x + x, centre_y - y + 1] = 255
circle_img[centre_x - x + 1, centre_y + y] = 255
circle_img[centre_x - x + 1, centre_y - y + 1] = 255
circle_img[centre_x + y, centre_y + x] = 255
circle_img[centre_x + y, centre_y - x + 1] = 255
circle_img[centre_x - y + 1, centre_y + x] = 255
circle_img[centre_x - y + 1, centre_y - x + 1] = 255
def draw_octant(img, centre_x, centre_y, x, y):
img[centre_x + x, centre_y + y] = 255
img[centre_x - x, centre_y + y] = 255
img[centre_x + x, centre_y - y] = 255
img[centre_x - x, centre_y - y] = 255
img[centre_x + y, centre_y + x] = 255
img[centre_x - y, centre_y + x] = 255
img[centre_x + y, centre_y - x] = 255
img[centre_x - y, centre_y - x] = 255
def create_pythagorean_circle(img, centre_x, centre_y, radius):
def point_on_circumference(x, y, centre_x, centre_y, radius):
x -= centre_y
y -= centre_x
hypot = math.sqrt(x*x + y*y)
return hypot >= radius - 0.5 and hypot <= radius + 0.5
circle_img = img.copy()
height, width = circle_img.shape
for y in range(0, height):
for x in range(0, width):
if point_on_circumference(x, y, centre_x, centre_y, radius):
circle_img[x, y] = 255
cv2.floodFill(circle_img, None, (int(centre_x), int(centre_y)), 255)
return circle_img
def create_bresenham_circle(img, centre_y, centre_x, radius):
height, width = img.shape
circle_img = img.copy()
x = 0
y = radius
d = 3 - 2 * radius
draw_octant_half_integer(circle_img, centre_x, centre_y, x, y)
while y >= x:
x += 1
if d > 0:
y -= 1
d = d + 4 * (x - y) - 10
else:
d = d + 4 * x + 6
draw_octant_half_integer(circle_img, centre_x, centre_y, x, y)
cv2.floodFill(circle_img, None, (int(centre_y), int(centre_x)), 255)
return circle_img
def create_midpoint_circle(img, centre_y, centre_x, radius):
x = radius
y = 0
circle_img = img.copy()
p = 1 - radius
circle_img[centre_x + x, centre_y + y] = 255
if radius > 0:
circle_img[centre_x + y, centre_y - x] = 255
circle_img[centre_x - x + 1, centre_y + y] = 255
circle_img[centre_x - y + 1, centre_y + x] = 255
while x > y:
y += 1
if p < -1:
p = p + 2 * y + 1
else:
x -= 1
p = p + 2 * y - 2 * x + 1
if x < y:
break
circle_img[centre_x + x, centre_y + y] = 255
circle_img[centre_x - x + 1, centre_y + y] = 255
circle_img[centre_x + x, centre_y - y + 1] = 255
circle_img[centre_x - x + 1, centre_y - y + 1] = 255
if x != y:
circle_img[centre_x + y, centre_y + x] = 255
circle_img[centre_x - y + 1, centre_y + x] = 255
circle_img[centre_x + y, centre_y - x + 1] = 255
circle_img[centre_x - y + 1, centre_y - x + 1] = 255
cv2.floodFill(circle_img, None, (int(centre_y), int(centre_x)), 255)
return circle_img
def create_arithmetic_circle(img, centre_x, centre_y, radius, up=0.5, down=0.5):
def decision_parameter(x, y):
hypot = x**2 + y**2
return (radius - up)**2 <= hypot and hypot <= (radius + down)**2
x = 1
y = radius
circle_img = img.copy()
while y >= x:
draw_octant_half_integer(circle_img, centre_x, centre_y, x, y)
if decision_parameter(x, y - 1):
y -= 1
elif decision_parameter(x + 1, y):
x += 1
else:
x += 1
y -= 1
circle_img = circle_img.astype(np.uint8)
cv2.floodFill(circle_img, None, (int(centre_y), int(centre_x)), 255)
return circle_img
def andres_circle(img, centre_x, centre_y, radius):
x = 0
y = int(radius)
d = radius - 1
circle_img = img.copy()
while y >= x:
draw_octant_half_integer(circle_img, centre_x, centre_y, x, y)
if d <= 2 * (radius - y):
d += 2 * y - 1
y -= 1
elif d > 2 * x:
d -= 2 * x + 1
x += 1
else:
d += 2 * (y - x - 1)
x += 1
y -= 1
circle_img = circle_img.astype(np.uint8)
cv2.floodFill(circle_img, None, (int(centre_y), int(centre_x)), 255)
return circle_img
if __name__ == "__main__":
# Loading Constants
photoshop_circle = cv2.imread("photoshop_circle.png", cv2.IMREAD_GRAYSCALE)
width = 2370
height = 1770
centre_x = width/2
centre_y = height/2
radius = 257
img = np.zeros((height, width))
img = img.astype(np.uint8)
# Optimizing Arithmetic Circle
# lowest_mse = np.inf
# lowest_up = 0
# lowest_down = 0
# for up in np.linspace(0.76, 0.77, 100):
# for down in np.linspace(0.49, 0.50, 100):
# circle = create_arithmetic_circle(img, int(round(centre_y))-1, int(round(centre_x))-1, radius, up=up, down=down)
# diff = photoshop_circle - circle
# mse = np.sum(diff**2)
# print("MSE:", mse, "Up:", up, "Down:", down)
# if mse < lowest_mse:
# lowest_mse = mse
# lowest_up = up
# lowest_down = down
# print("Optimal MSE:", lowest_mse, "Optimal Up:", lowest_up, "Lowest Down:", lowest_down)
# Drawing/Computing Difference
# circle = createPythagorasCircle(img, centre_x, centre_y, radius)
# circle = cv2.circle(img.copy(), (int(centre_x), int(centre_y)), radius, 255, cv2.FILLED)
# circle = create_bresenham_circle(img, int(round(centre_x))-1, int(round(centre_y))-1, radius)
# circle = createMidpointCircle(img, int(round(centre_x))-1, int(round(centre_y))-1, radius)
# Unoptimized
# circle = create_arithmetic_circle(img, int(round(centre_y))-1, int(round(centre_x))-1, radius)
# Optimized
# circle = create_arithmetic_circle(img, int(round(centre_y))-1, int(round(centre_x))-1, radius, up=0.7638, down=0.4976)
circle = andres_circle(img, int(round(centre_y))-1, int(round(centre_x))-1, radius)
diff = photoshop_circle - circle
diff[diff > 0] = 1
error = np.sum(diff)
print("Pixels Different:", error, "Radius:", radius)
diff[diff < 0] = 255
diff[diff > 0] = 255
io.imsave("circle.png", diff, check_contrast=False)
Run Code Online (Sandbox Code Playgroud)