Coverage for modules/QuadrantFolder.py: 46%
600 statements
« prev ^ index » next coverage.py v7.0.4, created at 2023-01-10 09:27 -0600
« prev ^ index » next coverage.py v7.0.4, created at 2023-01-10 09:27 -0600
1"""
2Copyright 1999 Illinois Institute of Technology
4Permission is hereby granted, free of charge, to any person obtaining
5a copy of this software and associated documentation files (the
6"Software"), to deal in the Software without restriction, including
7without limitation the rights to use, copy, modify, merge, publish,
8distribute, sublicense, and/or sell copies of the Software, and to
9permit persons to whom the Software is furnished to do so, subject to
10the following conditions:
12The above copyright notice and this permission notice shall be
13included in all copies or substantial portions of the Software.
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18IN NO EVENT SHALL ILLINOIS INSTITUTE OF TECHNOLOGY BE LIABLE FOR ANY
19CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23Except as contained in this notice, the name of Illinois Institute
24of Technology shall not be used in advertising or otherwise to promote
25the sale, use or other dealings in this Software without prior written
26authorization from Illinois Institute of Technology.
27"""
29import os
30import pickle
31import fabio
32import pyFAI
33from scipy.ndimage.filters import gaussian_filter, convolve1d
34from scipy.interpolate import UnivariateSpline
35from skimage.morphology import white_tophat, disk
36import ccp13
37from pyFAI.azimuthalIntegrator import AzimuthalIntegrator
38from musclex import __version__
39try:
40 from . import QF_utilities as qfu
41 from ..utils.file_manager import fullPath, createFolder, getBlankImageAndMask, getMaskOnly, ifHdfReadConvertless
42 from ..utils.histogram_processor import *
43 from ..utils.image_processor import *
44except: # for coverage
45 from modules import QF_utilities as qfu
46 from utils.file_manager import fullPath, createFolder, getBlankImageAndMask, getMaskOnly, ifHdfReadConvertless
47 from utils.histogram_processor import *
48 from utils.image_processor import *
50# Make sure the cython part is compiled
51# from subprocess import call
52# call(["python setup2.py build_ext --inplace"], shell = True)
54class QuadrantFolder:
55 """
56 A class for Quadrant Folding processing - go to process() to see all processing steps
57 """
58 def __init__(self, img_path, img_name, parent, file_list=None, extension=''):
59 """
60 Initial value for QuadrantFolder object
61 :param img_path: directory path of input image
62 :param img_name: image file name
63 """
64 if extension in ('.hdf5', '.h5'):
65 index = next((i for i, item in enumerate(file_list[0]) if item == img_name), 0)
66 self.orig_img = file_list[1][index]
67 else:
68 self.orig_img = fabio.open(fullPath(img_path, img_name)).data
69 self.orig_img = ifHdfReadConvertless(img_name, self.orig_img)
70 self.orig_img = self.orig_img.astype("float32")
71 self.orig_image_center = None
72 self.dl, self.db = 0, 0
73 if self.orig_img.shape == (1043, 981):
74 self.img_type = "PILATUS"
75 else:
76 self.img_type = "NORMAL"
77 self.empty = False
78 self.img_path = img_path
79 self.img_name = img_name
80 self.imgCache = {} # displayed images will be saved in this param
81 self.ignoreFolds = set()
82 self.version = __version__
83 cache = self.loadCache() # load from cache if it's available
85 self.initImg = None
86 self.centImgTransMat = None # Centerize image transformation matrix
87 self.center_before_rotation = None # we need the center before rotation is applied each time we rotate the image
88 self.rotMat = None # store the rotation matrix used so that any point specified in current co-ordinate system can be transformed to the base (original image) co-ordinate system
89 self.centerChanged = False
90 self.expandImg = 1
91 if parent is not None:
92 self.parent = parent
93 else:
94 self.parent = self
95 self.newImgDimension = None
96 self.masked = False
98 # info dictionary will save all results
99 if cache is not None:
100 self.info = cache
101 else:
102 self.info = {
103 'imgType' : str(self.orig_img.dtype)
104 }
106 def cacheInfo(self):
107 """
108 Save info dict to cache. Cache file will be save as filename.info in folder "qf_cache"
109 :return: -
110 """
111 cache_file = fullPath(fullPath(self.img_path, "qf_cache"), self.img_name + ".info")
112 createFolder(fullPath(self.img_path, "qf_cache"))
113 self.info['program_version'] = self.version
114 with open(cache_file, "wb") as c:
115 pickle.dump(self.info, c)
117 def loadCache(self):
118 """
119 Load info dict from cache. Cache file will be filename.info in folder "qf_cache"
120 :return: cached info (dict)
121 """
122 cache_file = fullPath(fullPath(self.img_path, "qf_cache"), self.img_name+".info")
123 if os.path.isfile(cache_file):
124 with open(cache_file, "rb") as c:
125 info = pickle.load(c)
126 if info is not None:
127 if info['program_version'] == self.version:
128 return info
129 print("Cache version " + info['program_version'] + " did not match with Program version " + self.version)
130 print("Invalidating cache and reprocessing the image")
131 return None
133 def delCache(self):
134 """
135 Delete cache
136 :return: -
137 """
138 cache_path = fullPath(self.img_path, "qf_cache")
139 cache_file = fullPath(cache_path, self.img_name + '.info')
140 if os.path.exists(cache_path) and os.path.isfile(cache_file):
141 os.remove(cache_file)
143 def deleteFromDict(self, dicto, delStr):
144 """
145 Delete a key and value from dictionary
146 :param dict: input dictionary
147 :param delStr: deleting key
148 :return: -
149 """
150 if delStr in dicto:
151 del dicto[delStr]
153 def process(self, flags):
154 """
155 All processing steps - all flags are provided by Quadrant Folding app as a dictionary
156 settings must have ...
157 ignore_folds - ignored quadrant = quadrant that will not be averaged
158 bgsub - background subtraction method (-1 = no bg sub, 0 = Circular, 1 = 2D convex hull, 2 = white-top-hat)
159 mask_thres - pixel value that won't be averaged (deplicated)
160 sigmoid - merging gradient
161 other backgound subtraction params - cirmin, cirmax, nbins, tophat1, tophat2
162 """
163 print(str(self.img_name) + " is being processed...")
164 self.updateInfo(flags)
165 self.initParams()
166 self.applyBlankImageAndMask()
167 self.findCenter()
168 self.centerizeImage()
169 self.rotateImg()
170 self.calculateAvgFold()
171 self.getRminmax()
172 self.applyBackgroundSubtraction()
173 self.mergeImages()
174 self.generateResultImage()
176 if "no_cache" not in flags:
177 self.cacheInfo()
179 self.parent.statusPrint("")
181 def updateInfo(self, flags):
182 """
183 Update info dict using flags
184 :param flags: flags
185 :return: -
186 """
187 if flags['orientation_model'] is None:
188 if 'orientation_model' not in self.info:
189 flags['orientation_model'] = 0
190 else:
191 del flags['orientation_model']
192 self.info.update(flags)
194 def initParams(self):
195 """
196 Initial some parameters in case GUI doesn't specified
197 """
198 if 'mask_thres' not in self.info:
199 self.info['mask_thres'] = getMaskThreshold(self.orig_img, self.img_type)
200 if 'ignore_folds' not in self.info:
201 self.info['ignore_folds'] = set()
202 if 'bgsub' not in self.info:
203 self.info['bgsub'] = 0
204 if 'sigmoid' not in self.info:
205 self.info['sigmoid'] = 0.05
207 def applyBlankImageAndMask(self):
208 """
209 Apply the blank image and mask threshold on the orig_img
210 :return: -
211 """
212 if 'blank_mask' in self.info and self.info['blank_mask'] and not self.masked:
213 img = np.array(self.orig_img, 'float32')
214 blank, mask = getBlankImageAndMask(self.img_path)
215 maskOnly = getMaskOnly(self.img_path)
216 # blank = None
217 if blank is not None:
218 img = img - blank
219 if mask is not None:
220 img[mask > 0] = self.info['mask_thres'] - 1.
221 if maskOnly is not None:
222 print("Applying mask only image")
223 img[maskOnly > 0] = self.info['mask_thres'] - 1
225 self.orig_img = img
226 self.masked = True
228 def findCenter(self):
229 """
230 Find center of the diffraction. The center will be kept in self.info["center"].
231 Once the center is calculated, the rotation angle will be re-calculated, so self.info["rotationAngle"] is deleted
232 """
233 self.parent.statusPrint("Finding Center...")
234 if 'mask_thres' not in self.info:
235 self.initParams()
236 if 'center' in self.info:
237 self.centerChanged = False
238 return
239 self.centerChanged = True
240 if 'calib_center' in self.info:
241 self.info['center'] = self.info['calib_center']
242 return
243 if 'manual_center' in self.info:
244 center = self.info['manual_center']
245 if self.rotMat is not None:
246 center = np.dot(cv2.invertAffineTransform(self.rotMat), [center[0] + self.dl, center[1] + self.db, 1])
247 self.info['manual_center'] = center
248 self.info['center'] = self.info['manual_center']
249 return
250 print("Center is being calculated ... ")
251 self.orig_image_center = getCenter(self.orig_img)
252 self.orig_img, self.info['center'] = processImageForIntCenter(self.orig_img, self.orig_image_center, self.img_type, self.info['mask_thres'])
253 print("Done. Center = "+str(self.info['center']))
256 def rotateImg(self):
257 """
258 Find rotation angle of the diffraction. Turn the diffraction equator to be horizontal. The angle will be kept in self.info["rotationAngle"]
259 Once the rotation angle is calculated, the average fold will be re-calculated, so self.info["avg_fold"] is deleted
260 """
261 self.parent.statusPrint("Finding Rotation Angle...")
262 if 'manual_rotationAngle' in self.info:
263 self.info['rotationAngle'] = self.info['manual_rotationAngle']
264 del self.info['manual_rotationAngle']
265 self.deleteFromDict(self.info, 'avg_fold')
266 elif "mode_angle" in self.info:
267 print(f'Using mode orientation {self.info["mode_angle"]}')
268 self.info['rotationAngle'] = self.info["mode_angle"]
269 self.deleteFromDict(self.info, 'avg_fold')
270 elif not self.empty and 'rotationAngle' not in self.info.keys():
271 print("Rotation Angle is being calculated ... ")
272 # Selecting disk (base) image and corresponding center for determining rotation as for larger images (formed from centerize image) rotation angle is wrongly computed
273 _, center = self.parent.getExtentAndCenter()
274 img = copy.copy(self.initImg) if self.initImg is not None else copy.copy(self.orig_img)
275 self.info['rotationAngle'] = getRotationAngle(img, center, self.info['orientation_model'])
276 self.deleteFromDict(self.info, 'avg_fold')
277 print("Done. Rotation Angle is " + str(self.info['rotationAngle']) +" degree")
279 def getExtentAndCenter(self):
280 """
281 Give the extent and the center of the image in self.
282 :return: extent, center
283 """
284 if self is None:
285 return [0,0], (0,0)
286 if self.orig_image_center is None:
287 self.findCenter()
288 self.statusPrint("Done.")
289 if 'calib_center' in self.info:
290 center = self.info['calib_center']
291 elif 'manual_center' in self.info:
292 center = self.info['manual_center']
293 else:
294 center = self.orig_image_center
296 extent = [self.info['center'][0] - center[0], self.info['center'][1] - center[1]]
298 return extent, center
300 def centerizeImage(self):
301 """
302 Create an enlarged image such that image center is at the center of new image
303 """
304 self.parent.statusPrint("Centererizing image...")
305 if not self.centerChanged:
306 return
307 center = self.info['center']
308 if self.centImgTransMat is not None and 'calib_center' not in self.info:
309 # convert center in initial img coordinate system
310 M = self.centImgTransMat
311 M[0,2] = -1*M[0,2]
312 M[1,2] = -1*M[1,2]
313 center = [center[0], center[1], 1]
314 center = np.dot(M, center)
315 if 'manual_center' in self.info:
316 self.info['manual_center'] = (int(center[0]), int(center[1]))
317 if 'calib_center' in self.info:
318 self.info['calib_center'] = (int(center[0]), int(center[1]))
320 center = (int(center[0]), int(center[1]))
321 if self.initImg is None:
322 # While centerizing image use the first image after reading from file and processing for int center
323 self.initImg = self.orig_img
324 print("Dimension of initial image before centerize ", self.orig_img.shape)
325 img = self.initImg
326 print("Dimension of image before centerize ", img.shape)
328 b, l = img.shape
329 if self.parent.newImgDimension is None:
330 dim = int(self.expandImg*max(l, b))
331 self.parent.newImgDimension = dim
332 else:
333 dim = self.parent.newImgDimension
334 new_img = np.zeros((dim,dim)).astype("float32")
335 new_img[0:b,0:l] = img
337 #Translate image to appropriate position
338 transx = int(((dim/2) - center[0]))
339 transy = int(((dim/2) - center[1]))
340 M = np.float32([[1,0,transx],[0,1,transy]])
341 self.centImgTransMat = M
342 rows,cols = new_img.shape
343 mask_thres = self.info["mask_thres"]
345 if self.img_type == "PILATUS":
346 if mask_thres == -999:
347 mask_thres = getMaskThreshold(img, self.img_type)
348 mask = np.zeros((new_img.shape[0], new_img.shape[1]), dtype=np.uint8)
349 mask[new_img <= mask_thres] = 255
350 cv2.setNumThreads(1) # Added to prevent segmentation fault due to cv2.warpAffine
351 translated_Img = cv2.warpAffine(new_img, M, (cols, rows))
352 translated_mask = cv2.warpAffine(mask, M, (cols, rows))
353 translated_mask[translated_mask > 0.] = 255
354 translated_Img[translated_mask > 0] = mask_thres
355 else:
356 cv2.setNumThreads(1) # Added to prevent segmentation fault due to cv2.warpAffine
357 translated_Img = cv2.warpAffine(new_img,M,(cols,rows))
359 self.orig_img = translated_Img
360 self.info['center'] = (int(dim / 2), int(dim / 2))
361 self.center_before_rotation = (int(dim / 2), int(dim / 2))
362 print("Dimension of image after centerize ", self.orig_img.shape)
365 def getRotatedImage(self):
366 """
367 Get rotated image by angle while image = original input image, and angle = self.info["rotationAngle"]
368 """
369 img = np.array(self.orig_img, dtype="float32")
370 center = self.info["center"]
371 if self.center_before_rotation is not None:
372 center = self.center_before_rotation
373 else:
374 self.center_before_rotation = center
376 b, l = img.shape
377 rotImg, newCenter, self.rotMat = rotateImage(img, center, self.info["rotationAngle"], self.img_type, self.info['mask_thres'])
379 # Cropping off the surrounding part since we had already expanded the image to maximum possible extent in centerize image
380 bnew, lnew = rotImg.shape
381 db, dl = (bnew - b)//2, (lnew-l)//2
382 final_rotImg = rotImg[db:bnew-db, dl:lnew-dl]
383 self.info["center"] = (newCenter[0]-dl, newCenter[1]-db)
384 self.dl, self.db = dl, db # storing the cropped off section to recalculate coordinates when manual center is given
386 return final_rotImg
388 def getFoldNumber(self, x, y):
389 """
390 Get quadrant number by coordinates x, y (top left = 0, top right = 1, bottom left = 2, bottom right = 3)
391 :param x: x coordinate
392 :param y: y coordinate
393 :return: coordinate number
394 """
395 center = self.info['center']
396 center_x = center[0]
397 center_y = center[1]
399 if x < center_x and y < center_y:
400 return 0
401 if x >= center_x and y < center_y:
402 return 1
403 if x < center_x and y >= center_y:
404 return 2
405 if x >= center_x and y >= center_y:
406 return 3
407 return -1
409 def applyAngularBGSub(self):
410 """
411 Apply Circular Background Subtraction to average fold, and save the result to self.info['bgimg1']
412 """
413 copy_img = copy.copy(self.info['avg_fold'])
414 center = [copy_img.shape[1]-1, copy_img.shape[0]-1]
415 npt_rad = int(distance(center,(0,0)))
417 ai = AzimuthalIntegrator(detector="agilent_titan")
418 ai.setFit2D(100, center[0], center[1])
419 mask = np.zeros((copy_img.shape[0], copy_img.shape[1]))
421 start_p = self.info["cirmin"] # minimum value of circular background subtraction pixel range in percent
422 end_p = self.info["cirmax"] # maximum value of circular background subtraction pixel range in percent
423 rmin = self.info["rmin"] # minimum radius for background subtraction
424 rmax = self.info["rmax"] # maximum radius for background subtraction
425 theta_size = self.info["bin_theta"] # bin size in degree
426 nBins = 90/theta_size
428 I2D = []
429 for deg in range(180, 271):
430 _, I = ai.integrate1d(copy_img, npt_rad, mask=mask, unit="r_mm", method="csr_ocl", azimuth_range=(deg, deg+1))
431 I2D.append(I)
433 I2D = np.array(I2D)
435 sub_tr = []
436 for i in range(nBins):
437 # loop in each theta range
438 subr = []
439 theta1 = i * theta_size
440 theta2 = (i+1) * theta_size
441 if i+1 == nBins:
442 theta2 += 1
444 for r in range(0, I2D.shape[1]):
445 # Get azimuth line on each radius (in theta range)
446 rad = I2D[theta1:theta2,r]
448 if start_p == end_p:
449 percentile = int(round(start_p * len(rad) / 100.))
450 rad = np.array(sorted(rad)[percentile: percentile+1])
451 else:
452 s = int(round(start_p * len(rad) / 100.))
453 e = int(round(end_p * len(rad) / 100.))
454 if s == e:
455 rad = sorted(rad)[s: s+1]
456 else:
457 rad = np.array(sorted(rad)[s: e])
459 # Get mean value of pixel range
460 subr.append(np.mean(rad))
462 subr_hist = subr[rmin:rmax + 1]
463 hist_x = list(range(0, len(subr_hist)))
465 # Get pchip line from subtraction histogram
466 hull_x, hull_y = getHull(hist_x, subr_hist)
467 y_pchip = np.array(pchip(hull_x, hull_y, hist_x))
469 subr_hist = np.concatenate((np.zeros(rmin), y_pchip))
470 subr_hist = np.concatenate((subr_hist, np.zeros(len(subr) - rmax)))
472 sub_tr.append(subr_hist)
475 # Create Angular background from subtraction lines (pchipline in each bin)
476 bg_img = qfu.createAngularBG(copy_img.shape[1], copy_img.shape[0], np.array(sub_tr, dtype=np.float32), nBins)
478 result = copy_img - bg_img
479 result -= result.min()
481 # Subtract original average fold by background
482 self.info['bgimg1'] = result
484 def applyCircularlySymBGSub2(self):
485 """
486 Apply Circular Background Subtraction to average fold, and save the result to self.info['bgimg1']
487 """
488 fold = copy.copy(self.info['avg_fold'])
489 # center = [fold.shape[1] + .5, fold.shape[0] + .5]
491 img = self.makeFullImage(fold)
492 img = img.astype("float32")
493 width = img.shape[1]
494 height = img.shape[0]
496 ad = np.ravel(img)
497 ad = np.array(ad, 'f')
498 b = np.array(ad, 'f')
499 rmin = float(self.info['rmin'])
500 rmax = float(self.info['rmax'])
501 bin_size = float(self.info["radial_bin"])
502 smoo = self.info['smooth']
503 tension = self.info['tension']
504 max_bin = int(np.ceil((rmax - rmin) / bin_size))*10
505 max_num = int(np.ceil(rmax * 2 * np.pi))*10
506 pc1 = self.info['cirmin']/100.
507 pc2 = self.info['cirmax']/100.
509 csyb = np.zeros(max_bin, 'f')
510 csyd = np.zeros(max_bin, 'f')
511 ys = np.zeros(max_bin, 'f')
512 ysp = np.zeros(max_bin, 'f')
513 wrk = np.zeros(max_bin * 9, 'f')
514 pixbin = np.zeros(max_num, 'f')
515 index_bn = np.zeros(max_num, 'f')
517 ccp13.bgcsym2(ad=ad, b=b,
518 smoo=smoo,
519 tens=tension,
520 pc1=pc1,
521 pc2=pc2,
522 npix=width,
523 nrast=height,
524 dmin=rmin,
525 dmax=rmax,
526 xc=width/2.-.5,
527 yc=height/2.-.5,
528 dinc=bin_size,
529 csyb=csyb,
530 csyd=csyd,
531 ys=ys,
532 ysp=ysp,
533 wrk=wrk,
534 pixbin=pixbin,
535 index_bn=index_bn,
536 iprint=0,
537 ilog=6,
538 maxbin=max_bin,
539 maxnum=max_num)
541 background = copy.copy(b)
542 background[np.isnan(background)] = 0.
543 background = np.array(background, 'float32')
544 background = background.reshape((height, width))
545 background = background[:fold.shape[0], :fold.shape[1]]
546 result = np.array(fold - background, dtype=np.float32)
547 result = qfu.replaceRmin(result, int(rmin), 0.)
549 self.info['bgimg1'] = result
551 def applySmoothedBGSub(self, typ='gauss'):
552 """
553 Apply the background substraction smoothed, with default type to gaussian.
554 :param typ: type of the substraction
555 """
556 fold = copy.copy(self.info['avg_fold'])
558 img = self.makeFullImage(fold)
559 img = img.astype("float32")
560 width = img.shape[1]
561 height = img.shape[0]
563 img = np.ravel(img)
564 buf = np.array(img, 'f')
565 maxfunc = len(buf)
566 cback = np.zeros(maxfunc, 'f')
567 b = np.zeros(maxfunc, 'f')
568 smbuf = np.zeros(maxfunc, 'f')
569 vals = np.zeros(20, 'f')
571 if typ == 'gauss':
572 vals[0] = self.info['fwhm']
573 vals[1] = self.info['cycles']
574 vals[2] = float(self.info['rmin'])
575 vals[3] = float(self.info['rmax'])
576 vals[4] = width / 2. - .5
577 vals[5] = height / 2. - .5
578 vals[6] = img.min() - 1
580 options = np.zeros((10, 10), 'S')
581 options[0] = ['G', 'A', 'U', 'S', 'S', '', '', '', '', '']
582 options = np.array(options, dtype='S')
583 else:
584 vals[0] = self.info['boxcar_x']
585 vals[1] = self.info['boxcar_y']
586 vals[2] = self.info['cycles']
587 vals[3] = float(self.info['rmin'])
588 vals[4] = float(self.info['rmax'])
589 vals[5] = width / 2. - .5
590 vals[6] = height / 2. - .5
592 options = np.zeros((10, 10), 'S')
593 options[0] = ['B', 'O', 'X', 'C', 'A', '', '', '', '', '']
594 options = np.array(options, dtype='S')
596 npix = width
597 nrast = height
598 xb = np.zeros(npix, 'f')
599 yb = np.zeros(npix, 'f')
600 ys = np.zeros(npix, 'f')
601 ysp = np.zeros(npix, 'f')
602 sig = np.zeros(npix, 'f')
603 wrk = np.zeros(9 * npix, 'f')
604 iflag = np.zeros(npix * nrast, 'f')
605 ilog = 6
607 ccp13.bcksmooth(buf=buf,
608 cback=cback,
609 b=b,
610 smbuf=smbuf,
611 vals=vals,
612 options=options,
613 xb=xb,
614 yb=yb,
615 ys=ys,
616 ysp=ysp,
617 sig=sig,
618 wrk=wrk,
619 iflag=iflag,
620 ilog=ilog,
621 nrast=nrast,
622 npix=npix)
624 background = copy.copy(b)
625 background[np.isnan(background)] = 0.
626 background = np.array(background, 'float32')
627 background = background.reshape((height, width))
628 background = background[:fold.shape[0], :fold.shape[1]]
629 result = np.array(fold - background, dtype=np.float32)
630 result = qfu.replaceRmin(result, int(self.info['rmin']), 0.)
632 self.info['bgimg1'] = result
635 def applyRovingWindowBGSub(self):
636 """
637 Apply Roving Window background subtraction
638 :return:
639 """
640 fold = copy.copy(self.info['avg_fold'])
641 # center = [fold.shape[1] + .5, fold.shape[0] + .5]
643 img = self.makeFullImage(fold)
644 width = img.shape[1]
645 height = img.shape[0]
646 img = np.ravel(img)
647 buf = np.array(img, 'f')
648 b = np.zeros(len(buf), 'f')
649 iwid = self.info['win_size_x']
650 jwid = self.info['win_size_y']
651 isep = self.info['win_sep_x']
652 jsep = self.info['win_sep_y']
653 smoo = self.info['smooth']
654 tension = self.info['tension']
655 pc1 = self.info['cirmin'] / 100.
656 pc2 = self.info['cirmax'] / 100.
658 maxdim = width * height
659 maxwin = (iwid * 2 + 1) * (jwid * 2 + 1)
661 ccp13.bgwsrt2(buf=buf,
662 b=b,
663 iwid=iwid,
664 jwid=jwid,
665 isep=isep,
666 jsep=jsep,
667 smoo=smoo,
668 tens=tension,
669 pc1=pc1,
670 pc2=pc2,
671 npix=width,
672 nrast=height,
673 maxdim=maxdim,
674 maxwin=maxwin,
675 xb=np.zeros(maxdim, 'f'),
676 yb=np.zeros(maxdim, 'f'),
677 ys=np.zeros(maxdim, 'f'),
678 ysp=np.zeros(maxdim, 'f'),
679 wrk=np.zeros(9 * maxdim, 'f'),
680 bw=np.zeros(maxwin, 'f'),
681 index_bn=np.zeros(maxwin, 'i'),
682 iprint=0,
683 ilog=6)
685 background = copy.copy(b)
686 background[np.isnan(background)] = 0.
687 background = np.array(background, 'float32')
688 background = background.reshape((height, width))
689 background = background[:fold.shape[0], :fold.shape[1]]
690 result = np.array(fold - background, dtype=np.float32)
691 result = qfu.replaceRmin(result, int(self.info['rmin']), 0.)
693 self.info['bgimg1'] = result
696 def applyCircularlySymBGSub(self):
697 """
698 Apply Circular Background Subtraction to average fold, and save the result to self.info['bgimg1']
699 """
700 copy_img = copy.copy(self.info['avg_fold'])
701 center = [copy_img.shape[1] - .5, copy_img.shape[0] - .5]
702 # npt_rad = int(distance(center, (0, 0)))
704 ai = AzimuthalIntegrator(detector="agilent_titan")
705 ai.setFit2D(100, center[0], center[1])
706 # mask = np.zeros((copy_img.shape[0], copy_img.shape[1]))
708 start_p = self.info["cirmin"] # minimum value of circular background subtraction pixel range in percent
709 end_p = self.info["cirmax"] # maximum value of circular background subtraction pixel range in percent
710 rmin = self.info["rmin"] # minimum radius for background subtraction
711 rmax = self.info["rmax"] # maximum radius for background subtraction
712 radial_bin = self.info["radial_bin"]
713 smoo = self.info['smooth']
714 # tension = self.info['tension']
716 max_pts = (2.*np.pi*rmax / 4. + 10) * radial_bin
717 nBin = int((rmax-rmin)/radial_bin)
719 xs, ys = qfu.getCircularDiscreteBackground(np.array(copy_img, np.float32), rmin, start_p, end_p, radial_bin, nBin, max_pts)
721 max_distance = int(round(distance(center, (0,0)))) + 10
722 sp = UnivariateSpline(xs, ys, s=smoo)
723 newx = np.arange(rmin, rmax)
724 interpolate = sp(newx)
726 newx = np.arange(0, max_distance)
727 newy = list(np.zeros(rmin))
728 newy.extend(list(interpolate))
729 newy.extend(np.zeros(max_distance-rmax))
731 self.info['bg_line'] = [xs, ys, newx, newy]
732 # Create background from spline line
733 background = qfu.createCircularlySymBG(copy_img.shape[1],copy_img.shape[0], np.array(newy, dtype=np.float32))
735 result = copy_img - background
736 # result -= result.min()
738 # Subtract original average fold by background
739 self.info['bgimg1'] = result
741 def getFirstPeak(self, hist):
742 """
743 Find the first peak using the histogram.
744 Start from index 5 and go to the right until slope is less than -10
745 :param hist: histogram
746 """
747 for i in range(5, int(len(hist)/2)):
748 if hist[i] - hist[i-1] < -10:
749 return i
750 return 20
752 def getRminmax(self):
753 """
754 Get R-min and R-max for background subtraction process. If these value is changed, background subtracted images need to be reproduced.
755 """
756 self.parent.statusPrint("Finding Rmin and Rmax...")
757 print("R-min and R-max is being calculated.")
759 if 'fixed_rmin' in self.info and 'fixed_rmax' in self.info:
760 if 'rmin' in self.info and 'rmax' in self.info:
761 if self.info['rmin'] == self.info['fixed_rmin'] and self.info['rmax'] == self.info['fixed_rmax']:
762 return
763 self.info['rmin'] = self.info['fixed_rmin']
764 self.info['rmax'] = self.info['fixed_rmax']
765 elif 'rmin' in self.info and 'rmax' in self.info:
766 return
767 else:
768 copy_img = copy.copy(self.info['avg_fold'])
769 center = [copy_img.shape[1] - 1, copy_img.shape[0] - 1]
770 npt_rad = int(distance(center, (0, 0)))
772 # Get 1D azimuthal integration histogram
773 ai = AzimuthalIntegrator(detector="agilent_titan")
774 ai.setFit2D(100, center[0], center[1])
775 integration_method = pyFAI.method_registry.IntegrationMethod.select_one_available("csr_ocl", 1)
776 _, totalI = ai.integrate1d(copy_img, npt_rad, unit="r_mm", method=integration_method, azimuth_range=(180, 270))
778 self.info['rmin'] = int(round(self.getFirstPeak(totalI) * 1.5))
779 self.info['rmax'] = int(round((min(copy_img.shape[0], copy_img.shape[1]) - 1) * .8))
781 self.deleteFromDict(self.info, 'bgimg1') # remove "bgimg1" from info to make it reprocess
782 self.deleteFromDict(self.info, 'bgimg2') # remove "bgimg1" from info to make it reprocess
783 print("Done. R-min is "+str(self.info['rmin']) + " and R-max is " + str(self.info['rmax']))
785 def apply2DConvexhull(self):
786 """
787 Apply 2D Convex hull Background Subtraction to average fold, and save the result to self.info['bgimg1']
788 """
789 copy_img = copy.copy(self.info['avg_fold'])
791 rmin = self.info['rmin']
792 rmax = self.info['rmax']
793 center = [copy_img.shape[1] - 1, copy_img.shape[0] - 1]
795 hist_x = list(np.arange(rmin, rmax + 1))
796 pchiplines = []
798 det = "agilent_titan"
799 npt_rad = int(distance(center, (0, 0)))
800 ai = AzimuthalIntegrator(detector=det)
801 ai.setFit2D(100, center[0], center[1])
803 for deg in np.arange(180, 271, 1):
804 if deg == 180 :
805 _, I = ai.integrate1d(copy_img, npt_rad, unit="r_mm", method="csr_ocl", azimuth_range=(180, 180.5))
806 elif deg == 270:
807 _, I = ai.integrate1d(copy_img, npt_rad, unit="r_mm", method="csr_ocl", azimuth_range=(269.5, 270))
808 else:
809 _, I = ai.integrate1d(copy_img, npt_rad, unit="r_mm", method="csr_ocl", azimuth_range=(deg-0.5, deg+0.5))
811 hist_y = I[int(rmin):int(rmax+1)]
812 hist_y = list(np.concatenate((hist_y, np.zeros(len(hist_x) - len(hist_y)))))
813 #hist_y = list(I[hist_x])
815 hull_x, hull_y = getHull(hist_x, hist_y)
816 y_pchip = pchip(hull_x, hull_y, hist_x)
817 pchiplines.append(y_pchip)
819 # Smooth each histogram by radius
820 pchiplines = np.array(pchiplines, dtype="float32")
821 pchiplines2 = convolve1d(pchiplines, [1,2,1], axis=0)/4.
823 # Produce Background from each pchip line
824 background = qfu.make2DConvexhullBG2(pchiplines2, copy_img.shape[1], copy_img.shape[0], center[0], center[1], rmin, rmax)
826 # Smooth background image by gaussian filter
827 s = 10
828 w = 4
829 t = (((w - 1.) / 2.) - 0.5) / s
830 background = gaussian_filter(background, sigma=s, truncate=t)
832 # Subtract original average fold by background
833 result = copy_img - background
835 self.info['bgimg1'] = result
837 def calculateAvgFold(self):
838 """
839 Calculate an average fold for 1-4 quadrants. Quadrants are splitted by center and rotation
840 """
841 self.parent.statusPrint("Calculating Avg Fold...")
842 if 'avg_fold' not in self.info.keys():
843 self.deleteFromDict(self.info, 'rmin')
844 self.deleteFromDict(self.info, 'rmax')
845 # self.imgResultForDisplay = None
846 rotate_img = copy.copy(self.getRotatedImage())
847 center = self.info['center']
848 center_x = int(center[0])
849 center_y = int(center[1])
851 print("Quadrant folding is being processed...")
852 img_width = rotate_img.shape[1]
853 img_height = rotate_img.shape[0]
854 fold_width = max(int(center[0]), img_width-int(center[0]))
855 fold_height = max(int(center[1]), img_height-int(center[1]))
857 # Get each fold, and flip them to the same direction
858 top_left = rotate_img[max(center_y-fold_height,0):center_y, max(center_x-fold_width,0):center_x]
859 top_right = rotate_img[max(center_y-fold_height,0):center_y, center_x:center_x+fold_width]
860 top_right = cv2.flip(top_right,1)
861 buttom_left = rotate_img[center_y:center_y+fold_height, max(center_x-fold_width,0):center_x]
862 buttom_left = cv2.flip(buttom_left,0)
863 buttom_right = rotate_img[center_y:center_y+fold_height, center_x:center_x+fold_width]
864 buttom_right = cv2.flip(buttom_right,1)
865 buttom_right = cv2.flip(buttom_right,0)
867 # Add all folds which are not ignored
868 quadrants = np.ones((4, fold_height, fold_width), rotate_img.dtype) * (self.info['mask_thres'] - 1.)
869 for i, quad in enumerate([top_left, top_right, buttom_left, buttom_right]):
870 quadrants[i][-quad.shape[0]:, -quad.shape[1]:] = quad
871 remained = np.ones(4, dtype=bool)
872 remained[list(self.info["ignore_folds"])] = False
873 quadrants = quadrants[remained]
875 # Get average fold from all folds
876 self.get_avg_fold(quadrants,fold_height,fold_width)
877 if 'resultImg' in self.imgCache:
878 del self.imgCache['resultImg']
880 print("Done.")
882 def get_avg_fold(self, quadrants, fold_height, fold_width):
883 """
884 Get average fold from input
885 :param quadrants: 1-4 quadrants
886 :param fold_height: quadrant height
887 :param fold_width: quadrant width
888 :return:
889 """
890 result = np.zeros((fold_height, fold_width))
892 if len(self.info["ignore_folds"]) < 4:
893 # if self.info['pixel_folding']:
894 # average fold by pixel to pixel by cython
895 result = qfu.get_avg_fold_float32(np.array(quadrants, dtype="float32"), len(quadrants), fold_height, fold_width,
896 self.info['mask_thres'])
897 # else:
898 # result = np.mean( np.array(quadrants), axis=0 )
900 self.info['avg_fold'] = result
902 def applyBackgroundSubtraction(self):
903 """
904 Apply background subtraction by user's choice. There are 2 images produced in this process
905 - bgimg1 : image after applying background subtraction INSIDE merge radius
906 - bgimg2 : image after applying background subtraction OUTSIDE merge radius
907 """
908 self.parent.statusPrint("Applying Background Subtraction...")
909 print("Background Subtraction is being processed...")
910 method = self.info["bgsub"]
912 # Produce bgimg1
913 if "bgimg1" not in self.info:
914 avg_fold = np.array(self.info['avg_fold'], dtype="float32")
915 if method == 'None':
916 self.info["bgimg1"] = avg_fold # if method is None, original average fold will be used
917 elif method == '2D Convexhull':
918 self.apply2DConvexhull()
919 elif method == 'Circularly-symmetric':
920 self.applyCircularlySymBGSub2()
921 # self.applyCircularlySymBGSub()
922 elif method == 'White-top-hats':
923 self.info["bgimg1"] = white_tophat(avg_fold, disk(self.info["tophat1"]))
924 elif method == 'Roving Window':
925 self.applyRovingWindowBGSub()
926 elif method == 'Smoothed-Gaussian':
927 self.applySmoothedBGSub('gauss')
928 elif method == 'Smoothed-BoxCar':
929 self.applySmoothedBGSub('boxcar')
930 else:
931 self.info["bgimg1"] = avg_fold
932 self.deleteFromDict(self.imgCache, "BgSubFold")
934 # Produce bgimg2
935 if "bgimg2" not in self.info:
936 avg_fold = np.array(self.info['avg_fold'], dtype="float32")
937 if method == 'None':
938 self.info["bgimg2"] = avg_fold # if method is 'None', original average fold will be used
939 else:
940 self.info["bgimg2"] = white_tophat(avg_fold, disk(self.info["tophat2"]))
941 self.deleteFromDict(self.imgCache, "BgSubFold")
943 print("Done.")
945 def mergeImages(self):
946 """
947 Merge bgimg1 and bgimg2 at merge radius, with sigmoid as a merge gradient param.
948 The result of merging will be kept in self.info["BgSubFold"]
949 :return:
950 """
951 self.parent.statusPrint("Merging Images...")
952 print("Merging images...")
954 if "BgSubFold" not in self.imgCache:
955 img1 = np.array(self.info["bgimg1"], dtype="float32")
956 img2 = np.array(self.info["bgimg2"], dtype="float32")
957 sigmoid = self.info["sigmoid"]
958 center = [img1.shape[1]-1, img1.shape[0]-1]
959 rad = self.info["rmax"] - 10
961 # Merge 2 images at merge radius using sigmoid as merge gradient
962 self.imgCache['BgSubFold'] = qfu.combine_bgsub_float32(img1, img2, center[0], center[1], sigmoid, rad)
963 self.deleteFromDict(self.imgCache, "resultImg")
965 print("Done.")
967 def generateResultImage(self):
968 """
969 Put 4 self.info["BgSubFold"] together as a result image
970 :return:
971 """
972 self.parent.statusPrint("Generating Resultant Image...")
973 print("Generating result image from average fold...")
974 result = self.makeFullImage(copy.copy(self.imgCache['BgSubFold']))
975 if 'rotate' in self.info and self.info['rotate']:
976 result = np.rot90(result)
977 self.imgCache['resultImg'] = result
978 print("Done.")
980 def makeFullImage(self, fold):
981 """
982 Flip + rotate 4 folds and combine them to 1 image
983 :param fold:
984 :return: result image
985 """
986 fold_height = fold.shape[0]
987 fold_width = fold.shape[1]
989 top_left = fold
990 top_right = cv2.flip(fold, 1)
992 buttom_left = cv2.flip(fold, 0)
993 buttom_right = cv2.flip(buttom_left, 1)
995 resultImg = np.zeros((fold_height * 2, fold_width * 2))
996 resultImg[0:fold_height, 0:fold_width] = top_left
997 resultImg[0:fold_height, fold_width:fold_width * 2] = top_right
998 resultImg[fold_height:fold_height * 2, 0:fold_width] = buttom_left
999 resultImg[fold_height:fold_height * 2, fold_width:fold_width * 2] = buttom_right
1001 return resultImg
1003 def statusPrint(self, text):
1004 """
1005 Print the text in the window or in the terminal depending on if we are using GUI or headless.
1006 :param text: text to print
1007 :return: -
1008 """
1009 print(text)