Coverage for ui/DIImageWindowh.py: 44%

265 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 logging 

30import json 

31from csv import writer 

32from matplotlib import scale as mscale 

33from matplotlib import transforms as mtransforms 

34from matplotlib.ticker import Formatter, AutoLocator 

35import pandas as pd 

36import numpy as np 

37from numpy import ma 

38try: 

39 from .pyqt_utils import * 

40 from ..utils.file_manager import * 

41 from ..modules.ScanningDiffraction import * 

42 from ..csv_manager import DI_CSVManager 

43except: # for coverage 

44 from ui.pyqt_utils import * 

45 from utils.file_manager import * 

46 from modules.ScanningDiffraction import * 

47 from csv_manager import DI_CSVManager 

48 

49class DSpacingScale(mscale.ScaleBase): 

50 """ 

51 D Spacing scale class 

52 """ 

53 name = 'dspacing' 

54 def __init__(self, axis, **kwargs): 

55 mscale.ScaleBase.__init__(self) 

56 self.lambda_sdd = kwargs.pop('lambda_sdd', 1501.45) 

57 

58 def get_transform(self): 

59 """ 

60 Give the D Spacing tranform object 

61 """ 

62 return self.DSpacingTransform(self.lambda_sdd) 

63 

64 def set_default_locators_and_formatters(self, axis): 

65 """ 

66 Override to set up the locators and formatters to use with the 

67 scale. This is only required if the scale requires custom 

68 locators and formatters. Writing custom locators and 

69 formatters is rather outside the scope of this example, but 

70 there are many helpful examples in ``ticker.py``. 

71 

72 In our case, the Mercator example uses a fixed locator from 

73 -90 to 90 degrees and a custom formatter class to put convert 

74 the radians to degrees and put a degree symbol after the 

75 value:: 

76 """ 

77 class DSpacingFormatter(Formatter): 

78 """ 

79 D Spacing formatter 

80 """ 

81 def __init__(self, lambda_sdd): 

82 Formatter.__init__(self) 

83 self.lambda_sdd = lambda_sdd 

84 def __call__(self, x, pos=None): 

85 if x == 0: 

86 return "\u221E" 

87 else: 

88 return "%.2f" % (self.lambda_sdd / x) 

89 

90 axis.set_major_locator(AutoLocator()) 

91 axis.set_major_formatter(DSpacingFormatter(self.lambda_sdd)) 

92 axis.set_minor_formatter(DSpacingFormatter(self.lambda_sdd)) 

93 

94 def limit_range_for_scale(self, vmin, vmax, minpos): 

95 """ 

96 Override to limit the bounds of the axis to the domain of the 

97 transform. In the case of Mercator, the bounds should be 

98 limited to the threshold that was passed in. Unlike the 

99 autoscaling provided by the tick locators, this range limiting 

100 will always be adhered to, whether the axis range is set 

101 manually, determined automatically or changed through panning 

102 and zooming. 

103 """ 

104 return max(vmin, 1), vmax 

105 

106 class DSpacingTransform(mtransforms.Transform): 

107 """ 

108 There are two value members that must be defined. 

109 ``input_dims`` and ``output_dims`` specify number of input 

110 dimensions and output dimensions to the transformation. 

111 These are used by the transformation framework to do some 

112 error checking and prevent incompatible transformations from 

113 being connected together. When defining transforms for a 

114 scale, which are, by definition, separable and have only one 

115 dimension, these members should always be set to 1. 

116 """ 

117 input_dims = 1 

118 output_dims = 1 

119 is_separable = True 

120 has_inverse = True 

121 def __init__(self, lambda_sdd): 

122 mtransforms.Transform.__init__(self) 

123 self.lambda_sdd = lambda_sdd 

124 

125 def transform_non_affine(self, a): 

126 """ 

127 This transform takes an Nx1 ``numpy`` array and returns a 

128 transformed copy. Since the range of the Mercator scale 

129 is limited by the user-specified threshold, the input 

130 array must be masked to contain only valid values. 

131 ``matplotlib`` will handle masked arrays and remove the 

132 out-of-range data from the plot. Importantly, the 

133 ``transform`` method *must* return an array that is the 

134 same shape as the input array, since these values need to 

135 remain synchronized with values in the other dimension. 

136 """ 

137 masked = ma.masked_where(a <= 0, a) 

138 if masked.mask.any(): 

139 return self.lambda_sdd / masked 

140 else: 

141 return self.lambda_sdd / a 

142 

143 def inverted(self): 

144 """ 

145 Override this method so matplotlib knows how to get the 

146 inverse transform for this transform. 

147 """ 

148 return DSpacingScale.InvertedDSpacingTransform( 

149 self.lambda_sdd) 

150 

151 class InvertedDSpacingTransform(mtransforms.Transform): 

152 """ 

153 Inverted of the previous class 

154 """ 

155 input_dims = 1 

156 output_dims = 1 

157 is_separable = True 

158 has_inverse = True 

159 def __init__(self, lambda_sdd): 

160 mtransforms.Transform.__init__(self) 

161 self.lambda_sdd = lambda_sdd 

162 

163 def transform_non_affine(self, a): 

164 """ 

165 See previous class 

166 """ 

167 masked = ma.masked_where(a <= 0, a) 

168 if masked.mask.any(): 

169 return np.flipud(self.lambda_sdd / masked) 

170 else: 

171 return np.flipud(self.lambda_sdd / a) 

172 

173 def inverted(self): 

174 """ 

175 See previous class 

176 """ 

177 return DSpacingScale.DSpacingTransform(self.lambda_sdd) 

178 

179mscale.register_scale(DSpacingScale) 

180 

181class DIImageWindowh(): 

182 """ 

183 A class to process Scanning diffraction on a file 

184 """ 

185 def __init__(self, image_name = "", dir_path = "", inputflags=False, delcache=False, inputflagpath='musclex/settings/disettings.json', process_folder=False,imgList = None): 

186 self.fileName = image_name 

187 self.filePath = dir_path 

188 self.fullPath = os.path.join(dir_path, image_name) 

189 self.inputflag=inputflags 

190 self.delcache=delcache 

191 self.inputflagfile=inputflagpath 

192 

193 self.csvManager = DI_CSVManager(dir_path) 

194 self.imgList = [] 

195 self.numberOfFiles = 0 

196 self.currentFileNumber = 0 

197 

198 self.cirProj = None 

199 self.calSettings = None 

200 self.mask = None 

201 self.function = None 

202 self.checkable_buttons = [] 

203 self.fixed_hull_range = None 

204 self.ROI = None 

205 self.merged_peaks = None 

206 self.orientationModel = None 

207 self.in_batch_process = False 

208 self.pixelDataFile = None 

209 

210 self.stop_process = False 

211 self.intensityRange = [] 

212 self.updatingUI = False 

213 self.ring_colors = [] 

214 

215 self.m1_selected_range = 0 

216 self.update_plot = {'m1_partial_hist': True, 

217 'm1_hist': True, 

218 'm2_diff': True, 

219 'image_result': True, 

220 'results_text': True 

221 } 

222 self.logger = None 

223 self.onNewFileSelected(imgList) 

224 if process_folder and len(self.imgList) > 0: 

225 self.processFolder() 

226 elif len(self.imgList) > 0: 

227 self.onImageChanged() 

228 

229 def generateRingColors(self): 

230 """ 

231 Generate colors for the rings 

232 """ 

233 possible_vals = [0, 255] 

234 self.ring_colors = [] 

235 for b in possible_vals: 

236 for g in possible_vals: 

237 for r in possible_vals: 

238 if b==0 and g==0 and r==0: 

239 continue 

240 self.ring_colors.append([b,g,r]) 

241 

242 def processFolder(self): 

243 """ 

244 Process current folder 

245 """ 

246 ## Popup confirm dialog with settings 

247 nImg = len(self.imgList) 

248 print('Process Current Folder') 

249 text = 'The current folder will be processed using current settings. Make sure to adjust them before processing the folder. \n\n' 

250 flags = self.getFlags() 

251 text += "\nCurrent Settings" 

252 text += "\n - Partial integration angle range : "+ str(flags['partial_angle']) 

253 if 'orientation_model' in flags: 

254 text += "\n - Orientation Model : "+ flags['orientation_model'] 

255 if 'ROI' in flags: 

256 text += "\n - ROI : "+ str(flags['ROI']) 

257 if 'fixed_hull' in flags: 

258 text += "\n - R-min & R-max : "+ str(flags['fixed_hull']) 

259 text += '\n\nAre you sure you want to process ' + str(nImg) + ' image(s) in this Folder? \nThis might take a long time.' 

260 

261 log_path = fullPath(self.filePath, 'log') 

262 if not exists(log_path): 

263 os.makedirs(log_path) 

264 

265 current = time.localtime() 

266 filename = "CirProj_""%02d" % current.tm_year + "%02d" % current.tm_mon + "%02d" % current.tm_mday + \ 

267 "_" + "%02d" % current.tm_hour + "%02d" % current.tm_min + "%02d" % current.tm_sec + ".log" 

268 filename = fullPath(log_path, filename) 

269 self.logger = logging.getLogger('di') 

270 self.logger.setLevel(logging.DEBUG) 

271 self.logger.propagate = False 

272 

273 # create a file handler 

274 handler = logging.FileHandler(filename) 

275 handler.setLevel(logging.DEBUG) 

276 

277 # create a logging format 

278 formatter = logging.Formatter('%(asctime)s: %(message)s') 

279 handler.setFormatter(formatter) 

280 

281 # add the handlers to the self.logger 

282 self.logger.addHandler(handler) 

283 self.logger.addFilter(logging.Filter(name='di')) 

284 

285 ## Process all images and update progress bar 

286 self.in_batch_process = True 

287 self.stop_process = False 

288 for _ in range(nImg): 

289 if self.stop_process: 

290 break 

291 self.nextImage() 

292 

293 self.in_batch_process = False 

294 self.folder_processed = True 

295 

296 def getFlags(self, imgChanged=True): 

297 """ 

298 Give the flags for the object associated 

299 """ 

300 if self.inputflag: 

301 try: 

302 with open(self.inputflagfile) as f: 

303 flags=json.load(f) 

304 except Exception: 

305 print("Can't load setting file") 

306 self.inputflag=False 

307 flags={"partial_angle": 90, "orientation_model": "GMM3", "90rotation": False} 

308 else: 

309 flags={"partial_angle": 90, "orientation_model": "GMM3", "90rotation": False} 

310 

311 return flags 

312 

313 def onNewFileSelected(self, imgList): 

314 """ 

315 Used when a new file is selected 

316 """ 

317 if imgList is not None: 

318 self.imgList = imgList 

319 else: 

320 self.filePath, self.imgList, self.currentFileNumber, self.fileList, self.ext = getImgFiles(self.fullPath) 

321 # self.imgList, _ = getFilesAndHdf(self.filePath) 

322 

323 # self.imgList.sort() 

324 self.numberOfFiles = len(self.imgList) 

325 # if len(self.fileName) > 0: 

326 # self.filePath, self.imgList, self.currentFileNumber, self.fileList, self.ext = getImgFiles(self.fullPath) 

327 # self.currentFileNumber = self.imgList.index(self.fileName) 

328 # else: 

329 # self.currentFileNumber = 0 

330 

331 def onImageChanged(self): 

332 """ 

333 When the image is changed, process the scanning diffraction again 

334 """ 

335 file=self.fileName+'.info' 

336 cache_path = os.path.join(self.filePath, "di_cache",file) 

337 cache_exist=os.path.isfile(cache_path) 

338 if self.delcache: 

339 if os.path.isfile(cache_path): 

340 print('cache is deleted') 

341 os.remove(cache_path) 

342 fileName = self.imgList[self.currentFileNumber] 

343 print("current file is "+fileName) 

344 self.cirProj = ScanningDiffraction(self.filePath, fileName, self.fileList, self.ext, logger=self.logger) 

345 self.processImage(True) 

346 print('---------------------------------------------------') 

347 

348 if self.inputflag and cache_exist and not self.delcache: 

349 print('cache exists, provided setting file was not used ') 

350 elif self.inputflag and (not cache_exist or self.delcache): 

351 print('setting file provided and used for fitting') 

352 elif not self.inputflag and cache_exist and not self.delcache: 

353 print('cache exist, no fitting was performed') 

354 elif not self.inputflag and (self.delcache or not cache_exist): 

355 print('fitting with default settings') 

356 print('default settings are "partial_angle": 90, "orientation_model": "GMM3", "90rotation": False') 

357 

358 print('---------------------------------------------------') 

359 

360 def processImage(self, imgChanged=False): 

361 """ 

362 Process the scanning diffraction 

363 """ 

364 if self.cirProj is not None: 

365 flags = self.getFlags(imgChanged) 

366 self.cirProj.process(flags) 

367 self.updateParams() 

368 self.csvManager.write_new_data(self.cirProj) 

369 

370 def create_circular_mask(self, h, w, center, radius): 

371 """ 

372 Create a circular mask 

373 """ 

374 Y, X = np.ogrid[:h, :w] 

375 dist_from_center = np.sqrt((X - center[0]) ** 2 + (Y - center[1]) ** 2) 

376 

377 mask = dist_from_center > radius 

378 return mask 

379 

380 def addPixelDataToCsv(self, grid_lines): 

381 """ 

382 Add pixel data to csv 

383 """ 

384 if self.pixelDataFile is None: 

385 self.pixelDataFile = self.filePath + '/di_results/BackgroundSummary.csv' 

386 if not os.path.isfile(self.pixelDataFile): 

387 header = ['File Name', 'Average Pixel Value (Outside rmin or mask)', 'Number of Pixels (Outside rmin or mask)'] 

388 f = open(self.pixelDataFile, 'a') 

389 csv_writer = writer(f) 

390 csv_writer.writerow(header) 

391 f.close() 

392 

393 csvDF = pd.read_csv(self.pixelDataFile) 

394 recordedFileNames = set(csvDF['File Name'].values) 

395 

396 # Compute the average pixel value and number of pixels outside rmin/mask 

397 _, mask = getBlankImageAndMask(self.filePath) 

398 img = copy.copy(self.cirProj.original_image) 

399 if mask is not None: 

400 numberOfPixels = np.count_nonzero(mask == 0) 

401 averagePixelValue = np.average(img[mask == 0]) 

402 else: 

403 h,w = img.shape 

404 rmin = self.cirProj.info['start_point'] 

405 cir_mask = self.create_circular_mask(h,w,center=self.cirProj.info['center'], radius=rmin) 

406 # Exclude grid lines in computation 

407 print("Gird Lines Coordinates ", grid_lines) 

408 cir_mask[grid_lines] = 0 

409 numberOfPixels = np.count_nonzero(cir_mask) 

410 averagePixelValue = np.average(img[cir_mask]) 

411 

412 if self.cirProj.filename in recordedFileNames: 

413 csvDF.loc[csvDF['File Name'] == self.cirProj.filename, 'Average Pixel Value'] = averagePixelValue 

414 csvDF.loc[csvDF['File Name'] == self.cirProj.filename, 'Number of Pixels'] = numberOfPixels 

415 else: 

416 next_row_index = csvDF.shape[0] 

417 csvDF.loc[next_row_index] = [self.cirProj.filename, averagePixelValue, numberOfPixels] 

418 csvDF.to_csv(self.pixelDataFile, index=False) 

419 

420 def setMinMaxIntensity(self, img, minInt, maxInt, minIntLabel, maxIntLabel): 

421 """ 

422 Set the min and max intensity 

423 """ 

424 min_val = img.min() 

425 max_val = img.max() 

426 self.intensityRange = [min_val, max_val-1, min_val+1, max_val] 

427 minInt.setMinimum(self.intensityRange[0]) 

428 minInt.setMaximum(self.intensityRange[1]) 

429 maxInt.setMinimum(self.intensityRange[2]) 

430 maxInt.setMaximum(self.intensityRange[3]) 

431 step = max(1., (max_val-min_val)/100) 

432 minInt.setSingleStep(step) 

433 maxInt.setSingleStep(step) 

434 minIntLabel.setText("Min intensity (" + str(min_val) + ")") 

435 maxIntLabel.setText("Max intensity (" + str(max_val) + ")") 

436 

437 if img.dtype == 'float32': 

438 decimal = 2 

439 else: 

440 decimal = 0 

441 

442 maxInt.setDecimals(decimal) 

443 minInt.setDecimals(decimal) 

444 

445 if maxInt.value() == 1. and minInt.value() == 0.: 

446 self.updatingUI = True 

447 minInt.setValue(min_val) 

448 maxInt.setValue(max_val*0.1) 

449 self.updatingUI = False 

450 

451 def updateParams(self): 

452 """ 

453 Update the parameters 

454 """ 

455 info = self.cirProj.info 

456 if 'fixed_hull' in info: 

457 self.fixed_hull_range = info['fixed_hull'] 

458 if 'merged_peaks' in info: 

459 self.merged_peaks = info['merged_peaks'] 

460 if self.ROI is None and info['ROI'] != [info['start_point'], info['rmax']]: 

461 self.ROI = info['ROI'] 

462 

463 def nextImage(self): 

464 """ 

465 When next image is clicked 

466 """ 

467 self.currentFileNumber = (self.currentFileNumber + 1) % self.numberOfFiles 

468 self.onImageChanged()