Coverage for /usr/local/lib/python3.8/site-packages/spam/kalisphera/spheroid.py: 100%
68 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-11-22 13:26 +0000
« prev ^ index » next coverage.py v7.2.3, created at 2023-11-22 13:26 +0000
1"""
2Library of SPAM functions for generating 3D spheroids
3Copyright (C) 2020 SPAM Contributors
5This program is free software: you can redistribute it and/or modify it
6under the terms of the GNU General Public License as published by the Free
7Software Foundation, either version 3 of the License, or (at your option)
8any later version.
10This program is distributed in the hope that it will be useful, but WITHOUT
11ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13more details.
15You should have received a copy of the GNU General Public License along with
16this program. If not, see <http://www.gnu.org/licenses/>.
17"""
18import numpy
20def makeBlurryNoisySpheroid(dims, centres, axisLengthsAC, orientations, blur=0, noise=0, flatten=True, background=0.25, foreground=0.75):
21 """
22 This function creates a sheroid or series of spheroids in a 3D volume, adding noise and blur if requested.
23 Note that this does not handle the partial volume effect like kalisphera, and so some blur or downscaling is recommended!
25 The base code is from https://sbrisard.github.io/posts/20150930-orientation_correlations_among_rice_grains-06.html
27 Parameters
28 ----------
29 dims : 3-component list
30 Dimensions of volume to create
32 centre : 1D or 2D numpy array
33 Either a 3-component vector, or an Nx3 array of centres to draw with respect to 0,0,0 in `vol`
35 axisLengthsAC : 1D or 2D numpy array of floats
36 Half-axis lengths for each spheroid. If c>a, a prolate (rice-like) is generated; while a>c yields an oblate (lentil-like).
37 This is either a 2-component array for 1 particle, or a Nx2 array of values.
39 orientations : 1D or 2D numpy array of floats
40 Orientation vectors of the axis of rotational symmetry for the spheroid.
41 This is either a 3-component ZYX array for 1 particle, or a Nx3 array of values.
43 blur : float, optional
44 Standard deviation of the blur kernel (in ~pixels)
45 Default = 0
47 noise : float, optional
48 Standard devitaion of the noise (in greylevels), reminder background=0 and sphere=1
49 Default = 0
51 flatten : bool, optional
52 Flatten greyvalues >1 to 1 (caused by overlaps)?
53 Default = True
55 background : float, optional
56 Desired mean greyvalue of the background
57 Default = 0.25
59 foreground : float, optional
60 Desired mean greyvalue of the background
61 Default = 0.75
63 Returns
64 -------
65 vol : the input array
66 """
67 # 3D
68 D = 3
70 vol = numpy.zeros(dims, dtype='<f4')
72 centres = numpy.array(centres)
73 axisLengthsAC = numpy.array(axisLengthsAC)
74 orientations = numpy.array(orientations)
75 dims = numpy.array(dims)
77 if len(centres.shape) == 1:
78 nParticles = 1
80 # There is just one spheroid
81 centres = numpy.array([centres])
83 assert len(axisLengthsAC.shape) == 1
84 assert axisLengthsAC.shape[0] == 2
85 axisLengthsAC = numpy.array([axisLengthsAC])
87 assert len(orientations.shape) == 1
88 assert orientations.shape[0] == 3
89 assert numpy.isclose(numpy.linalg.norm(orientations), 1)
90 orientations = numpy.array([orientations])
92 elif len(centres.shape) == 2:
93 nParticles = centres.shape[0]
95 # more than one spheroid
96 assert len(axisLengthsAC.shape) == 2
97 assert axisLengthsAC.shape[0] == nParticles
98 assert axisLengthsAC.shape[1] == 2
100 assert len(orientations.shape) == 2
101 assert orientations.shape[0] == nParticles
102 assert orientations.shape[1] == 3
104 def spheroidDistanceFunction(x, invQ):
105 """
106 Ordering of points: ``x[i, ...]`` is the i-th coordinate
107 """
108 y = numpy.tensordot(invQ, x, axes=([-1], [0]))
109 numpy.multiply(x, y, y)
110 return numpy.sum(y, axis=0)
112 # pool?
113 for centre, axisLengthAC, orientation in zip(centres, axisLengthsAC, orientations):
114 # Preamble
115 p = numpy.outer(orientation, orientation)
116 q = numpy.eye(D, dtype=numpy.float64) - p
117 Q = axisLengthAC[1]**2*p+axisLengthAC[0]**2*q
118 invQ = p/axisLengthAC[1]**2+q/axisLengthAC[0]**2
120 # Must implement local bounding box calculation here...
121 bb = numpy.sqrt(numpy.diag(Q))
124 h = 1
125 #bb = dims
126 i_max = numpy.ceil(bb/h-0.5)
127 bb = i_max*h
128 shape = 2*i_max+1
130 slices = [slice(-x, x, i*1j) for (x, i) in zip(bb, shape)]
131 x = numpy.mgrid[slices]
133 # Thresholding distance function!!
134 spheroidLocal = spheroidDistanceFunction(x, invQ) < 1
136 spheroidLocalShape = numpy.array(spheroidLocal.shape).astype(int)
137 spheroidLocalHalfWindow = (spheroidLocalShape - 1) / 2
138 #GP 2021-06-23: Round to the closest int
139 centre = numpy.floor(centre)
140 spheroidGlobalTop = (centre-spheroidLocalHalfWindow).astype(int)
141 spheroidGlobalBot = (centre+spheroidLocalHalfWindow+1).astype(int)
143 # How much the local BB is going over the edges of the image
144 spheroidOffsetTop = numpy.zeros(3, dtype=int)
145 spheroidOffsetBot = numpy.zeros(3, dtype=int)
146 if spheroidGlobalTop[0] < 0: spheroidOffsetTop[0] = -spheroidGlobalTop[0]
147 if spheroidGlobalTop[1] < 0: spheroidOffsetTop[1] = -spheroidGlobalTop[1]
148 if spheroidGlobalTop[2] < 0: spheroidOffsetTop[2] = -spheroidGlobalTop[2]
150 if spheroidGlobalBot[0] > dims[0]-1: spheroidOffsetBot[0] = spheroidGlobalBot[0] - dims[0]
151 if spheroidGlobalBot[1] > dims[1]-1: spheroidOffsetBot[1] = spheroidGlobalBot[1] - dims[1]
152 if spheroidGlobalBot[2] > dims[2]-1: spheroidOffsetBot[2] = spheroidGlobalBot[2] - dims[2]
154 sliceGlobal = (slice(spheroidGlobalTop[0]+spheroidOffsetTop[0],spheroidGlobalBot[0]-spheroidOffsetBot[0]),
155 slice(spheroidGlobalTop[1]+spheroidOffsetTop[1],spheroidGlobalBot[1]-spheroidOffsetBot[1]),
156 slice(spheroidGlobalTop[2]+spheroidOffsetTop[2],spheroidGlobalBot[2]-spheroidOffsetBot[2]))
157 sliceLocal = (slice(spheroidOffsetTop[0],spheroidLocalShape[0]-spheroidOffsetBot[0]),
158 slice(spheroidOffsetTop[1],spheroidLocalShape[1]-spheroidOffsetBot[1]),
159 slice(spheroidOffsetTop[2],spheroidLocalShape[2]-spheroidOffsetBot[2]))
161 vol[sliceGlobal] += spheroidLocal[sliceLocal]
163 if flatten: vol[vol>1]=1
165 if blur != 0:
166 import scipy.ndimage
167 vol = scipy.ndimage.gaussian_filter(vol, sigma=blur)
169 if noise != 0:
170 vol += numpy.random.normal(size=dims, scale=noise)
172 vol *= float(foreground)-float(background)
173 vol += float(background)
175 return vol