How to get angle of rotation of object

I’m trying to find the angle of rotation of an object (irregular shape, cannot share a picture. It looks sort of like a tree’s trunk, with a thinner base, two branches stemming from the lower middle (different sized branches) and a thicker top area). Currently my code is like this:

imgBlur = cv2.GaussianBlur(search_area, (7,7), 1)
imgGray = cv2.cvtColor(imgBlur, cv2.COLOR_BGR2GRAY)
img_canny = cv2.Canny(imgGray,threshold1, threshold2)
kernel = np.ones((5,5))
img_dilated = cv2.dilate(img_canny, kernel, iterations=1)
getContours(img_dilated, img_contour)

def getContours(img, imgContour):
    contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

  for cnt in contours:
        perimeter = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, 0.02 * perimeter, True) 
        x,y,w,h = cv2.boundingRect(approx)
        bounding_box_area = w*h
        
        if bounding_box_area < 15000 and bounding_box_area > 150:
            cv2.drawContours(imgContour, cnt, -1, (255, 0, 255), 2)
            cv2.rectangle(imgContour, (x,y), (x+w, y+h), (0,255,0), 2)

            [vx, vy, lx, ly] = cv2.fitLine(cnt, cv2.DIST_L2, 0, 0,.01, 0.01)
            lefty = int((-lx*vy/vx) + ly)
            righty = int(((img.shape[1]-lx)*vy/vx)+ly)

            M = cv2.moments(cnt)
            cx = cy = 0

            if M['m00'] != 0:
                cx = int(M['m10']/M['m00'])
                cy = int(M['m01']/M['m00'])
                cv2.circle(imgContour, (cx, cy), 3, (0, 0, 255), -1)

            if lefty < 100000 and righty < 100000:
              ret, p1, p2 = cv2.clipLine((x,y,w,h), (img.shape[1]-1,righty), (0,lefty)) 
              cv2.line(imgContour,p1, p2,255,2)

So far I managed to get the contour, the bounding box, the centroid and the fit line, but I still cannot find the angle of rotation. I tried all sorts of methods (minAreaRect, angle between fit line and arbitrary axis…): the best I found is this

              cnt = cnt.reshape(-1, 2).astype(np.float32)
              cnt -= np.array([[cx, cy]])
              mean, eigenvectors = cv2.PCACompute(cnt - np.array([[cx, cy]]), mean=None)

              angle = np.arctan2(eigenvectors[0,1], eigenvectors[0,0]) * 180 / np.pi

              if angle < 0:
                  angle += 360

but it has issues with some angles (jumping from 120 to 320, for example).

Maybe you want to use the Fourier-Mellin? some example code here:

From what I can tell, it’s used to detect a base image in an image that has been modified. How would that get me the angle?

My mistake, as you didnt provide any samole images I misunderstood your question. FM is not the solution then.

I’m not sure I can share the pictures I took with my current script, I’ll have to ask. If I can, I’ll post them in a comment.

why subtract the center twice ?

also, atan hs singularities/changes sign at ± 90 deg.
so your angle += 360 formula is wrong, too.

The entire second code snippet is wrong then, I tried to adapt some code I found online the way I thought made sense. I’m quite new to CV and trying to find the most versatile solution to this problem.

On a side note, I read up on Image Moments and Hu moments and how you can get the orientation using second order moments. I also found a practical application on a paper, specifically


   % Central moments (intermediary step) 
    a = E.m20/E.m00 - E.x^2; 
    b = 2*(E.m11/E.m00 - E.x*E.y); 
    c = E.m02/E.m00 - E.y^2;   
    % Orientation (radians)  
    E.theta = 1/2*atan(b/(a-c)) + (a<c)*pi/2;

I adapted it to Python, but the reported angle is still erratic and imprecise. Is this a viable path to get the angle of rotation or is this used for something else completely?

edited
raw

Here are the raw image and the image with contour, bounding box, rotated bounding box and fit line drawn on it.

So let’s see that code!

def getContours(img, imgContour):
    contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) #approssimazione semplice per ottenere forme geometriche
    cv2.putText(imgContour, str(len(contours)), (0, 20), cv2.FONT_HERSHEY_COMPLEX, .7,(0,255,0), 2) #conteggio pezzi presenti

    for cnt in contours:
        perimeter = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, 0.02 * perimeter, True)
        x,y,w,h = cv2.boundingRect(approx)
        bounding_box_area = w*h

        minRect = cv2.minAreaRect(cnt)
        box = cv2.boxPoints(minRect)
        box = np.intp(box)
        cv2.drawContours(imgContour, [box], 0, (0, 0, 0),2)

        if bounding_box_area > 150:
            cv2.drawContours(imgContour, cnt, -1, (255, 0, 255), 2)
            cv2.rectangle(imgContour, (x,y), (x+w, y+h), (0,255,0), 2)

            [vx, vy, lx, ly] = cv2.fitLine(cnt, cv2.DIST_L2, 0, 0.01, 0.01) 
            lefty = righty = 0
            with warnings.catch_warnings():
                warnings.filterwarnings("ignore",category=DeprecationWarning)
                lefty = int((-lx*vy/vx) + ly)
                righty = int(((img.shape[1]-lx)*vy/vx)+ly)

                if lefty < 100000 and righty < 100000: 
                    ret, p1, p2 = cv2.clipLine((x,y,w,h), (img.shape[1]-1,righty), (0,lefty)) 
                    cv2.arrowedLine(imgContour,p1, p2,255,2)

            M = cv2.moments(cnt)
            cx = cy = 0

            if M['m00'] != 0:
                cx = int(M['m10']/M['m00'])
                cy = int(M['m01']/M['m00'])
                cv2.circle(imgContour, (cx, cy), 3, (0, 0, 255), -1)

Some design choices might not be great (checking for bounding box area instead of area, ignoring deprecation warnings and that < 100000 check on the fit line y’s). Hopefully they aren’t disrupting the code’s functionality.