Coverage for ui/DIImageWindowh.py: 44%
265 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 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
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)
58 def get_transform(self):
59 """
60 Give the D Spacing tranform object
61 """
62 return self.DSpacingTransform(self.lambda_sdd)
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``.
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)
90 axis.set_major_locator(AutoLocator())
91 axis.set_major_formatter(DSpacingFormatter(self.lambda_sdd))
92 axis.set_minor_formatter(DSpacingFormatter(self.lambda_sdd))
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
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
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
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)
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
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)
173 def inverted(self):
174 """
175 See previous class
176 """
177 return DSpacingScale.DSpacingTransform(self.lambda_sdd)
179mscale.register_scale(DSpacingScale)
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
193 self.csvManager = DI_CSVManager(dir_path)
194 self.imgList = []
195 self.numberOfFiles = 0
196 self.currentFileNumber = 0
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
210 self.stop_process = False
211 self.intensityRange = []
212 self.updatingUI = False
213 self.ring_colors = []
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()
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])
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.'
261 log_path = fullPath(self.filePath, 'log')
262 if not exists(log_path):
263 os.makedirs(log_path)
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
273 # create a file handler
274 handler = logging.FileHandler(filename)
275 handler.setLevel(logging.DEBUG)
277 # create a logging format
278 formatter = logging.Formatter('%(asctime)s: %(message)s')
279 handler.setFormatter(formatter)
281 # add the handlers to the self.logger
282 self.logger.addHandler(handler)
283 self.logger.addFilter(logging.Filter(name='di'))
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()
293 self.in_batch_process = False
294 self.folder_processed = True
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}
311 return flags
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)
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
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('---------------------------------------------------')
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')
358 print('---------------------------------------------------')
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)
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)
377 mask = dist_from_center > radius
378 return mask
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()
393 csvDF = pd.read_csv(self.pixelDataFile)
394 recordedFileNames = set(csvDF['File Name'].values)
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])
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)
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) + ")")
437 if img.dtype == 'float32':
438 decimal = 2
439 else:
440 decimal = 0
442 maxInt.setDecimals(decimal)
443 minInt.setDecimals(decimal)
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
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']
463 def nextImage(self):
464 """
465 When next image is clicked
466 """
467 self.currentFileNumber = (self.currentFileNumber + 1) % self.numberOfFiles
468 self.onImageChanged()