I found the SVD approaches weren't working for me, maybe because my points are not uniformly spaced?
I settled on a function to construct an ellipse (parametric, given timepoints 0 to 2 pi), an error function (finding the distance between my points and points on the ellipse), and scipy.optimize.minimize.
def ellipse(t,xc,yc,a,b,theta): # works for a list of t points x,y=a*np.cos(t),b*np.sin(t) # start with a scrunched circle c=np.cos(theta) ; s=np.sin(theta) ; R=np.asarray([[c,-s],[s,c]]) x,y=np.matmul(R,[x,y]) # apply rotation matrix return x+xc,y+yc # shift by center position def findEllipse(xs,ys): def dz(args): # error function xc,yc,a,b,theta = args ts = np.linspace(0,2*np.pi,360*3,endpoint=False) x,y=ellipse(ts,xc,yc,a,b,theta) # points for the ellipse for args passed # distance from all given points (xs,ys) to all ellipse points (x,y) distances=np.sqrt( (xs[:,None]-x[None,:])**2+(ys[:,None]-y[None,:])**2 ) # collapse to find each xs,ys points' closest point on ellipse distances = np.amin(distances,axis=1) return np.sqrt(np.sum(distances**2)) # use MSE distance as our error metric # guesses: center in x,y, width and height, zero angle to start x0 = ( np.mean(xs) , np.mean(ys) , np.ptp(xs)/2 , np.ptp(ys)/2 , 0 ) res = minimize(dz,x0) return res.x cx,cy,a,b,theta = findEllipse( xs , ys )
here's an example of my result (I am using the blue pixels to define the "edge" around which i would like to draw the ellipse:

where I found the green pixels with a simple:
mask = np.zeros(data.shape)
mask[data > np.mean(data) + np.std(data)]
and found the blue pixels via:
rolled = mask+np.roll(mask,1,axis=0)+np.roll(mask,-1,axis=0)+\ np.roll(mask,1,axis=1)+np.roll(mask,-1,axis=1) # counts how many neighbors are selected in the mask border = np.where(rolled == 3) # pixels selected by the mask(1) + 2 neighbors