python - 在skimage中绘制渐变椭圆

标签 python opencv graphics scikit-image

我想在 skimage 中绘制一个渐变颜色的椭圆蒙版。颜色从椭圆内开始到椭圆外结束。如何用 skimage 或 open-cv 绘制它?

像下图:
Ellipse with gradual change color

最佳答案

介绍
让我们从详细描述示例图像开始。

  • 它是一个 4 channel 图像(RGB + alpha 透明度),但它只使用灰色阴影。
  • 图像非常适合绘图,形状周围只有很小的边距。
  • 有一个填充的、抗锯齿的、旋转的外椭圆,被透明背景包围。
  • 外椭圆填充有旋转的椭圆渐变(同心,并与椭圆旋转相同),边缘为黑色,在中心线性渐变为白色。
  • 外椭圆与同心、填充、抗锯齿、旋转的内椭圆重叠(同样旋转,两个轴以相同的比例缩放)。填充颜​​色为白色。

  • 此外,让:
  • ab是椭圆的半长轴和半短轴
  • theta是椭圆围绕其中心的旋转角度,以弧度为单位(在 0 a 点沿 x 轴)
  • inner_scale是内部椭圆与外部椭圆的对应轴之间的比率(即 0.5 表示内部按比例缩小 50%)
  • hk是椭圆中心的 (x,y) 坐标

  • 在演示的代码中,我们将使用以下导入:
    import cv2
    import numpy as np
    import math
    
    并将定义我们绘图的参数(我们将计算 hk )设置为:
    a, b = (360.0, 200.0) # Semi-major and semi-minor axis
    theta = math.radians(40.0) # Ellipse rotation (radians)
    inner_scale = 0.6 # Scale of the inner full-white ellipse
    

    第1步
    为了生成这样的图像,我们需要采取的第一步是计算我们需要的“ Canvas ”(我们将要绘制的图像)的大小。为此,我们可以计算旋转外椭圆的边界框并在其周围添加一些小的边距。
    我不知道现有的 OpenCV 函数可以有效地执行此操作,但是 StackOverflow 节省了时间——已经有一个相关的 question带有 answer链接到有用的 article讨论问题。我们可以使用这些资源来提出以下 Python 实现:
    def ellipse_bbox(h, k, a, b, theta):
        ux = a * math.cos(theta)
        uy = a * math.sin(theta)
        vx = b * math.cos(theta + math.pi / 2)
        vy = b * math.sin(theta + math.pi / 2)
        box_halfwidth = np.ceil(math.sqrt(ux**2 + vx**2))
        box_halfheight = np.ceil(math.sqrt(uy**2 + vy**2))
        return ((int(h - box_halfwidth), int(k - box_halfheight))
            , (int(h + box_halfwidth), int(k + box_halfheight)))
    
    注意:我将浮点大小四舍五入,因为我们必须覆盖整个像素,并将左上角和右下角作为整数 (x,y) 对返回。
    然后我们可以通过以下方式使用该函数:
    # Calculate the image size needed to draw this and center the ellipse
    _, (h, k) = ellipse_bbox(0, 0, a, b, theta) # Ellipse center
    h += 2 # Add small margin
    k += 2 # Add small margin
    width, height = (h*2+1, k*2+1) # Canvas size
    

    第2步
    第二步是生成透明层。这是一个单 channel 8 位图像,其中黑色 (0) 代表完全透明,白色 (255) 代表完全不透明像素。这个任务相当简单,因为我们可以使用 cv2.ellipse .
    我们可以以 RotatedRect 的形式定义我们的外椭圆结构(紧密贴合椭圆的旋转矩形)。在 Python 中,这表示为包含以下内容的元组:
  • 表示旋转矩形中心的元组(x 和 y 坐标)
  • 表示旋转矩形大小的元组(其宽度和高度)
  • 以度为单位的旋转角度

  • 这是代码:
    ellipse_outer = ((h,k), (a*2, b*2), math.degrees(theta))
    
    transparency = np.zeros((height, width), np.uint8)
    cv2.ellipse(transparency, ellipse_outer, 255, -1, cv2.LINE_AA)
    
    ...以及它产生的图像:
    Transparency layer

    第 3 步
    第三步,我们创建一个包含我们想要的旋转椭圆梯度的单 channel (灰度或强度)图像。但首先,我们如何根据图像的笛卡尔(x, y) 在数学上定义旋转椭圆坐标,使用我们的 (a, b) , theta (θ) 和 (h, k)参数?
    这一次 Mathematics StackExchange 拯救了这一天:有一个 question完全匹配我们的问题,还有一个 answer提供这个有用的方程:
    Rotated ellipse formula
    请注意,对于我们从椭圆中心取的任何方向,左侧在椭圆周长处的计算结果为 1。它在中心为 0,在周边向 1 线性增加,然后进一步超过它。
    让我们调用右手边 weight因为没有更好的词。由于它从中心向外扩展得很好,我们可以用它来计算我们想要的梯度。我们的公式在外部为我们提供了白色(在浮点图像的情况下为 1.0),在中心为黑色(0.0)。我们想要倒数,所以我们只需减去 weight来自 1.0并将结果剪辑到范围 [0.0, 1.0] .
    让我们从一个简单的仅 Python 实现开始(如手动迭代代表我们图像的 numpy.array 的各个元素)来计算权重。然而,由于我们是懒惰的程序员,我们将使用 Numpy 来转换计算出的 weight s 到分级图像,使用矢量化减法,以及 numpy.clip .
    这是代码:
    def make_gradient_v1(width, height, h, k, a, b, theta):
        # Precalculate constants
        st, ct =  math.sin(theta), math.cos(theta)
        aa, bb = a**2, b**2
    
        weights = np.zeros((height, width), np.float64)    
        for y in range(height):
            for x in range(width):
                weights[y,x] = ((((x-h) * ct + (y-k) * st) ** 2) / aa
                    + (((x-h) * st - (y-k) * ct) ** 2) / bb)
                
        return np.clip(1.0 - weights, 0, 1)
    
    ...以及它产生的图像:
    Rotated elliptical inverse gradient
    这一切都很好,很花哨,但是由于我们遍历每个像素并在 Python 解释器中进行计算,所以它也是 aaawwwfffuuullllllyyy ssslllooowww .... 可能需要一秒钟,但我们正在使用 Numpy,所以如果我们使用 Numpy,我们肯定可以做得更好利用它。这意味着我们可以矢量化任何东西。
    首先,让我们注意到唯一变化的输入是每个给定像素的坐标。这意味着要矢量化我们的算法,我们需要两个数组(与图像大小相同)作为输入,其中包含 xy每个像素的坐标。幸运的是,Numpy 为我们提供了生成此类数组的工具—— numpy.mgrid .我们可以写
    y,x = np.mgrid[:height,:width]
    
    生成我们需要的输入数组。但是,让我们观察一下,我们从不使用 xy直接——而是我们总是用一个常数来抵消它们。让我们通过生成 x-h 来避免偏移操作和 y-k ...
    y,x = np.mgrid[-k:height-k,-h:width-h]
    
    我们可以再次预先计算 4 个常量,除此之外,其余的只是向量化 addition , subtraction , multiplication , divisionpowers ,这些都是由 Numpy 作为矢量化操作提供的(即更快)。
    def make_gradient_v2(width, height, h, k, a, b, theta):
        # Precalculate constants
        st, ct =  math.sin(theta), math.cos(theta)
        aa, bb = a**2, b**2
            
        # Generate (x,y) coordinate arrays
        y,x = np.mgrid[-k:height-k,-h:width-h]
        # Calculate the weight for each pixel
        weights = (((x * ct + y * st) ** 2) / aa) + (((x * st - y * ct) ** 2) / bb)
    
        return np.clip(1.0 - weights, 0, 1)
    
    与仅使用 Python 的脚本相比,使用此版本的脚本需要大约 30% 的时间。没什么了不起的,但它产生了相同的结果,而且这项任务似乎不是你必须经常做的事情,所以对我来说已经足够了。
    如果您 [读者] 知道更快的方法,请将其作为答案发布。
    现在我们有一个浮点图像,其强度范围在 0.0 到 1.0 之间。为了生成我们的结果,我们想要一个 8 位图像,其值在 0 到 255 之间。
    intensity = np.uint8(make_gradient_v2(width, height, h, k, a, b, theta) * 255)
    

    第四步
    第四步——画内椭圆。这很简单,我们以前做过。我们只需要适本地缩放轴。
    ellipse_inner = ((h,k), (a*2*inner_scale, b*2*inner_scale), math.degrees(theta))
    
    cv2.ellipse(intensity, ellipse_inner, 255, -1, cv2.LINE_AA)
    
    这为我们提供了以下强度图像:
    Gradient with inner ellipse

    第 5 步
    第五步——我们快到了。我们所要做的就是将强度和透明度图层组合成一个 BGRA 图像,然后将其保存为 PNG。
    result = cv2.merge([intensity, intensity, intensity, transparency])
    
    注意:对红色、绿色和蓝色使用相同的强度只会给我们带来灰色阴影。
    当我们保存结果时,我们得到以下图像:
    Result image

    结论
    鉴于我已经估计了您用于生成示例图像的参数,我想说我的脚本的结果非常接近。它的运行速度也相当快——如果你想要更好的东西,你可能无法避免接近裸机(C、C++ 等)。更聪明的方法,或者 GPU 可能做得更好。值得尝试...
    总而言之,这里有一个小演示,说明此代码也适用于其他旋转:
    Mosaic of ellipses rotated in 10 degree increments
    以及我用来写这个的完整脚本:
    import cv2
    import numpy as np
    import math
    
    # ============================================================================
    
    def ellipse_bbox(h, k, a, b, theta):
        ux = a * math.cos(theta)
        uy = a * math.sin(theta)
        vx = b * math.cos(theta + math.pi / 2)
        vy = b * math.sin(theta + math.pi / 2)
        box_halfwidth = np.ceil(math.sqrt(ux**2 + vx**2))
        box_halfheight = np.ceil(math.sqrt(uy**2 + vy**2))
        return ((int(h - box_halfwidth), int(k - box_halfheight))
            , (int(h + box_halfwidth), int(k + box_halfheight)))
    
    # ----------------------------------------------------------------------------
            
    # Rotated elliptical gradient - slow, Python-only approach
    def make_gradient_v1(width, height, h, k, a, b, theta):
        # Precalculate constants
        st, ct =  math.sin(theta), math.cos(theta)
        aa, bb = a**2, b**2
    
        weights = np.zeros((height, width), np.float64)    
        for y in range(height):
            for x in range(width):
                weights[y,x] = ((((x-h) * ct + (y-k) * st) ** 2) / aa
                    + (((x-h) * st - (y-k) * ct) ** 2) / bb)
                
        return np.clip(1.0 - weights, 0, 1)
    
    # ----------------------------------------------------------------------------
        
    # Rotated elliptical gradient - faster, vectorized numpy approach
    def make_gradient_v2(width, height, h, k, a, b, theta):
        # Precalculate constants
        st, ct =  math.sin(theta), math.cos(theta)
        aa, bb = a**2, b**2
            
        # Generate (x,y) coordinate arrays
        y,x = np.mgrid[-k:height-k,-h:width-h]
        # Calculate the weight for each pixel
        weights = (((x * ct + y * st) ** 2) / aa) + (((x * st - y * ct) ** 2) / bb)
    
        return np.clip(1.0 - weights, 0, 1)
    
    # ============================================================================ 
    
    def draw_image(a, b, theta, inner_scale, save_intermediate=False):
        # Calculate the image size needed to draw this and center the ellipse
        _, (h, k) = ellipse_bbox(0,0,a,b,theta) # Ellipse center
        h += 2 # Add small margin
        k += 2 # Add small margin
        width, height = (h*2+1, k*2+1) # Canvas size
    
        # Parameters defining the two ellipses for OpenCV (a RotatedRect structure)
        ellipse_outer = ((h,k), (a*2, b*2), math.degrees(theta))
        ellipse_inner = ((h,k), (a*2*inner_scale, b*2*inner_scale), math.degrees(theta))
    
        # Generate the transparency layer -- the outer ellipse filled and anti-aliased
        transparency = np.zeros((height, width), np.uint8)
        cv2.ellipse(transparency, ellipse_outer, 255, -1, cv2.LINE_AA)
        if save_intermediate:
            cv2.imwrite("eligrad-t.png", transparency) # Save intermediate for demo
    
        # Generate the gradient and scale it to 8bit grayscale range
        intensity = np.uint8(make_gradient_v1(width, height, h, k, a, b, theta) * 255)
        if save_intermediate:
            cv2.imwrite("eligrad-i1.png", intensity) # Save intermediate for demo
        
        # Draw the inter ellipse filled and anti-aliased
        cv2.ellipse(intensity, ellipse_inner, 255, -1, cv2.LINE_AA)
        if save_intermediate:
            cv2.imwrite("eligrad-i2.png", intensity) # Save intermediate for demo
    
        # Turn it into a BGRA image
        result = cv2.merge([intensity, intensity, intensity, transparency])
        return result
    
    # ============================================================================ 
    
    a, b = (360.0, 200.0) # Semi-major and semi-minor axis
    theta = math.radians(40.0) # Ellipse rotation (radians)
    inner_scale = 0.6 # Scale of the inner full-white ellipse
        
    cv2.imwrite("eligrad.png", draw_image(a, b, theta, inner_scale, True))
    
    # ============================================================================ 
    
    rows = []
    for j in range(0, 4, 1):
        cols = []
        for i in range(0, 90, 10):
            tile = np.zeros((170, 170, 4), np.uint8)
            image = draw_image(80.0, 50.0, math.radians(i + j * 90), 0.6)
            tile[:image.shape[0],:image.shape[1]] = image
            cols.append(tile)
        rows.append(np.hstack(cols))
    
    cv2.imwrite("eligrad-m.png", np.vstack(rows))
    

    注意:如果您发现这篇文章有任何愚蠢的错误、令人困惑的术语或任何其他问题,请随时留下 build 性的评论,或者直接编辑答案以使其更好。我知道有一些方法可以进一步优化这一点——让读者来做这个练习(也许会提供免费的答案)。

    关于python - 在skimage中绘制渐变椭圆,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49829783/

    相关文章:

    python - 如何检查你的文件是否左对齐

    android - FFmpegFrameGrabber.start() 在加载 mp4 文件时崩溃

    image - 立方到等角投影算法

    python : Split list based on negative integers

    python - 元类和 __slots__?

    java - 编译opencv时出现"cannot find symbol"

    python - 如何使用 Python OpenCV 将灰度图像转换为热图图像

    iphone - 对于非常动态的数据使用顶点缓冲区对象在性能方面是一个好主意吗?

    java - 访问 BufferedImage 线程是否安全

    python - 在 Django 管理中为子模型添加按钮