| 1 | # -----------------------------------------------------------------------------
|
|---|
| 2 | # Try to automatically mask cylindrical capillary from x-ray tomogram.
|
|---|
| 3 | # Operates on highlighted volume in volume dialog when this script is opened.
|
|---|
| 4 | # Creates a new volume data set.
|
|---|
| 5 | #
|
|---|
| 6 | # Tested with Chimera 1.5, July 19, 2010 daily build.
|
|---|
| 7 | #
|
|---|
| 8 | # Method: Look at two xy planes at 10% and 90% z values. Sum along y axis
|
|---|
| 9 | # to get a 1d funtion. Take first maxima on each end that are above
|
|---|
| 10 | # (mean + 2 standard deviations) as ellipse cross-section boundaries.
|
|---|
| 11 | # Likewise along x axis. Connect two circles to find cylinder axis and radius.
|
|---|
| 12 | #
|
|---|
| 13 | def fit_and_mask_cylinder(zpos = (0.10, 0.90), num_sd = 2.0,
|
|---|
| 14 | scale_radius = 0.95, scale_height = 1.05,
|
|---|
| 15 | color = (0,0,.7,1), subdivisions = 100,
|
|---|
| 16 | mask = True):
|
|---|
| 17 |
|
|---|
| 18 | from VolumeViewer import active_volume
|
|---|
| 19 | v = active_volume() # Highlighted in dialog.
|
|---|
| 20 | center, axis, radius, height = fit_cylinder(v.data, zpos, num_sd)
|
|---|
| 21 | radius *= scale_radius # Scale cylinder radius.
|
|---|
| 22 | height *= scale_height # Scale cylinder height.
|
|---|
| 23 | s = show_cylinder(center, axis, radius, height, subdivisions, color, v)
|
|---|
| 24 | if mask:
|
|---|
| 25 | from chimera import runCommand
|
|---|
| 26 | runCommand('mask #%d #%d' % (v.id, s.id)) # Mask volume.
|
|---|
| 27 |
|
|---|
| 28 | # -----------------------------------------------------------------------------
|
|---|
| 29 | # Returns center, axis, radius, height in volume data index units.
|
|---|
| 30 | #
|
|---|
| 31 | def fit_cylinder(data, zpos, num_sd):
|
|---|
| 32 |
|
|---|
| 33 | # Get data planes.
|
|---|
| 34 | xsize, ysize, zsize = data.size
|
|---|
| 35 | z1, z2 = [int(round(zp*zsize)) for zp in zpos]
|
|---|
| 36 | m1, m2 = data_z_plane(data,z1), data_z_plane(data,z2)
|
|---|
| 37 |
|
|---|
| 38 | # Set values below threshold to 0.
|
|---|
| 39 | m1.put(m1 < m1.mean() + num_sd * m1.std(), 0)
|
|---|
| 40 | m2.put(m2 < m2.mean() + num_sd * m2.std(), 0)
|
|---|
| 41 |
|
|---|
| 42 | # Find box bounding cylinder using 2 z-planes.
|
|---|
| 43 | from math import sqrt
|
|---|
| 44 | sdx = num_sd / sqrt(ysize)
|
|---|
| 45 | sdy = num_sd / sqrt(xsize)
|
|---|
| 46 | cx1, rx1 = local_maxima_bounds(m1.sum(axis=0), sdx)
|
|---|
| 47 | cy1, ry1 = local_maxima_bounds(m1.sum(axis=1), sdy)
|
|---|
| 48 | cx2, rx2 = local_maxima_bounds(m2.sum(axis=0), sdx)
|
|---|
| 49 | cy2, ry2 = local_maxima_bounds(m2.sum(axis=1), sdy)
|
|---|
| 50 | if cx1 is None or cy1 is None or cx2 is None or cy2 is None:
|
|---|
| 51 | print 'Projection with no local maxima above threshold'
|
|---|
| 52 | return
|
|---|
| 53 |
|
|---|
| 54 | # Correct radius for axis tilted away from z.
|
|---|
| 55 | dx,dy,dz = (cx2-cx1, cy2-cy1, z2-z1)
|
|---|
| 56 | from math import sqrt
|
|---|
| 57 | cosx = dz/sqrt(dx*dx+dz*dz)
|
|---|
| 58 | cosy = dz/sqrt(dy*dy+dz*dz)
|
|---|
| 59 | # Four radius values from x and y widths on z1 and z2 planes.
|
|---|
| 60 | radii = (rx1*cosx, rx2*cosx, ry1*cosy, ry2*cosy)
|
|---|
| 61 | radius = sum(radii)/len(radii) # Use average radius.
|
|---|
| 62 |
|
|---|
| 63 | # Calculate cylinder center, axis and height.
|
|---|
| 64 | center = (0.5*(cx1+cx2), 0.5*(cy1+cy2), 0.5*(z1+z2))
|
|---|
| 65 | l = sqrt(dx*dx+dy*dy+dz*dz)
|
|---|
| 66 | axis = (dx/l,dy/l,dz/l)
|
|---|
| 67 | height = l * float(zsize)/(z2-z1)
|
|---|
| 68 |
|
|---|
| 69 | return center, axis, radius, height
|
|---|
| 70 |
|
|---|
| 71 | # -----------------------------------------------------------------------------
|
|---|
| 72 | #
|
|---|
| 73 | def local_maxima_bounds(data, num_sd):
|
|---|
| 74 |
|
|---|
| 75 | cutoff = data.mean() + num_sd * data.std()
|
|---|
| 76 | local_max = ((data[1:-1] >= cutoff) &
|
|---|
| 77 | (data[2:] <= data[1:-1]) &
|
|---|
| 78 | (data[:-2] <= data[1:-1]))
|
|---|
| 79 | mi = local_max.nonzero()[0] # indices of local maxima above mean
|
|---|
| 80 | if len(mi) == 0:
|
|---|
| 81 | return None, None
|
|---|
| 82 |
|
|---|
| 83 | i0 = mi[0]+1
|
|---|
| 84 | i1 = mi[-1]+1
|
|---|
| 85 | c = 0.5 * (i0 + i1)
|
|---|
| 86 | r = 0.5 * (i1 - i0)
|
|---|
| 87 | return c, r
|
|---|
| 88 |
|
|---|
| 89 | # -----------------------------------------------------------------------------
|
|---|
| 90 | #
|
|---|
| 91 | def data_z_plane(data, z):
|
|---|
| 92 |
|
|---|
| 93 | xsize, ysize, zsize = data.size
|
|---|
| 94 | m3d = data.matrix((0,0,z), (xsize,ysize,1))
|
|---|
| 95 | m = m3d[0,:,:].copy()
|
|---|
| 96 | return m
|
|---|
| 97 |
|
|---|
| 98 | # -----------------------------------------------------------------------------
|
|---|
| 99 | #
|
|---|
| 100 | def show_cylinder(center, axis, radius, height, divisions, color, volume):
|
|---|
| 101 |
|
|---|
| 102 | transform = volume.data.ijk_to_xyz_transform
|
|---|
| 103 | varray, tarray = cylinder_geometry(center, axis, radius, height,
|
|---|
| 104 | divisions, transform)
|
|---|
| 105 | name = volume.name + ' cyl'
|
|---|
| 106 | align = volume.openState.xform
|
|---|
| 107 | s = create_cylinder_model(varray, tarray, color, align, name)
|
|---|
| 108 |
|
|---|
| 109 | return s
|
|---|
| 110 |
|
|---|
| 111 | # -----------------------------------------------------------------------------
|
|---|
| 112 | # Mesh for cylinder.
|
|---|
| 113 | #
|
|---|
| 114 | def cylinder_geometry(center, axis, radius, height, divisions, transform):
|
|---|
| 115 |
|
|---|
| 116 | from math import pi
|
|---|
| 117 | circumf = 2*pi*radius
|
|---|
| 118 | if circumf > height:
|
|---|
| 119 | nc = divisions
|
|---|
| 120 | nz = max(1, int(divisions * height / circumf))
|
|---|
| 121 | else:
|
|---|
| 122 | nc = max(6, int(divisions * circumf / height))
|
|---|
| 123 | nz = divisions
|
|---|
| 124 |
|
|---|
| 125 | # Surface for cylinder with axis along z.
|
|---|
| 126 | from Shape.shapecmd import cylinder_geometry
|
|---|
| 127 | varray, tarray = cylinder_geometry(radius, height, nz, nc, caps = True)
|
|---|
| 128 |
|
|---|
| 129 | # Tilt cylinder axis
|
|---|
| 130 | import Matrix
|
|---|
| 131 | xa,ya,za = Matrix.orthonormal_frame(axis)
|
|---|
| 132 | cx,cy,cz = center
|
|---|
| 133 | tf = ((xa[0], ya[0], za[0], cx),
|
|---|
| 134 | (xa[1], ya[1], za[1], cy),
|
|---|
| 135 | (xa[2], ya[2], za[2], cz))
|
|---|
| 136 | from _contour import affine_transform_vertices
|
|---|
| 137 | affine_transform_vertices(varray, tf)
|
|---|
| 138 | affine_transform_vertices(varray, transform)
|
|---|
| 139 |
|
|---|
| 140 | return varray, tarray
|
|---|
| 141 |
|
|---|
| 142 | # -----------------------------------------------------------------------------
|
|---|
| 143 | #
|
|---|
| 144 | def create_cylinder_model(vertices, triangles, color, align, name):
|
|---|
| 145 |
|
|---|
| 146 | # Create surface model
|
|---|
| 147 | import _surface
|
|---|
| 148 | s = _surface.SurfaceModel()
|
|---|
| 149 | s.name = name
|
|---|
| 150 | p = s.addPiece(vertices, triangles, color)
|
|---|
| 151 | p.displayStyle = p.Mesh
|
|---|
| 152 |
|
|---|
| 153 | # Show model
|
|---|
| 154 | from chimera import openModels
|
|---|
| 155 | openModels.add([s])
|
|---|
| 156 | if align:
|
|---|
| 157 | s.openState.xform = align
|
|---|
| 158 | return s
|
|---|
| 159 |
|
|---|
| 160 | # -----------------------------------------------------------------------------
|
|---|
| 161 | #
|
|---|
| 162 | fit_and_mask_cylinder()
|
|---|