I've implemented the following simple code:
import cv2
import numpy as np
nr_im = 9876
font = cv2.FONT_HERSHEY_SIMPLEX
fontScale = 1
colorText = (0, 0, 255)
thickness = 2
img = cv2.imread('testing/' + str(nr_im) + '.jpg')
original = img.copy()
blured_img = cv2.GaussianBlur(img,(17,17),5)
image = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower = np.array([0, 0, 140], dtype="uint8")
upper = np.array([0, 0, 255], dtype="uint8")
mask = cv2.inRange(image, lower, upper)
# Morphological Closing: Get rid of the noise inside the object
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (25, 25)))
# Find contours
cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
print(len(cnts))
cntsElps = []
for num_cnt, cnt in enumerate(cnts):
genEllipse= cv2.fitEllipse(cnt)
cntsElps.append(genEllipse)
cv2.ellipse(original,genEllipse,(0,255,0),2)
cv2.putText(original, str(num_cnt+1), (int(genEllipse[0][0]),int(genEllipse[0][1])), font, fontScale, colorText, thickness, cv2.LINE_AA)
print("Ellipse nb: " + str(num_cnt+1) + " has angle: " + str(genEllipse[2]) + "\n")
cv2.imwrite('testing/' + str(nr_im) + '_' + 'trash2' + '.png', original)
And I used this image as example:

I've got the following image result:

And the rotation angle for each ellipse was:
- Ellipse nb: 1 has angle: 55.63788986206055
- Ellipse nb: 2 has angle: 108.58539581298828
- Ellipse nb: 3 has angle: 170.23861694335938
- Ellipse nb: 4 has angle: 73.59089660644531
So, my conclusion is that an angle between vertical axis and major side of rectangle(=major ellipse axis) is the rotation angle in fitEllipse() method.
Addendum
If you look at this question from the point of view of how opencv-python defines axes (positive x-axis to the right, positive y-axis downwards), the angle is defined between the horizontal axis and the minor ellipse diameter. To demonstrate this, different angles are plotted on a white canvas
import math
import cv2
import numpy as np
# create white canvas
img = np.zeros([512, 512, 3], dtype=np.uint8)
img.fill(255)
xc = 256
yc = 256
angles = list(range(0, 360, 30))
# radii = np.linspace(30, 200, len(angles))
radii = [175] * len(angles)
for idx, (angle, radius) in enumerate(zip(angles, radii)):
xtop = xc + math.cos(math.radians(angle)) * radius
ytop = yc + math.sin(math.radians(angle)) * radius
cv2.line(img, (int(xtop), int(ytop)), (int(xc), int(yc)), (0, 0, 255), 1)
# Put the contour index in the ellipse
cv2.putText(img, f'{round(math.radians(angle) / math.pi, 2)} pi', (int(xtop), int(ytop)),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255),
2, cv2.LINE_AA)
cv2.imwrite('opencv_angles.jpg', img)
cv2.imshow('Definition of angle', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

The text in the image shows the angles for that line in radians
The angle can also be plotted for the fitted ellipses, using the angle returned by fitEllipse
.
(xc, yc), (width, height), angle = genEllipse
rminor = min(width, height) / 2
xtop = xc + math.cos(math.radians(angle)) * rminor
ytop = yc + math.sin(math.radians(angle)) * rminor
cv2.line(result, (int(xtop), int(ytop)), (int(xc), int(yc)), (0, 0, 255), 3)

Here you can see that the angle is between the horizontal axis and minor diameter. Remember the rotation angle for each ellipse:
- Ellipse nb: 1 has angle: 55.63788986206055
- Ellipse nb: 2 has angle: 108.58539581298828
- Ellipse nb: 3 has angle: 170.23861694335938
- Ellipse nb: 4 has angle: 73.59089660644531
Conclusion
Angles in opencv, where positive x is to the right and positive y is downwards in images, means that the rotation angle for ellipses is best seen as between positive x-axis and minor ellipse diameter (downwards towards the positive y-axis).
However, if you flip the axes (in your mind), so that positive y-axis is upwards and positive x-axis is to the right in the image, then you can also interpret the ellipse rotation angle as between the positive y-axis and major ellipse diameter (to the right towards the positive x-axis)