像 Photoshop 的椭圆工具一样绘制圆形

Bri*_*erk 5 python algorithm geometry drawing pixel

问题

我想要绘制一个与 Photoshop 的椭圆工具完全相同的实心圆(精确到像素)。

我不想实现任何抗锯齿,所以我的问题可以被视为循环二进制掩码生成问题。实现相同的圆一开始似乎相当简单,但是,我现在已经实现了 5 种以上不同的圆绘制算法,但没有一个能够完全匹配 Photoshop 的圆生成。


尝试的解决方案

我使用 Python 进行算法编程,使用 numpy 进行数组操作,使用 OpenCV 进行圆形绘制实现/洪水填充,使用 Scikit-Image 进行图像保存功能。我将问题表述为一个函数,该函数在给定图像上绘制给定半径和 x,y 位置的圆,并返回一个在其上绘制圆的新图像。

我已经编程/使用了以下圆形绘制实现:

  1. 朴素毕达哥拉斯方法
  2. OpenCV 的圈子
  3. 布雷森纳姆圆环
  4. 算术圈
  5. 安德烈斯圈子

测试解决方案

这些算法在两种不同的场景中进行了测试:一个半径为 10 像素的小圆和一个半径为 257 像素的大圆。小圆圈用于比较更明显的视觉差异,而大圆圈用于针对 Photoshop 中生成的相同半径的圆计算误差度量。

以下是这些测试的结果:

小圆圈测试

上述六种算法被指示在 40x40px 图像的中心绘制一个 10px 半径的圆。为了便于比较,我将所有结果合并起来。

结果

  • 蓝色圆圈是真实的、图形化的圆圈,已缩放以适合 20 像素的直径。

大圆圈测试

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

作为参考,在 Photoshop 中生成的圆如下:

这里

朴素毕达哥拉斯学派:

  • 像素差异:1082
  • 差异结果图像:

结果

开放式CV:

  • 像素差异:724
  • 差异结果图像:

结果

布雷森纳姆:

  • 像素差异:251
  • 差异结果图像:

结果

中点:

  • 像素差异:224
  • 差异图像:

结果

算术圈:

本实施例中对参数进行了优化。

  • 像素差异(未经优化):155
  • 像素不同(经过优化):107(最佳结果)
  • 差异图像(经过优化):

结果

安德烈斯圈子:

  • 像素差:155
  • 差异图像:

结果


最后的想法

在这个阶段,我在使用算术圆算法方面取得了最大的成功,但是,我仍然不 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)