Search code examples
pythonopencvdrawingellipse

how to force cv2.ellipse to draw a clockwise circular arc?


cv2.ellipse seems to have a mind of its own about which way it interprets start-and end angles. I want to draw a circular arc from 300° to 130° in the clockwise direction, i.e. like this:

Image

with this code:

import numpy as np
import cv2

center = (200, 200) # x,y
axes = (100, 100) # first, second
angle = 0. # clockwise, first axis, starts horizontal
for i in range( 330,130,-1):
  image = np.zeros((400, 400, 3)) # creates a black image
  image = cv2.ellipse(image, center, axes, angle, 0., 360, (0,0,255))
  image = cv2.ellipse(image, center, axes, angle, 330, i, (0,255,0))
  cv2.imshow("image", image)
  cv2.waitKey(5)

cv2.waitKey(0)
cv2.destroyAllWindows()

I got this instead, with the green segment being drawn counter-clockwise from 330 to 130 instead of clockwise.

Image

The code generates the red circle first and then overlays the green arc, but it demonstrates the problem.


Solution

  • Note: You should have probably mentioned that your code comes from [SO]: Understanding the ellipse parameters in Open CV using Python (@api55's answer).

    • Listing [OpenCV.Docs]: Drawing Functions - ellipse()

    • Everything is a bit complicated since the display xOy system is flipped vertically ((+∞ on) Y axis is pointing down) from what we are used to (on paper), the origin is the top left corner instead of the bottom left one (and the 1st quadrant is corresponding to the 4th).

    Given a circle, and 2 points on it A and B, where each point name is also the degrees (A, B ∈ [0, 360)), there are 2 ways to move from A to B (on the circle), as there are 2 arches. Choosing which arch to take has the following implications:

    1. How much to move (distance):

      • Arch 1 (let this be the shorter): D1 = abs(B - A) - as distance (even in angles) is always positive

      • Arch 2: D2 = 360 - D1 - the remainder of the circle

    2. Direction to move to (CCW, CW). This translates to the angle increment being added / subtracted to / from the initial value (A).
      Here, I must also mention that moving from B to A is like moving from A to B but in the opposite direction

    The 2 things I mentioned above (combined with the fact that CCW is the default direction), translate into the 2 if clauses in the modified version of your code (below). Maybe the conditions are not very obvious (that's why I tried expanding them in the commented lines - code has same effect), but I couldn't find a nicer version (yet).
    Anyway, it should work for any angles and directions.

    code00.py:

    #!/usr/bin/env python
    
    import sys
    
    import numpy as np
    import cv2
    
    
    def main(*argv):
        center = (200, 200)
        axes = (100, 100)
        angle = 0.
        start = 330
        end = 130
        cw = 0 #len(sys.argv) > 1  # @TODO cfati: ClockWise?
        if start < end:
            steps = 360 + start - end
        else:
            steps = start - end
        if cw:
            steps = 360 - steps
            cw_factor = 1
        else:
            cw_factor = -1
        """
        # The above if conditions, expanded (same effect)
        if start < end:
            if cw:
                steps = end - start
                cw_factor = 1
            else:
                steps = 360 + start - end
                cw_factor = -1
        else:
            if cw:
                steps = 360 - start + end
                cw_factor = 1
            else:
                steps = start - end
                cw_factor = -1
        """
        step = 1
        base_color = (0, 0, 255)
        active_color = (0, 255, 0)
        image = np.zeros((400, 400, 3))
        for i in range(1, steps, step):
            cv2.ellipse(image, center, axes, angle, 0, 360, base_color)
            cv2.ellipse(image, center, axes, angle, start, start + cw_factor * i, active_color)
            cv2.imshow("image", image)
            cv2.waitKey(1)
    
        print("Close drawing window to exit...")
        cv2.waitKey(0)
        cv2.destroyAllWindows()
    
    
    if __name__ == "__main__":
        print(
            "Python {:s} {:03d}bit on {:s}\n".format(
                " ".join(elem.strip() for elem in sys.version.split("\n")),
                64 if sys.maxsize > 0x100000000 else 32,
                sys.platform,
            )
        )
        rc = main(*sys.argv[1:])
        print("\nDone.\n")
        sys.exit(rc)