Coverage for modules/QuadrantFolder.py: 46%

600 statements  

« prev     ^ index     » next       coverage.py v7.0.4, created at 2023-01-10 09:27 -0600

1""" 

2Copyright 1999 Illinois Institute of Technology 

3 

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: 

11 

12The above copyright notice and this permission notice shall be 

13included in all copies or substantial portions of the Software. 

14 

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. 

22 

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""" 

28 

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 * 

49 

50# Make sure the cython part is compiled 

51# from subprocess import call 

52# call(["python setup2.py build_ext --inplace"], shell = True) 

53 

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 

84 

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 

97 

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 } 

105 

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) 

116 

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 

132 

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) 

142 

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] 

152 

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() 

175 

176 if "no_cache" not in flags: 

177 self.cacheInfo() 

178 

179 self.parent.statusPrint("") 

180 

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) 

193 

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 

206 

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 

224 

225 self.orig_img = img 

226 self.masked = True 

227 

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'])) 

254 

255 

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") 

278 

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 

295 

296 extent = [self.info['center'][0] - center[0], self.info['center'][1] - center[1]] 

297 

298 return extent, center 

299 

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])) 

319 

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) 

327 

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 

336 

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"] 

344 

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)) 

358 

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) 

363 

364 

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 

375 

376 b, l = img.shape 

377 rotImg, newCenter, self.rotMat = rotateImage(img, center, self.info["rotationAngle"], self.img_type, self.info['mask_thres']) 

378 

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 

385 

386 return final_rotImg 

387 

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] 

398 

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 

408 

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))) 

416 

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])) 

420 

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 

427 

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) 

432 

433 I2D = np.array(I2D) 

434 

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 

443 

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] 

447 

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]) 

458 

459 # Get mean value of pixel range 

460 subr.append(np.mean(rad)) 

461 

462 subr_hist = subr[rmin:rmax + 1] 

463 hist_x = list(range(0, len(subr_hist))) 

464 

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)) 

468 

469 subr_hist = np.concatenate((np.zeros(rmin), y_pchip)) 

470 subr_hist = np.concatenate((subr_hist, np.zeros(len(subr) - rmax))) 

471 

472 sub_tr.append(subr_hist) 

473 

474 

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) 

477 

478 result = copy_img - bg_img 

479 result -= result.min() 

480 

481 # Subtract original average fold by background 

482 self.info['bgimg1'] = result 

483 

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] 

490 

491 img = self.makeFullImage(fold) 

492 img = img.astype("float32") 

493 width = img.shape[1] 

494 height = img.shape[0] 

495 

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. 

508 

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') 

516 

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) 

540 

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.) 

548 

549 self.info['bgimg1'] = result 

550 

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']) 

557 

558 img = self.makeFullImage(fold) 

559 img = img.astype("float32") 

560 width = img.shape[1] 

561 height = img.shape[0] 

562 

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') 

570 

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 

579 

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 

591 

592 options = np.zeros((10, 10), 'S') 

593 options[0] = ['B', 'O', 'X', 'C', 'A', '', '', '', '', ''] 

594 options = np.array(options, dtype='S') 

595 

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 

606 

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) 

623 

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.) 

631 

632 self.info['bgimg1'] = result 

633 

634 

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] 

642 

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. 

657 

658 maxdim = width * height 

659 maxwin = (iwid * 2 + 1) * (jwid * 2 + 1) 

660 

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) 

684 

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.) 

692 

693 self.info['bgimg1'] = result 

694 

695 

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))) 

703 

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])) 

707 

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'] 

715 

716 max_pts = (2.*np.pi*rmax / 4. + 10) * radial_bin 

717 nBin = int((rmax-rmin)/radial_bin) 

718 

719 xs, ys = qfu.getCircularDiscreteBackground(np.array(copy_img, np.float32), rmin, start_p, end_p, radial_bin, nBin, max_pts) 

720 

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) 

725 

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)) 

730 

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)) 

734 

735 result = copy_img - background 

736 # result -= result.min() 

737 

738 # Subtract original average fold by background 

739 self.info['bgimg1'] = result 

740 

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 

751 

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.") 

758 

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))) 

771 

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)) 

777 

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)) 

780 

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'])) 

784 

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']) 

790 

791 rmin = self.info['rmin'] 

792 rmax = self.info['rmax'] 

793 center = [copy_img.shape[1] - 1, copy_img.shape[0] - 1] 

794 

795 hist_x = list(np.arange(rmin, rmax + 1)) 

796 pchiplines = [] 

797 

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]) 

802 

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)) 

810 

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]) 

814 

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) 

818 

819 # Smooth each histogram by radius 

820 pchiplines = np.array(pchiplines, dtype="float32") 

821 pchiplines2 = convolve1d(pchiplines, [1,2,1], axis=0)/4. 

822 

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) 

825 

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) 

831 

832 # Subtract original average fold by background 

833 result = copy_img - background 

834 

835 self.info['bgimg1'] = result 

836 

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]) 

850 

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])) 

856 

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) 

866 

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] 

874 

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'] 

879 

880 print("Done.") 

881 

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)) 

891 

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 ) 

899 

900 self.info['avg_fold'] = result 

901 

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"] 

911 

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") 

933 

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") 

942 

943 print("Done.") 

944 

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...") 

953 

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 

960 

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") 

964 

965 print("Done.") 

966 

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.") 

979 

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] 

988 

989 top_left = fold 

990 top_right = cv2.flip(fold, 1) 

991 

992 buttom_left = cv2.flip(fold, 0) 

993 buttom_right = cv2.flip(buttom_left, 1) 

994 

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 

1000 

1001 return resultImg 

1002 

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)