-
Notifications
You must be signed in to change notification settings - Fork 39
/
dasch.py
441 lines (331 loc) · 12.5 KB
/
dasch.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import os.path
import numpy as np
from glob import glob
import abel
from scipy.linalg import inv
###############################################################################
#
# Dasch two-point, three_point, and onion-peeling deconvolution
# as described in Applied Optics 31, 1146 (1992), page 1147-8 sect. B & C.
# https://www.osapublishing.org/ao/abstract.cfm?uri=ao-31-8-1146
# see also discussion in PR #155 https://github.com/PyAbel/PyAbel/pull/155
#
# 2016-03-25 Dan Hickstein - one line Abel transform
# 2016-03-24 Steve Gibson - Python code framework
# 2015-12-29 Dhrubajyoti Das - original three_point code and
# highlighting the Dasch paper,see issue #61
# https://github.com/PyAbel/PyAbel/issues/61
#
###############################################################################
_dasch_parameter_docstring = \
"""
The :doc:`dasch-method deconvolution method
<transform_methods/dasch_method>`.
C. J. Dasch,
"One-dimensional tomography: a comparison of Abel, onion-peeling, and
filtered backprojection methods",
`Appl. Opt. 31, 1146–1152 (1992)
<https://doi.org/10.1364/AO.31.001146>`__.
Parameters
----------
IM : 1D or 2D numpy array
right-side half-image (or quadrant)
basis_dir: str or None
path to the directory for saving / loading the "dasch_method"
deconvolution operator array. Here, called ``basis_dir`` for
consistency with the other true basis methods. Use ``''`` for the
default directory. If ``None``, the operator array will not be loaded
from or saved to disk.
dr : float
sampling size (=1 for pixel images), used for Jacobian scaling.
The resulting inverse transform is simply scaled by 1/dr.
direction : str
only the `direction="inverse"` transform is currently implemented
verbose : bool
trace printing
Returns
-------
inv_IM: 1D or 2D numpy array
the "dasch_method" inverse Abel transformed half-image
"""
# cache deconvolution operator array
_D = None
_method = None
_source = None # 'cache', 'generated', or 'file', for unit testing
def two_point_transform(IM, basis_dir='', dr=1, direction="inverse",
verbose=False):
return _dasch_transform(IM, basis_dir=basis_dir, dr=dr,
direction=direction, method="two_point",
verbose=verbose)
def three_point_transform(IM, basis_dir='', dr=1, direction="inverse",
verbose=False):
return _dasch_transform(IM, basis_dir=basis_dir, dr=dr,
direction=direction, method="three_point",
verbose=verbose)
def onion_peeling_transform(IM, basis_dir='', dr=1, direction="inverse",
verbose=False):
return _dasch_transform(IM, basis_dir=basis_dir, dr=dr,
direction=direction, method="onion_peeling",
verbose=verbose)
two_point_transform.__doc__ = _dasch_parameter_docstring\
.replace("dasch-method", "two-point")\
.replace("dasch_method", "two_point")
three_point_transform.__doc__ = _dasch_parameter_docstring\
.replace("dasch-method", "three-point")\
.replace("dasch_method", "three_point")
onion_peeling_transform.__doc__ = _dasch_parameter_docstring\
.replace("dasch-method", "onion-peeling")\
.replace("dasch_method", "onion_peeling")
def _dasch_transform(IM, basis_dir='', dr=1, direction="inverse",
method="three_point", verbose=False):
global _D, _method
if direction != 'inverse':
raise ValueError('Forward "two_point" transform not implemented')
# make sure that the data has 2D shape
IM = np.atleast_2d(IM)
rows, cols = IM.shape
if cols < 2 and method == "two_point":
raise ValueError('"two_point" requires image width (cols) > 2')
if cols < 3 and method == "three_point":
raise ValueError('"three_point" requires image width (cols) > 3')
_D = get_bs_cached(method, cols, basis_dir=basis_dir, verbose=verbose)
inv_IM = dasch_transform(IM, _D)
if rows == 1:
inv_IM = inv_IM[0] # flatten array
return inv_IM/dr
def dasch_transform(IM, D):
"""Inverse Abel transform using the given deconvolution D-operator array.
Parameters
----------
IM : 2D numpy array
image data
D : 2D numpy array
deconvolution operator array, of shape (cols, cols)
Returns
-------
inv_IM : 2D numpy array
inverse Abel transform according to deconvolution operator D
"""
# one-line Abel transform - dot product of each row of IM with D
return np.tensordot(IM, D, axes=(1, 1))
def _bs_two_point(cols):
"""deconvolution function for two_point.
Parameters
----------
cols : int
width of the image
"""
# function Eq. (9) for j >= i
def J(i, j):
return np.log((np.sqrt((j + 1)**2 - i**2) + j + 1) /
(np.sqrt(j**2 - i**2) + j))/np.pi
# Eq. (8, 9) D-operator basis, is 0 for j < i
D = np.zeros((cols, cols))
# diagonal i == j
Ii, Jj = np.diag_indices(cols)
Ii = Ii[1:] # exclude special case i=j=0
Jj = Jj[1:]
D[Ii, Jj] = J(Ii, Jj)
# upper triangle j > i
Iu, Ju = np.triu_indices(cols, k=1)
Iu = Iu[1:] # exclude special case [0, 1]
Ju = Ju[1:]
D[Iu, Ju] = J(Iu, Ju) - J(Iu, Ju-1)
# special cases
D[0, 1] = J(0, 1) - 2/np.pi
D[0, 0] = 2/np.pi
return D
def _bs_three_point(cols):
"""deconvolution function for three_point.
Parameters
----------
cols : int
width of the image
"""
# function Eq. (7) for j >= i
def I0diag(i, j):
return np.log((np.sqrt((2*j+1)**2-4*i**2) + 2*j+1)/(2*j))/(2*np.pi)
# j > i
def I0(i, j):
return np.log(((np.sqrt((2*j + 1)**2 - 4*i**2) + 2*j + 1)) /
(np.sqrt((2*j - 1)**2 - 4*i**2) + 2*j - 1))/(2*np.pi)
# i = j NB minus -2I_ij typo in Dasch paper
def I1diag(i, j):
return np.sqrt((2*j+1)**2 - 4*i**2)/(2*np.pi) - 2*j*I0diag(i, j)
# j > i
def I1(i, j):
return (np.sqrt((2*j+1)**2 - 4*i**2) -
np.sqrt((2*j-1)**2 - 4*i**2))/(2*np.pi) - 2*j*I0(i, j)
D = np.zeros((cols, cols))
# matrix indices ------------------
# i = j
I, J = np.diag_indices(cols)
I = I[1:]
J = J[1:] # drop special cases (0,0), (0,1)
# j = i - 1
Ib, Jb = I, J-1
# j = i + 1
Iu, Ju = I-1, J
Iu = Iu[1:] # drop special case (0, 1)
Ju = Ju[1:]
# j > i + 1
Iut, Jut = np.triu_indices(cols, k=2)
Iut = Iut[1:] # drop special case (0, 2)
Jut = Jut[1:]
# D operator matrix ------------------
# j = i - 1
D[Ib, Jb] = I0diag(Ib, Jb+1) - I1diag(Ib, Jb+1)
# j = i
D[I, J] = I0(I, J+1) - I1(I, J+1) + 2*I1diag(I, J)
# j = i + 1
D[Iu, Ju] = I0(Iu, Ju+1) - I1(Iu, Ju+1) + 2*I1(Iu, Ju) -\
I0diag(Iu, Ju-1) - I1diag(Iu, Ju-1)
# j > i + 1
D[Iut, Jut] = I0(Iut, Jut+1) - I1(Iut, Jut+1) + 2*I1(Iut, Jut) -\
I0(Iut, Jut-1) - I1(Iut, Jut-1)
# special cases (that switch between I0, I1 cases)
D[0, 2] = I0(0, 3) - I1(0, 3) + 2*I1(0, 2) - I0(0, 1) - I1(0, 1)
D[0, 1] = I0(0, 2) - I1(0, 2) + 2*I1(0, 1) - 1/np.pi
D[0, 0] = I0(0, 1) - I1(0, 1) + 1/np.pi
return D
def _bs_onion_peeling(cols):
"""deconvolution function for onion_peeling.
Parameters
----------
cols : int
width of the image
"""
# weight matrix
W = np.zeros((cols, cols))
# diagonal elements i = j, Eq. (11)
I, J = np.diag_indices(cols)
W[I, J] = np.sqrt((2*J+1)**2 - 4*I**2)
# upper triangle j > i, Eq. (11)
Iu, Ju = np.triu_indices(cols, k=1)
W[Iu, Ju] = np.sqrt((2*Ju + 1)**2 - 4*Iu**2) -\
np.sqrt((2*Ju - 1)**2 - 4*Iu**2)
# operator used in Eq. (1)
D = inv(W)
return D
def get_bs_cached(method, cols, basis_dir='', verbose=False):
"""Load Dasch method deconvolution operator array from cache, or disk.
Generate and store if not available.
Checks whether ``method`` deconvolution array has been previously
calculated, or whether the file ``{method}_basis_{cols}.npy`` is
present in `basis_dir`.
Either, assign, read, or generate the deconvolution array
(saving it to file).
Parameters
----------
method : str
Abel transform method ``onion_peeling``, ``three_point``, or
``two_point``
cols : int
width of image
basis_dir : str or None
path to the directory for saving or loading the deconvolution array.
Use ``''`` for the default directory.
For ``None``, do not load or save the deconvolution operator array
verbose: boolean
print information (mainly for debugging purposes)
Returns
-------
D: numpy 2D array of shape (cols, cols)
deconvolution operator array for the associated method
file.npy: file
saves `D`, the deconvolution array to file name:
``{method}_basis_{cols}.npy``
"""
global _D, _method, _source
# check whether the deconvolution operator array is cached
if _D is not None:
if _D.shape[0] >= cols and _method == method:
if verbose:
print('Using memory cached deconvolution operator array,'
' shape {}'.format(_D.shape))
_source = 'cache'
return _D[:cols, :cols] # sliced to correct size
D_name = "{}_basis_{}.npy".format(method, cols)
D_generator = {
"onion_peeling": abel.dasch._bs_onion_peeling,
"three_point": abel.dasch._bs_three_point,
"two_point": abel.dasch._bs_two_point
}
_method = method
if basis_dir == '':
basis_dir = abel.transform.get_basis_dir(make=True)
# read deconvolution operator array if available
if basis_dir is not None:
path_to_basis_files = os.path.join(basis_dir, method+'_basis*')
basis_files = glob(path_to_basis_files)
for bf in basis_files:
if int(bf.split('_')[-1].split('.')[0]) >= cols:
# relies on file order
if verbose:
print("Loading deconvolution operator array from"
" file {:s}".format(bf))
# slice to size
_D = np.load(bf)[:cols, :cols]
_source = 'file'
return _D
if verbose:
print("A suitable deconvolution array for '{:s}' was not found.".
format(method))
print("A new array will be generated.")
_D = D_generator[method](cols)
_source = 'generated'
if basis_dir is not None:
path_to_basis_file = os.path.join(basis_dir, D_name)
np.save(path_to_basis_file, _D)
if verbose:
print("\ndeconvolution operator array saved to '{:s}"
.format(path_to_basis_file))
return _D
def cache_cleanup():
"""
Utility function.
Frees the memory caches created by :func:`get_bs_cached`.
This is usually pointless, but might be required after working
with very large images, if more RAM is needed for further tasks.
Parameters
----------
None
Returns
-------
None
"""
global _D, _method, _source
_D = None
_method = None
_source = None
def basis_dir_cleanup(method, basis_dir=''):
"""
Utility function.
Deletes deconvolution operator arrays saved on disk.
Parameters
----------
method : str
Abel transform method ``'onion_peeling'``, ``'three_point'``, or
``'two_point'``
basis_dir : str or None
absolute or relative path to the directory with saved deconvolution
operator arrays. Use ``''`` for the default directory. ``None`` does
nothing.
Returns
-------
None
"""
if basis_dir == '':
basis_dir = abel.transform.get_basis_dir(make=False)
if basis_dir is None:
return
if method not in ['onion_peeling', 'three_point', 'two_point']:
raise ValueError('Incorrect method "{}"!'.format(method))
files = glob(os.path.join(basis_dir, method + '_basis_*.npy'))
for fname in files:
os.remove(fname)