I am cross-posting this from GitHub as I am not sure whether the issue is a problem with my data or a bug.
https://github.com/scikit-image/scikit-image/issues/6699
I have thousands of elliptical features in my microscopy data that I want to fit models to using skimage. The model fails on some for no obvious reason. Here's code to reproduce:
import numpy as np
from skimage.measure import EllipseModel
import plotly.express as px
good_x1 = [779.026, 778.125, 776.953, 776.195, 775.617, 775.068, 774.127, 773.696, 773.305, 773.113, 773.088, 773.233, 773.449, 773.913, 774.344, 774.625, 775.179, 775.777, 776.254, 777.039, 777.926, 778.945, 780.023, 781.059, 781.973, 782.777, 783.244, 783.922, 784.995, 785.825, 786.196, 786.486, 786.65, 786.614, 786.482, 786.153, 785.749, 785.507, 784.901, 784.482, 783.879, 782.809, 781.965, 780.998, 780.001, 779.026]
good_y1 = [309.143, 309.432, 309.912, 310.35, 310.46, 311.087, 312.099, 312.879, 314.085, 315.012, 315.995, 316.948, 318.166, 319.044, 319.751, 320.283, 320.794, 321.34, 321.505, 321.908, 322.254, 322.478, 322.467, 322.243, 321.929, 321.561, 321.449, 320.891, 319.995, 318.905, 318.07, 316.872, 315.97, 315.037, 313.883, 312.943, 312.17, 311.623, 311.093, 310.477, 310.151, 309.54, 309.18, 309.027, 309.022, 309.143]
good_x2 = [434.959, 434.0, 433.012, 432.093, 430.938, 430.279, 429.847, 429.535, 429.257, 429.031, 428.843, 429.0, 429.348, 429.872, 430.313, 431.048, 432.189, 433.043, 434.003, 434.971, 435.769, 436.199, 436.743, 437.263, 437.824, 438.017, 438.018, 437.831, 437.449, 437.29, 436.807, 436.255, 435.776, 434.959]
good_y2 = [215.849, 216.001, 215.929, 215.684, 215.09, 214.615, 214.117, 213.631, 212.903, 211.992, 211.017, 210.0, 209.39, 208.857, 208.587, 208.087, 207.57, 207.247, 207.135, 207.2, 207.565, 207.73, 208.248, 208.819, 210.055, 210.998, 212.001, 212.952, 213.687, 214.168, 214.781, 215.333, 215.49, 215.849]
good_x3 = [1666.998, 1666.014, 1665.206, 1664.689, 1664.302, 1663.977, 1663.969, 1664.293, 1664.527, 1665.09, 1665.929, 1667.048, 1668.016, 1668.658, 1669.171, 1669.638, 1669.599, 1668.995, 1667.916, 1666.998]
good_y3 = [85.023, 85.07, 85.414, 85.685, 86.245, 86.994, 88.004, 88.835, 89.364, 89.862, 90.302, 90.338, 90.034, 89.491, 89.134, 87.917, 86.807, 86.004, 85.251, 85.023]
bad_x1 = [1541.221, 1541.848, 1543.009, 1544.15, 1544.962, 1545.777, 1545.943, 1545.786, 1545.103, 1543.986, 1543.14, 1541.968, 1541.094, 1540.765, 1540.799, 1541.221]
bad_y1 = [1254.78, 1255.29, 1255.535, 1255.395, 1254.945, 1253.922, 1253.0, 1252.063, 1250.892, 1250.374, 1250.401, 1250.959, 1252.049, 1252.968, 1254.069, 1254.78]
bad_x2 = [1739.079, 1738.567, 1738.392, 1738.118, 1738.17, 1738.782, 1739.302, 1740.179, 1741.013, 1741.999, 1742.997, 1743.423, 1744.178, 1743.811, 1743.735, 1743.595, 1743.308, 1742.834, 1742.342, 1741.813, 1740.998, 1739.995, 1739.079]
bad_y2 = [329.807, 329.316, 328.814, 327.989, 327.061, 325.853, 325.22, 324.478, 324.115, 324.078, 324.154, 324.49, 324.753, 325.994, 326.902, 327.679, 328.143, 328.836, 329.41, 329.628, 329.99, 330.067, 329.807]
bad_x3 = [992.001, 991.057, 989.879, 989.599, 989.252, 989.286, 989.894, 991.05, 991.983, 992.806, 993.286, 993.846, 994.32, 994.481, 994.088, 992.959, 992.001]
bad_y3 = [136.048, 136.19, 136.883, 137.551, 138.053, 138.929, 140.102, 140.767, 140.846, 140.551, 140.416, 139.851, 139.115, 137.938, 136.94, 136.168, 136.048]
xy = [(good_x1, good_y1, 'good_1'), (good_x2, good_y2, 'good_2'), (good_x3, good_y3, 'good_3'),
(bad_x1, bad_y1, 'bad_1'), (bad_x2, bad_y2, 'bad_2'), (bad_x3, bad_y3, 'bad_3')]
for ii in xy:
points = list(zip(ii[0], ii[1]))
a_points = np.array(points)
model = EllipseModel()
if model.estimate(a_points) == False:
fig = px.line(x= ii[0], y= ii[1], title='model fitting failed for ' + ii[2])
fig.show()
try:
xc, yc, a, b, theta = model.params
print(model.params)
ellipse_centre = (xc, yc)
residuals = model.residuals(a_points)
print(residuals)
except Exception as e: print(e)
else:
fig = px.line(x= ii[0], y= ii[1], title='model fitting successful for ' + ii[2])
fig.show()
xc, yc, a, b, theta = model.params
print(model.params)
ellipse_centre = (xc, yc)
residuals = model.residuals(a_points)
print(residuals)
Visually these features all seem elliptical and there is no difference between them in the length of the point lists or other properties. I think it's a bug but would appreciate a 2nd opinion, thanks.
Addition 2023-01-25:
There is another implementation of the same algorithm underlying the skimage function at SciPy, here: https://scipython.com/blog/direct-linear-least-squares-fitting-of-an-ellipse/
I have tested this on the same data and it variously fits, misfits or throws errors on my sets of points. Interesting it does this in a slightly different pattern to the skimage function. I think this problem is a fundamental flaw in the algorithm and not a bug.
Your x, y data points differ from their means by only a small amount, so the algorithms you are using are running into numerical errors which lead to failures in the linear algebra routines used in the fitting process.
You can solve this by subtracting off the means (i.e. spatially shifting the ellipses) so that there is a greater relative difference between the values: e.g. the following works for me:
import numpy as np
from skimage.measure import EllipseModel
import plotly.express as px
good_x1 = [779.026, 778.125, 776.953, 776.195, 775.617, 775.068, 774.127, 773.696, 773.305, 773.113, 773.088, 773.233, 773.449, 773.913, 774.344, 774.625, 775.179, 775.777, 776.254, 777.039, 777.926, 778.945, 780.023, 781.059, 781.973, 782.777, 783.244, 783.922, 784.995, 785.825, 786.196, 786.486, 786.65, 786.614, 786.482, 786.153, 785.749, 785.507, 784.901, 784.482, 783.879, 782.809, 781.965, 780.998, 780.001, 779.026]
good_y1 = [309.143, 309.432, 309.912, 310.35, 310.46, 311.087, 312.099, 312.879, 314.085, 315.012, 315.995, 316.948, 318.166, 319.044, 319.751, 320.283, 320.794, 321.34, 321.505, 321.908, 322.254, 322.478, 322.467, 322.243, 321.929, 321.561, 321.449, 320.891, 319.995, 318.905, 318.07, 316.872, 315.97, 315.037, 313.883, 312.943, 312.17, 311.623, 311.093, 310.477, 310.151, 309.54, 309.18, 309.027, 309.022, 309.143]
good_x2 = [434.959, 434.0, 433.012, 432.093, 430.938, 430.279, 429.847, 429.535, 429.257, 429.031, 428.843, 429.0, 429.348, 429.872, 430.313, 431.048, 432.189, 433.043, 434.003, 434.971, 435.769, 436.199, 436.743, 437.263, 437.824, 438.017, 438.018, 437.831, 437.449, 437.29, 436.807, 436.255, 435.776, 434.959]
good_y2 = [215.849, 216.001, 215.929, 215.684, 215.09, 214.615, 214.117, 213.631, 212.903, 211.992, 211.017, 210.0, 209.39, 208.857, 208.587, 208.087, 207.57, 207.247, 207.135, 207.2, 207.565, 207.73, 208.248, 208.819, 210.055, 210.998, 212.001, 212.952, 213.687, 214.168, 214.781, 215.333, 215.49, 215.849]
good_x3 = [1666.998, 1666.014, 1665.206, 1664.689, 1664.302, 1663.977, 1663.969, 1664.293, 1664.527, 1665.09, 1665.929, 1667.048, 1668.016, 1668.658, 1669.171, 1669.638, 1669.599, 1668.995, 1667.916, 1666.998]
good_y3 = [85.023, 85.07, 85.414, 85.685, 86.245, 86.994, 88.004, 88.835, 89.364, 89.862, 90.302, 90.338, 90.034, 89.491, 89.134, 87.917, 86.807, 86.004, 85.251, 85.023]
bad_x1 = [1541.221, 1541.848, 1543.009, 1544.15, 1544.962, 1545.777, 1545.943, 1545.786, 1545.103, 1543.986, 1543.14, 1541.968, 1541.094, 1540.765, 1540.799, 1541.221]
bad_y1 = [1254.78, 1255.29, 1255.535, 1255.395, 1254.945, 1253.922, 1253.0, 1252.063, 1250.892, 1250.374, 1250.401, 1250.959, 1252.049, 1252.968, 1254.069, 1254.78]
bad_x2 = [1739.079, 1738.567, 1738.392, 1738.118, 1738.17, 1738.782, 1739.302, 1740.179, 1741.013, 1741.999, 1742.997, 1743.423, 1744.178, 1743.811, 1743.735, 1743.595, 1743.308, 1742.834, 1742.342, 1741.813, 1740.998, 1739.995, 1739.079]
bad_y2 = [329.807, 329.316, 328.814, 327.989, 327.061, 325.853, 325.22, 324.478, 324.115, 324.078, 324.154, 324.49, 324.753, 325.994, 326.902, 327.679, 328.143, 328.836, 329.41, 329.628, 329.99, 330.067, 329.807]
bad_x3 = [992.001, 991.057, 989.879, 989.599, 989.252, 989.286, 989.894, 991.05, 991.983, 992.806, 993.286, 993.846, 994.32, 994.481, 994.088, 992.959, 992.001]
bad_y3 = [136.048, 136.19, 136.883, 137.551, 138.053, 138.929, 140.102, 140.767, 140.846, 140.551, 140.416, 139.851, 139.115, 137.938, 136.94, 136.168, 136.048]
xy = [(good_x1, good_y1, 'good_1'), (good_x2, good_y2, 'good_2'), (good_x3, good_y3, 'good_3'),
(bad_x1, bad_y1, 'bad_1'), (bad_x2, bad_y2, 'bad_2'), (bad_x3, bad_y3, 'bad_3')]
for ii in xy:
x = np.array(ii[0])
y = np.array(ii[1])
x = x - np.mean(x)
y = y - np.mean(y)
points = list(zip(x, y))
a_points = np.array(points)
model = EllipseModel()
if model.estimate(a_points) == False:
fig = px.line(x=x, y=y, title='model fitting failed for ' + ii[2])
fig.show()
try:
xc, yc, a, b, theta = model.params
print(model.params)
ellipse_centre = (xc, yc)
residuals = model.residuals(a_points)
print(residuals)
except Exception as e: print(e)
else:
fig = px.line(x=x, y=y, title='model fitting successful for ' + ii[2])
fig.show()
xc, yc, a, b, theta = model.params
print(model.params)
ellipse_centre = (xc, yc)
residuals = model.residuals(a_points)
print(residuals)
I make following Python Code to calculate center and size of Gaussian-like distribution basis of moment method. But, I can't make the code to calculate the angle of gaussian.
Please look at pictures.
First Picture is original data.
Second picture is reconstruct data from the result of moment method.
But, second picture is insufficient reconstruction. Because, original data is inclined distribution.
I have to, I think, calculate the angle of axis for Gaussian-like distribution.
To assume that the original distribution is sufficiently Gaussian-like distribution.
import numpy as np
import matplotlib.pyplot as plt
import json, glob
import sys, time, os
from mpl_toolkits.axes_grid1 import make_axes_locatable
from linecache import getline, clearcache
from scipy.integrate import simps
from scipy.constants import *
def integrate_simps (mesh, func):
nx, ny = func.shape
px, py = mesh[0][int(nx/2), :], mesh[1][:, int(ny/2)]
val = simps( simps(func, px), py )
return val
def normalize_integrate (mesh, func):
return func / integrate_simps (mesh, func)
def moment (mesh, func, index):
ix, iy = index[0], index[1]
g_func = normalize_integrate (mesh, func)
fxy = g_func * mesh[0]**ix * mesh[1]**iy
val = integrate_simps (mesh, fxy)
return val
def moment_seq (mesh, func, num):
seq = np.empty ([num, num])
for ix in range (num):
for iy in range (num):
seq[ix, iy] = moment (mesh, func, [ix, iy])
return seq
def get_centroid (mesh, func):
dx = moment (mesh, func, (1, 0))
dy = moment (mesh, func, (0, 1))
return dx, dy
def get_weight (mesh, func, dxy):
g_mesh = [mesh[0]-dxy[0], mesh[1]-dxy[1]]
lx = moment (g_mesh, func, (2, 0))
ly = moment (g_mesh, func, (0, 2))
return np.sqrt(lx), np.sqrt(ly)
def plot_contour_sub (mesh, func, loc=[0, 0], title="name", pngfile="./name"):
sx, sy = loc
nx, ny = func.shape
xs, ys = mesh[0][0, 0], mesh[1][0, 0]
dx, dy = mesh[0][0, 1] - mesh[0][0, 0], mesh[1][1, 0] - mesh[1][0, 0]
mx, my = int ( (sy-ys)/dy ), int ( (sx-xs)/dx )
fig, ax = plt.subplots()
divider = make_axes_locatable(ax)
ax.set_aspect('equal')
ax_x = divider.append_axes("bottom", 1.0, pad=0.5, sharex=ax)
ax_x.plot (mesh[0][mx, :], func[mx, :])
ax_x.set_title ("y = {:.2f}".format(sy))
ax_y = divider.append_axes("right" , 1.0, pad=0.5, sharey=ax)
ax_y.plot (func[:, my], mesh[1][:, my])
ax_y.set_title ("x = {:.2f}".format(sx))
im = ax.contourf (*mesh, func, cmap="jet")
ax.set_title (title)
plt.colorbar (im, ax=ax, shrink=0.9)
plt.savefig(pngfile + ".png")
def make_gauss (mesh, sxy, rxy, rot):
x, y = mesh[0] - sxy[0], mesh[1] - sxy[1]
px = x * np.cos(rot) - y * np.sin(rot)
py = y * np.cos(rot) + x * np.sin(rot)
fx = np.exp (-0.5 * (px/rxy[0])**2)
fy = np.exp (-0.5 * (py/rxy[1])**2)
return fx * fy
if __name__ == "__main__":
argvs = sys.argv
argc = len(argvs)
print (argvs)
nx, ny = 500, 500
lx, ly = 200, 150
rx, ry = 40, 25
sx, sy = 50, 10
rot = 30
px = np.linspace (-1, 1, nx) * lx
py = np.linspace (-1, 1, ny) * ly
mesh = np.meshgrid (px, py)
fxy0 = make_gauss (mesh, [sx, sy], [rx, ry], np.deg2rad(rot)) * 10
s0xy = get_centroid (mesh, fxy0)
w0xy = get_weight (mesh, fxy0, s0xy)
fxy1 = make_gauss (mesh, s0xy, w0xy, np.deg2rad(0))
s1xy = get_centroid (mesh, fxy1)
w1xy = get_weight (mesh, fxy1, s1xy)
print ([sx, sy], s0xy, s1xy)
print ([rx, ry], w0xy, w1xy)
plot_contour_sub (mesh, fxy0, loc=s0xy, title="Original", pngfile="./fxy0")
plot_contour_sub (mesh, fxy1, loc=s1xy, title="Reconst" , pngfile="./fxy1")
As Paul Panzer said, the flaw of your approach is that you look for "weight" and "angle" instead of covariance matrix. The covariance matrix fits perfectly in your approach: just compute one more moment, mixed xy.
The function get_weight should be replaced with
def get_covariance (mesh, func, dxy):
g_mesh = [mesh[0]-dxy[0], mesh[1]-dxy[1]]
Mxx = moment (g_mesh, func, (2, 0))
Myy = moment (g_mesh, func, (0, 2))
Mxy = moment (g_mesh, func, (1, 1))
return np.array([[Mxx, Mxy], [Mxy, Myy]])
Add one more import,
from scipy.stats import multivariate_normal
for reconstruction purpose. Still using your make_gauss function to create the original PDF, this is how it now gets reconstructed:
s0xy = get_centroid (mesh, fxy0)
w0xy = get_covariance (mesh, fxy0, s0xy)
fxy1 = multivariate_normal.pdf(np.stack(mesh, -1), mean=s0xy, cov=w0xy)
That's it; reconstruction works fine now.
Units on the color bar are not the same, because your make_gauss formula does not normalize the PDF.
It is my first post on StackOverflow.
I am writing a Mayavi Python program. Could anybody tell me how to update/modify the color of a point interactively? For example, in points3d(), changing the color of a point in real-time when I interactively modify its position.
I tried to do something under #on_trait_change, but it doesn't work. Color cannot be changed.
The following is my code:
import mayavi
import mayavi.mlab
from numpy import arange, pi, cos, sin
from traits.api import HasTraits, Range, Instance, \
on_trait_change
from traitsui.api import View, Item, HGroup
from mayavi.core.api import PipelineBase
from mayavi.core.ui.api import MayaviScene, SceneEditor, \
MlabSceneModel
def luc_func(x, y, z):
return x + y + z;
class Visualization(HasTraits):
x1 = Range(1, 30, 5)
z1 = Range(1, 30, 5)
scene = Instance(MlabSceneModel, ())
def __init__(self):
# Do not forget to call the parent's __init__
HasTraits.__init__(self)
z = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
y = [1,1,1,1,1,2,2,2,2,2,3,3,3,3,3,4,4,4,4,4,5,5,5,5,5]
x = [1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5]
self.plot = self.scene.mlab.points3d(x, y, z, luc_func, scale_mode = 'none')
#self.plot2 = self.scene.mlab.points3d(z, x, y, color = (0, 0, 1))
#on_trait_change('x1,z1')
def update_plot(self):
x = [1,2,3,4,self.x1,1,2,3,4,self.x1,1,2,3,4,self.x1,1,2,3,4,self.x1,1,2,3,4,self.x1]
z = [1,1,1,1,self.z1,1,1,1,1,self.z1,1,1,1,1,self.z1,1,1,1,1,self.z1,1,1,1,1,self.z1]
luc_func = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,self.z1]
self.plot.mlab_source.reset(x = x, z = z, luc_func = luc_func)
#self.plot2.mlab_source.set(y = y, z = z)
# the layout of the dialog created
view = View(Item('scene', editor=SceneEditor(scene_class=MayaviScene),
height=250, width=300, show_label=False),
HGroup(
'_', 'x1', "z1",
),
)
visualization = Visualization()
visualization.configure_traits()
Thanks for your help!
I have noticed a bug in the interactivity of points3d very similar to what you are describing here. I don't know exactly what is the origin of this bug but I regularly use the following workaround. The basic idea is to avoid mlab.points3d and instead call mlab.pipeline.glyph directly, as in:
def virtual_points3d(coords, figure=None, scale_factor=None, color=None,
name=None):
c = np.array(coords)
source = mlab.pipeline.scalar_scatter( c[:,0], c[:,1], c[:,2],
figure=figure)
return mlab.pipeline.glyph( source, scale_mode='none',
scale_factor=scale_factor,
mode='sphere', figure=figure, color=color, name=name)
Later you can change the colors by referring to the vtk object directly, rather than the mayavi trait that isn't connected properly:
glyph = virtual_points3d(coords)
glyph.mlab_source.dataset.point_data.scalars = new_values