graxpert 3.2.0a0.dev4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. graxpert/AstroImageRepository.py +114 -0
  2. graxpert/__init__.py +1 -0
  3. graxpert/ai_model_handling.py +215 -0
  4. graxpert/app_state.py +10 -0
  5. graxpert/application/__init__.py +0 -0
  6. graxpert/application/app.py +741 -0
  7. graxpert/application/app_events.py +96 -0
  8. graxpert/application/eventbus.py +24 -0
  9. graxpert/astroimage.py +308 -0
  10. graxpert/background_extraction.py +290 -0
  11. graxpert/background_flood_selection.py +240 -0
  12. graxpert/background_grid_selection.py +64 -0
  13. graxpert/cmdline_tools.py +547 -0
  14. graxpert/commands.py +200 -0
  15. graxpert/deconvolution.py +176 -0
  16. graxpert/denoising.py +178 -0
  17. graxpert/grid_utils.py +38 -0
  18. graxpert/img/GraXpert_LOGO_Hauptvariante.png +0 -0
  19. graxpert/img/Icon.png +0 -0
  20. graxpert/img/LDN1235_original.jpg +0 -0
  21. graxpert/img/LDN1235_processed.jpg +0 -0
  22. graxpert/img/__init__.py +0 -0
  23. graxpert/img/gfx_numbers.png +0 -0
  24. graxpert/img/gfx_numbers.svg +737 -0
  25. graxpert/img/graXpert_Startbadge_Umbriel.png +0 -0
  26. graxpert/img/hourglass.png +0 -0
  27. graxpert/locales/__init__.py +0 -0
  28. graxpert/locales/de_DE/LC_MESSAGES/base.mo +0 -0
  29. graxpert/localization.py +39 -0
  30. graxpert/main.py +441 -0
  31. graxpert/mp_logging.py +97 -0
  32. graxpert/parallel_processing.py +3 -0
  33. graxpert/preferences.py +139 -0
  34. graxpert/radialbasisinterpolation.py +215 -0
  35. graxpert/resource_utils.py +35 -0
  36. graxpert/s3_secrets.py +16 -0
  37. graxpert/skyall.py +152 -0
  38. graxpert/stretch.py +151 -0
  39. graxpert/theme/__init__.py +0 -0
  40. graxpert/theme/graxpert-dark-blue.json +367 -0
  41. graxpert/ui/__init__.py +1 -0
  42. graxpert/ui/application_frame.py +115 -0
  43. graxpert/ui/canvas.py +558 -0
  44. graxpert/ui/left_menu.py +437 -0
  45. graxpert/ui/loadingframe.py +142 -0
  46. graxpert/ui/right_menu.py +315 -0
  47. graxpert/ui/statusbar.py +102 -0
  48. graxpert/ui/styling.py +23 -0
  49. graxpert/ui/tooltip.py +194 -0
  50. graxpert/ui/ui_events.py +19 -0
  51. graxpert/ui/widgets.py +297 -0
  52. graxpert/ui_scaling.py +13 -0
  53. graxpert/version.py +2 -0
  54. graxpert-3.2.0a0.dev4.dist-info/METADATA +219 -0
  55. graxpert-3.2.0a0.dev4.dist-info/RECORD +58 -0
  56. graxpert-3.2.0a0.dev4.dist-info/WHEEL +5 -0
  57. graxpert-3.2.0a0.dev4.dist-info/entry_points.txt +2 -0
  58. graxpert-3.2.0a0.dev4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,741 @@
1
+ import logging
2
+ import os
3
+ import tkinter as tk
4
+ from tkinter import messagebox
5
+
6
+ import numpy as np
7
+ from appdirs import user_config_dir
8
+
9
+ from graxpert.ai_model_handling import (
10
+ ai_model_path_from_version,
11
+ bge_ai_models_dir,
12
+ deconvolution_object_ai_models_dir,
13
+ deconvolution_stars_ai_models_dir,
14
+ denoise_ai_models_dir,
15
+ download_version,
16
+ validate_local_version,
17
+ )
18
+ from graxpert.app_state import INITIAL_STATE
19
+ from graxpert.application.app_events import AppEvents
20
+ from graxpert.application.eventbus import eventbus
21
+ from graxpert.astroimage import AstroImage
22
+ from graxpert.AstroImageRepository import AstroImageRepository, ImageTypes
23
+ from graxpert.background_extraction import extract_background
24
+ from graxpert.commands import INIT_HANDLER, RESET_POINTS_HANDLER, RM_POINT_HANDLER, SEL_POINTS_HANDLER, Command
25
+ from graxpert.deconvolution import deconvolve
26
+ from graxpert.denoising import denoise
27
+ from graxpert.localization import _
28
+ from graxpert.mp_logging import logfile_name
29
+ from graxpert.preferences import fitsheader_2_app_state, load_preferences, prefs_2_app_state
30
+ from graxpert.s3_secrets import bge_bucket_name, deconvolution_object_bucket_name, deconvolution_stars_bucket_name, denoise_bucket_name
31
+ from graxpert.stretch import StretchParameters, stretch_all
32
+ from graxpert.ui.loadingframe import DynamicProgressThread
33
+
34
+
35
+ class GraXpert:
36
+
37
+ def __init__(self):
38
+ self.initialize()
39
+
40
+ def initialize(self):
41
+ # app preferences
42
+ prefs_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json")
43
+ self.prefs = load_preferences(prefs_filename)
44
+
45
+ self.filename = ""
46
+ self.data_type = ""
47
+
48
+ self.images = AstroImageRepository()
49
+ self.display_type = ImageTypes.Original
50
+
51
+ self.mat_affine = np.eye(3)
52
+
53
+ # state handling
54
+ tmp_state = prefs_2_app_state(self.prefs, INITIAL_STATE)
55
+
56
+ self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state.background_points)
57
+ self.cmd.execute()
58
+
59
+ # image loading
60
+ eventbus.add_listener(AppEvents.OPEN_FILE_DIALOG_REQUEST, self.on_open_file_dialog_request)
61
+ eventbus.add_listener(AppEvents.LOAD_IMAGE_REQUEST, self.on_load_image)
62
+ # image display
63
+ eventbus.add_listener(AppEvents.DISPLAY_TYPE_CHANGED, self.on_display_type_changed)
64
+ # stretch options
65
+ eventbus.add_listener(AppEvents.STRETCH_OPTION_CHANGED, self.on_stretch_option_changed)
66
+ eventbus.add_listener(AppEvents.CHANGE_SATURATION_REQUEST, self.on_change_saturation_request)
67
+ eventbus.add_listener(AppEvents.CHANNELS_LINKED_CHANGED, self.on_channels_linked_option_changed)
68
+ # sample selection
69
+ eventbus.add_listener(AppEvents.DISPLAY_PTS_CHANGED, self.on_display_pts_changed)
70
+ eventbus.add_listener(AppEvents.BG_FLOOD_SELECTION_CHANGED, self.on_bg_floot_selection_changed)
71
+ eventbus.add_listener(AppEvents.BG_PTS_CHANGED, self.on_bg_pts_changed)
72
+ eventbus.add_listener(AppEvents.BG_TOL_CHANGED, self.on_bg_tol_changed)
73
+ eventbus.add_listener(AppEvents.CREATE_GRID_REQUEST, self.on_create_grid_request)
74
+ eventbus.add_listener(AppEvents.RESET_POITS_REQUEST, self.on_reset_points_request)
75
+ # calculation
76
+ eventbus.add_listener(AppEvents.INTERPOL_TYPE_CHANGED, self.on_interpol_type_changed)
77
+ eventbus.add_listener(AppEvents.SMOTTHING_CHANGED, self.on_smoothing_changed)
78
+ eventbus.add_listener(AppEvents.CALCULATE_REQUEST, self.on_calculate_request)
79
+ # deconvolution
80
+ eventbus.add_listener(AppEvents.DECONVOLUTION_TYPE_CHANGED, self.on_deconvolution_type_changed)
81
+ eventbus.add_listener(AppEvents.DECONVOLUTION_STRENGTH_CHANGED, self.on_deconvolution_strength_changed)
82
+ eventbus.add_listener(AppEvents.DECONVOLUTION_PSFSIZE_CHANGED, self.on_deconvolution_psfsize_changed)
83
+ eventbus.add_listener(AppEvents.DECONVOLUTION_REQUEST, self.on_deconvolution_request)
84
+ # denoising
85
+ eventbus.add_listener(AppEvents.DENOISE_STRENGTH_CHANGED, self.on_denoise_strength_changed)
86
+ eventbus.add_listener(AppEvents.DENOISE_REQUEST, self.on_denoise_request)
87
+ # saving
88
+ eventbus.add_listener(AppEvents.SAVE_AS_CHANGED, self.on_save_as_changed)
89
+ eventbus.add_listener(AppEvents.SAVE_STRETCHED_CHANGED, self.on_save_stretched_changed)
90
+ eventbus.add_listener(AppEvents.SAVE_REQUEST, self.on_save_request)
91
+ # advanced settings
92
+ eventbus.add_listener(AppEvents.SAMPLE_SIZE_CHANGED, self.on_sample_size_changed)
93
+ eventbus.add_listener(AppEvents.SAMPLE_COLOR_CHANGED, self.on_sample_color_changed)
94
+ eventbus.add_listener(AppEvents.RBF_KERNEL_CHANGED, self.on_rbf_kernel_changed)
95
+ eventbus.add_listener(AppEvents.SPLINE_ORDER_CHANGED, self.on_spline_order_changed)
96
+ eventbus.add_listener(AppEvents.CORRECTION_TYPE_CHANGED, self.on_correction_type_changed)
97
+ eventbus.add_listener(AppEvents.LANGUAGE_CHANGED, self.on_language_selected)
98
+ eventbus.add_listener(AppEvents.BGE_AI_VERSION_CHANGED, self.on_bge_ai_version_changed)
99
+ eventbus.add_listener(AppEvents.DECONVOLUTION_OBJECT_AI_VERSION_CHANGED, self.on_deconvolution_object_ai_version_changed)
100
+ eventbus.add_listener(AppEvents.DECONVOLUTION_STARS_AI_VERSION_CHANGED, self.on_deconvolution_stars_ai_version_changed)
101
+ eventbus.add_listener(AppEvents.DENOISE_AI_VERSION_CHANGED, self.on_denoise_ai_version_changed)
102
+ eventbus.add_listener(AppEvents.SCALING_CHANGED, self.on_scaling_changed)
103
+ eventbus.add_listener(AppEvents.AI_BATCH_SIZE_CHANGED, self.on_ai_batch_size_changed)
104
+ eventbus.add_listener(AppEvents.AI_GPU_ACCELERATION_CHANGED, self.on_ai_gpu_acceleration_changed)
105
+
106
+ # event handling
107
+ def on_ai_batch_size_changed(self, event):
108
+ self.prefs.ai_batch_size = event["ai_batch_size"]
109
+
110
+ def on_ai_gpu_acceleration_changed(self, event):
111
+ self.prefs.ai_gpu_acceleration = event["ai_gpu_acceleration"]
112
+
113
+ def on_bge_ai_version_changed(self, event):
114
+ self.prefs.bge_ai_version = event["bge_ai_version"]
115
+
116
+ def on_bg_floot_selection_changed(self, event):
117
+ self.prefs.bg_flood_selection_option = event["bg_flood_selection_option"]
118
+
119
+ def on_bg_pts_changed(self, event):
120
+ self.prefs.bg_pts_option = event["bg_pts_option"]
121
+
122
+ def on_bg_tol_changed(self, event):
123
+ self.prefs.bg_tol_option = event["bg_tol_option"]
124
+
125
+ def on_calculate_request(self, event=None):
126
+ if self.images.get(ImageTypes.Original) is None:
127
+ messagebox.showerror("Error", _("Please load your picture first."))
128
+ return
129
+
130
+ background_points = self.cmd.app_state.background_points
131
+
132
+ # Error messages if not enough points
133
+ if len(background_points) == 0 and self.prefs.interpol_type_option != "AI":
134
+ messagebox.showerror("Error", _("Please select background points with left click."))
135
+ return
136
+
137
+ if len(background_points) < 2 and self.prefs.interpol_type_option == "Kriging":
138
+ messagebox.showerror("Error", _("Please select at least 2 background points with left click for the Kriging method."))
139
+ return
140
+
141
+ if len(background_points) < 16 and self.prefs.interpol_type_option == "Splines":
142
+ messagebox.showerror("Error", _("Please select at least 16 background points with left click for the Splines method."))
143
+ return
144
+
145
+ if self.prefs.interpol_type_option == "AI":
146
+ if not self.validate_bge_ai_installation():
147
+ return
148
+
149
+ eventbus.emit(AppEvents.CALCULATE_BEGIN)
150
+
151
+ progress = DynamicProgressThread(callback=lambda p: eventbus.emit(AppEvents.CALCULATE_PROGRESS, {"progress": p}))
152
+
153
+ downscale_factor = 1
154
+
155
+ if self.prefs.interpol_type_option == "Kriging" or self.prefs.interpol_type_option == "RBF":
156
+ downscale_factor = 4
157
+
158
+ try:
159
+ self.prefs.images_linked_option = False
160
+
161
+ img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Original).img_array)
162
+
163
+ background = AstroImage()
164
+ background.set_from_array(
165
+ extract_background(
166
+ img_array_to_be_processed,
167
+ np.array(background_points),
168
+ self.prefs.interpol_type_option,
169
+ self.prefs.smoothing_option,
170
+ downscale_factor,
171
+ self.prefs.sample_size,
172
+ self.prefs.RBF_kernel,
173
+ self.prefs.spline_order,
174
+ self.prefs.corr_type,
175
+ ai_model_path_from_version(bge_ai_models_dir, self.prefs.bge_ai_version),
176
+ progress,
177
+ self.prefs.ai_gpu_acceleration,
178
+ )
179
+ )
180
+
181
+ gradient_corrected = AstroImage()
182
+ gradient_corrected.set_from_array(img_array_to_be_processed)
183
+
184
+ # Update fits header and metadata
185
+ background_mean = np.mean(background.img_array)
186
+ gradient_corrected.update_fits_header(self.images.get(ImageTypes.Original).fits_header, background_mean, self.prefs, self.cmd.app_state)
187
+ gradient_corrected.update_fits_header(self.images.get(ImageTypes.Original).fits_header, background_mean, self.prefs, self.cmd.app_state)
188
+
189
+ gradient_corrected.copy_metadata(self.images.get(ImageTypes.Original))
190
+ background.copy_metadata(self.images.get(ImageTypes.Original))
191
+
192
+ self.images.set(ImageTypes.Gradient_Corrected, gradient_corrected)
193
+ self.images.set(ImageTypes.Background, background)
194
+
195
+ self.images.stretch_all(StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option), self.prefs.saturation)
196
+
197
+ eventbus.emit(AppEvents.CALCULATE_SUCCESS)
198
+ eventbus.emit(AppEvents.UPDATE_DISPLAY_TYPE_REEQUEST, {"display_type": "Gradient-Corrected"})
199
+
200
+ except Exception as e:
201
+ logging.exception(e)
202
+ eventbus.emit(AppEvents.CALCULATE_ERROR)
203
+ messagebox.showerror("Error", _("An error occured during background calculation. Please see the log at {}.".format(logfile_name)))
204
+ finally:
205
+ progress.done_progress()
206
+ eventbus.emit(AppEvents.CALCULATE_END)
207
+
208
+ def on_change_saturation_request(self, event):
209
+ if self.images.get(ImageTypes.Original) is None:
210
+ return
211
+
212
+ self.prefs.saturation = event["saturation"]
213
+
214
+ eventbus.emit(AppEvents.CHANGE_SATURATION_BEGIN)
215
+
216
+ self.images.update_saturation(self.prefs.saturation)
217
+
218
+ eventbus.emit(AppEvents.CHANGE_SATURATION_END)
219
+
220
+ def on_correction_type_changed(self, event):
221
+ self.prefs.corr_type = event["corr_type"]
222
+
223
+ def on_create_grid_request(self, event=None):
224
+ if self.images.get(ImageTypes.Original) is None:
225
+ messagebox.showerror("Error", _("Please load your picture first."))
226
+ return
227
+
228
+ eventbus.emit(AppEvents.CREATE_GRID_BEGIN)
229
+
230
+ self.cmd = Command(
231
+ SEL_POINTS_HANDLER, self.cmd, data=self.images.get(ImageTypes.Original).img_array, num_pts=self.prefs.bg_pts_option, tol=self.prefs.bg_tol_option, sample_size=self.prefs.sample_size
232
+ )
233
+ self.cmd.execute()
234
+
235
+ eventbus.emit(AppEvents.CREATE_GRID_END)
236
+
237
+ def on_deconvolution_type_changed(self, event):
238
+ self.prefs.deconvolution_type_option = event["deconvolution_type_option"]
239
+
240
+ def on_deconvolution_strength_changed(self, event):
241
+ self.prefs.deconvolution_strength = event["deconvolution_strength"]
242
+
243
+ def on_deconvolution_psfsize_changed(self, event):
244
+ self.prefs.deconvolution_psfsize = event["deconvolution_psfsize"]
245
+
246
+ def on_deconvolution_object_ai_version_changed(self, event):
247
+ self.prefs.deconvolution_object_ai_version = event["deconvolution_object_ai_version"]
248
+
249
+ def on_deconvolution_stars_ai_version_changed(self, event):
250
+ self.prefs.deconvolution_stars_ai_version = event["deconvolution_stars_ai_version"]
251
+
252
+ def on_deconvolution_request(self, event):
253
+ if self.images.get(ImageTypes.Original) is None:
254
+ messagebox.showerror("Error", _("Please load your picture first."))
255
+ return
256
+
257
+ if not self.validate_deconvolution_ai_installation():
258
+ return
259
+
260
+ eventbus.emit(AppEvents.DECONVOLUTION_BEGIN)
261
+
262
+ progress = DynamicProgressThread(callback=lambda p: eventbus.emit(AppEvents.DECONVOLUTION_PROGRESS, {"progress": p}))
263
+
264
+ deconvolution_type_option = self.prefs.deconvolution_type_option
265
+
266
+ try:
267
+ img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Original).img_array)
268
+ if self.images.get(ImageTypes.Gradient_Corrected) is not None:
269
+ img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Gradient_Corrected).img_array)
270
+
271
+ self.prefs.images_linked_option = True
272
+
273
+ if deconvolution_type_option == "Object-only":
274
+ ai_model_path = ai_model_path_from_version(deconvolution_object_ai_models_dir, self.prefs.deconvolution_object_ai_version)
275
+ else:
276
+ ai_model_path = ai_model_path_from_version(deconvolution_stars_ai_models_dir, self.prefs.deconvolution_stars_ai_version)
277
+ imarray = deconvolve(
278
+ img_array_to_be_processed,
279
+ ai_model_path,
280
+ self.prefs.deconvolution_strength,
281
+ self.prefs.deconvolution_psfsize,
282
+ batch_size=self.prefs.ai_batch_size,
283
+ progress=progress,
284
+ ai_gpu_acceleration=self.prefs.ai_gpu_acceleration,
285
+ )
286
+
287
+ if imarray is not None:
288
+
289
+ deconvolved = AstroImage()
290
+ deconvolved.set_from_array(imarray)
291
+
292
+ # Update fits header and metadata
293
+ background_mean = np.mean(self.images.get(ImageTypes.Original).img_array)
294
+ deconvolved.update_fits_header(self.images.get(ImageTypes.Original).fits_header, background_mean, self.prefs, self.cmd.app_state)
295
+
296
+ deconvolved.copy_metadata(self.images.get(ImageTypes.Original))
297
+
298
+ self.images.set(f"Deconvolved {deconvolution_type_option}", deconvolved)
299
+
300
+ self.images.stretch_all(StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option, self.prefs.images_linked_option), self.prefs.saturation)
301
+
302
+ eventbus.emit(AppEvents.DECONVOLUTION_SUCCESS, {"deconvolution_type_option": f"Deconvolved {deconvolution_type_option}"})
303
+ eventbus.emit(AppEvents.UPDATE_DISPLAY_TYPE_REEQUEST, {"display_type": f"Deconvolved {deconvolution_type_option}"})
304
+
305
+ except Exception as e:
306
+ logging.exception(e)
307
+ eventbus.emit(AppEvents.DECONVOLUTION_ERROR)
308
+ messagebox.showerror("Error", _("An error occured during deconvolution. Please see the log at {}.".format(logfile_name)))
309
+ finally:
310
+ progress.done_progress()
311
+ eventbus.emit(AppEvents.DECONVOLUTION_END)
312
+
313
+ def on_denoise_ai_version_changed(self, event):
314
+ self.prefs.denoise_ai_version = event["denoise_ai_version"]
315
+
316
+ def on_display_pts_changed(self, event):
317
+ self.prefs.display_pts = event["display_pts"]
318
+ eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST)
319
+
320
+ def on_display_type_changed(self, event):
321
+ self.display_type = event["display_type"]
322
+
323
+ eventbus.emit(AppEvents.STRETCH_IMAGE_END)
324
+
325
+ def on_interpol_type_changed(self, event):
326
+ self.prefs.interpol_type_option = event["interpol_type_option"]
327
+
328
+ def on_language_selected(self, event):
329
+ self.prefs.lang = event["lang"]
330
+ messagebox.showerror("", _("Please restart the program to change the language."))
331
+
332
+ def on_load_image(self, event):
333
+ eventbus.emit(AppEvents.LOAD_IMAGE_BEGIN)
334
+ filename = event["filename"]
335
+ self.display_type = ImageTypes.Original
336
+
337
+ try:
338
+ image = AstroImage()
339
+ image.set_from_file(filename, StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option), self.prefs.saturation)
340
+
341
+ except Exception as e:
342
+ eventbus.emit(AppEvents.LOAD_IMAGE_ERROR)
343
+ msg = _("An error occurred while loading your picture.")
344
+ logging.exception(msg)
345
+ messagebox.showerror("Error", _(msg))
346
+ return
347
+
348
+ self.filename = os.path.splitext(os.path.basename(filename))[0]
349
+
350
+ self.data_type = os.path.splitext(filename)[1]
351
+ self.images.reset()
352
+ self.images.set(ImageTypes.Original, image)
353
+ self.prefs.working_dir = os.path.dirname(filename)
354
+
355
+ os.chdir(os.path.dirname(filename))
356
+
357
+ width = self.images.get(ImageTypes.Original).img_display.width
358
+ height = self.images.get(ImageTypes.Original).img_display.height
359
+
360
+ if self.prefs.width != width or self.prefs.height != height:
361
+ self.reset_backgroundpts()
362
+
363
+ self.prefs.width = width
364
+ self.prefs.height = height
365
+
366
+ tmp_state = fitsheader_2_app_state(self, self.cmd.app_state, self.images.get(ImageTypes.Original).fits_header)
367
+ self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state.background_points)
368
+ self.cmd.execute()
369
+
370
+ eventbus.emit(AppEvents.LOAD_IMAGE_END, {"filename": filename})
371
+
372
+ def on_open_file_dialog_request(self, evet):
373
+ if self.prefs.working_dir != "" and os.path.exists(self.prefs.working_dir):
374
+ initialdir = self.prefs.working_dir
375
+ else:
376
+ initialdir = os.getcwd()
377
+
378
+ filename = tk.filedialog.askopenfilename(
379
+ filetypes=[
380
+ ("Image file", ".bmp .png .jpg .jpeg .tif .tiff .fit .fits .fts .xisf"),
381
+ ("Bitmap", ".bmp"),
382
+ ("PNG", ".png"),
383
+ ("JPEG", ".jpg .jpeg"),
384
+ ("Tiff", ".tif .tiff"),
385
+ ("Fits", ".fit .fits .fts"),
386
+ ("XISF", ".xisf"),
387
+ ],
388
+ initialdir=initialdir,
389
+ )
390
+
391
+ if filename == "":
392
+ return
393
+
394
+ eventbus.emit(AppEvents.LOAD_IMAGE_REQUEST, {"filename": filename})
395
+
396
+ def on_rbf_kernel_changed(self, event):
397
+ self.prefs.RBF_kernel = event["RBF_kernel"]
398
+
399
+ def on_reset_points_request(self, event):
400
+ eventbus.emit(AppEvents.RESET_POITS_BEGIN)
401
+
402
+ if len(self.cmd.app_state.background_points) > 0:
403
+ self.cmd = Command(RESET_POINTS_HANDLER, self.cmd)
404
+ self.cmd.execute()
405
+
406
+ eventbus.emit(AppEvents.RESET_POITS_END)
407
+
408
+ def on_sample_color_changed(self, event):
409
+ self.prefs.sample_color = event["sample_color"]
410
+ eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST)
411
+
412
+ def on_sample_size_changed(self, event):
413
+ self.prefs.sample_size = event["sample_size"]
414
+ eventbus.emit(AppEvents.REDRAW_POINTS_REQUEST)
415
+
416
+ def on_save_as_changed(self, event):
417
+ self.prefs.saveas_option = event["saveas_option"]
418
+
419
+ def on_save_stretched_changed(self, event):
420
+ self.prefs.saveas_stretched = event["saveas_stretched"]
421
+
422
+ def on_smoothing_changed(self, event):
423
+ self.prefs.smoothing_option = event["smoothing_option"]
424
+
425
+ def on_denoise_strength_changed(self, event):
426
+ self.prefs.denoise_strength = event["denoise_strength"]
427
+
428
+ def on_denoise_request(self, event):
429
+ if self.images.get(ImageTypes.Original) is None:
430
+ messagebox.showerror("Error", _("Please load your picture first."))
431
+ return
432
+
433
+ if not self.validate_denoise_ai_installation():
434
+ return
435
+
436
+ eventbus.emit(AppEvents.DENOISE_BEGIN)
437
+
438
+ progress = DynamicProgressThread(callback=lambda p: eventbus.emit(AppEvents.DENOISE_PROGRESS, {"progress": p}))
439
+
440
+ try:
441
+
442
+ if self.images.get(ImageTypes.Deconvolved_Object_only) is not None:
443
+ img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Deconvolved_Object_only).img_array)
444
+ elif self.images.get(ImageTypes.Gradient_Corrected) is not None:
445
+ img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Gradient_Corrected).img_array)
446
+ else:
447
+ img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Original).img_array)
448
+
449
+ self.prefs.images_linked_option = True
450
+ ai_model_path = ai_model_path_from_version(denoise_ai_models_dir, self.prefs.denoise_ai_version)
451
+ imarray = denoise(
452
+ img_array_to_be_processed,
453
+ ai_model_path,
454
+ self.prefs.denoise_strength,
455
+ batch_size=self.prefs.ai_batch_size,
456
+ progress=progress,
457
+ ai_gpu_acceleration=self.prefs.ai_gpu_acceleration,
458
+ )
459
+
460
+ if imarray is not None:
461
+
462
+ denoised = AstroImage()
463
+ denoised.set_from_array(imarray)
464
+
465
+ # Update fits header and metadata
466
+ background_mean = np.mean(self.images.get(ImageTypes.Original).img_array)
467
+ denoised.update_fits_header(self.images.get(ImageTypes.Original).fits_header, background_mean, self.prefs, self.cmd.app_state)
468
+
469
+ denoised.copy_metadata(self.images.get(ImageTypes.Original))
470
+
471
+ self.images.set(ImageTypes.Denoised, denoised)
472
+
473
+ self.images.stretch_all(StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option, self.prefs.images_linked_option), self.prefs.saturation)
474
+
475
+ eventbus.emit(AppEvents.DENOISE_SUCCESS)
476
+ eventbus.emit(AppEvents.UPDATE_DISPLAY_TYPE_REEQUEST, {"display_type": "Denoised"})
477
+
478
+ except Exception as e:
479
+ logging.exception(e)
480
+ eventbus.emit(AppEvents.DENOISE_ERROR)
481
+ messagebox.showerror("Error", _("An error occured during denoising. Please see the log at {}.".format(logfile_name)))
482
+ finally:
483
+ progress.done_progress()
484
+ eventbus.emit(AppEvents.DENOISE_END)
485
+
486
+ def on_save_request(self, event):
487
+
488
+ suffix_1 = "_graxpert"
489
+
490
+ match self.display_type:
491
+ case ImageTypes.Gradient_Corrected:
492
+ suffix_2 = "_bge"
493
+ case ImageTypes.Background:
494
+ suffix_2 = "_background"
495
+ case ImageTypes.Deconvolved_Object_only:
496
+ suffix_2 = "_obj_decon"
497
+ case ImageTypes.Deconvolved_Stars_only:
498
+ suffix_2 = "_stars_decon"
499
+ case ImageTypes.Denoised:
500
+ suffix_2 = "_denoised"
501
+ case _:
502
+ suffix_2 = ""
503
+
504
+ match self.prefs.saveas_stretched:
505
+ case True:
506
+ suffix_3 = "_stretched"
507
+ case _:
508
+ suffix_3 = ""
509
+
510
+ if self.prefs.saveas_option == "16 bit Tiff" or self.prefs.saveas_option == "32 bit Tiff":
511
+ dir = tk.filedialog.asksaveasfilename(
512
+ initialfile=self.filename + f"{suffix_1}{suffix_2}{suffix_3}.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs.working_dir
513
+ )
514
+ elif self.prefs.saveas_option == "16 bit XISF" or self.prefs.saveas_option == "32 bit XISF":
515
+ dir = tk.filedialog.asksaveasfilename(
516
+ initialfile=self.filename + f"{suffix_1}{suffix_2}{suffix_3}.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs.working_dir
517
+ )
518
+ else:
519
+ dir = tk.filedialog.asksaveasfilename(
520
+ initialfile=self.filename + f"{suffix_1}{suffix_2}{suffix_3}.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=self.prefs.working_dir
521
+ )
522
+
523
+ if dir == "":
524
+ return
525
+
526
+ eventbus.emit(AppEvents.SAVE_BEGIN)
527
+
528
+ try:
529
+ if self.prefs.saveas_stretched:
530
+ self.images.get(self.display_type).save_stretched(dir, self.prefs.saveas_option, StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option))
531
+ else:
532
+ self.images.get(self.display_type).save(dir, self.prefs.saveas_option)
533
+
534
+ except Exception as e:
535
+ logging.exception(e)
536
+ eventbus.emit(AppEvents.SAVE_ERROR)
537
+ messagebox.showerror("Error", _("Error occured when saving the image."))
538
+
539
+ eventbus.emit(AppEvents.SAVE_END)
540
+
541
+ def on_scaling_changed(self, event):
542
+ self.prefs.scaling = event["scaling"]
543
+
544
+ def on_spline_order_changed(self, event):
545
+ self.prefs.spline_order = event["spline_order"]
546
+
547
+ def on_stretch_option_changed(self, event):
548
+ self.prefs.stretch_option = event["stretch_option"]
549
+ self.do_stretch()
550
+
551
+ def on_channels_linked_option_changed(self, event):
552
+ self.prefs.channels_linked_option = event["channels_linked"]
553
+ self.do_stretch()
554
+
555
+ # application logic
556
+ def do_stretch(self):
557
+ eventbus.emit(AppEvents.STRETCH_IMAGE_BEGIN)
558
+
559
+ try:
560
+ self.images.stretch_all(StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option, self.prefs.images_linked_option), self.prefs.saturation)
561
+ except Exception as e:
562
+ eventbus.emit(AppEvents.STRETCH_IMAGE_ERROR)
563
+ logging.exception(e)
564
+
565
+ eventbus.emit(AppEvents.STRETCH_IMAGE_END)
566
+
567
+ def remove_pt(self, event):
568
+ if len(self.cmd.app_state.background_points) == 0 or not self.prefs.display_pts:
569
+ return False
570
+
571
+ point_im = self.to_image_point(event.x, event.y)
572
+ if len(point_im) == 0:
573
+ return False
574
+
575
+ eventx_im = point_im[0]
576
+ eventy_im = point_im[1]
577
+
578
+ background_points = self.cmd.app_state.background_points
579
+
580
+ min_idx = -1
581
+ min_dist = -1
582
+
583
+ for i in range(len(background_points)):
584
+ x_im = background_points[i][0]
585
+ y_im = background_points[i][1]
586
+
587
+ dist = np.max(np.abs([x_im - eventx_im, y_im - eventy_im]))
588
+
589
+ if min_idx == -1 or dist < min_dist:
590
+ min_dist = dist
591
+ min_idx = i
592
+
593
+ if min_idx != -1 and min_dist <= self.prefs.sample_size:
594
+ point = background_points[min_idx]
595
+ self.cmd = Command(RM_POINT_HANDLER, self.cmd, idx=min_idx, point=point)
596
+ self.cmd.execute()
597
+ return True
598
+ else:
599
+ return False
600
+
601
+ def reset_backgroundpts(self):
602
+ if len(self.cmd.app_state.background_points) > 0:
603
+ self.cmd = Command(RESET_POINTS_HANDLER, self.cmd)
604
+ self.cmd.execute()
605
+
606
+ def reset_transform(self):
607
+ self.mat_affine = np.eye(3)
608
+
609
+ def scale_at(self, scale: float, cx: float, cy: float):
610
+ self.translate(-cx, -cy)
611
+ self.scale(scale)
612
+ self.translate(cx, cy)
613
+
614
+ def scale(self, scale: float):
615
+ mat = np.eye(3)
616
+ mat[0, 0] = scale
617
+ mat[1, 1] = scale
618
+ self.mat_affine = np.dot(mat, self.mat_affine)
619
+
620
+ def to_canvas_point(self, x, y):
621
+ return np.dot(self.mat_affine, (x, y, 1.0))
622
+
623
+ def to_image_point(self, x, y):
624
+ if self.images.get(self.display_type) is None:
625
+ return []
626
+
627
+ mat_inv = np.linalg.inv(self.mat_affine)
628
+ image_point = np.dot(mat_inv, (x, y, 1.0))
629
+
630
+ width = self.images.get(self.display_type).width
631
+ height = self.images.get(self.display_type).height
632
+
633
+ if image_point[0] < 0 or image_point[1] < 0 or image_point[0] > width or image_point[1] > height:
634
+ return []
635
+
636
+ return image_point
637
+
638
+ def to_image_point_pinned(self, x, y):
639
+ if self.images.get(self.display_type) is None:
640
+ return []
641
+
642
+ mat_inv = np.linalg.inv(self.mat_affine)
643
+ image_point = np.dot(mat_inv, (x, y, 1.0))
644
+
645
+ width = self.images.get(self.display_type).width
646
+ height = self.images.get(self.display_type).height
647
+
648
+ if image_point[0] < 0:
649
+ image_point[0] = 0
650
+ if image_point[1] < 0:
651
+ image_point[1] = 0
652
+ if image_point[0] > width:
653
+ image_point[0] = width
654
+ if image_point[1] > height:
655
+ image_point[1] = height
656
+
657
+ return image_point
658
+
659
+ def translate(self, offset_x, offset_y):
660
+ mat = np.eye(3)
661
+ mat[0, 2] = float(offset_x)
662
+ mat[1, 2] = float(offset_y)
663
+
664
+ self.mat_affine = np.dot(mat, self.mat_affine)
665
+
666
+ def validate_bge_ai_installation(self):
667
+ if self.prefs.bge_ai_version is None or self.prefs.bge_ai_version == "None":
668
+ messagebox.showerror("Error", _("No Background Extraction AI-Model selected. Please select one from the Advanced panel on the right."))
669
+ return False
670
+
671
+ if not validate_local_version(bge_ai_models_dir, self.prefs.bge_ai_version):
672
+ if not messagebox.askyesno(_("Install AI-Model?"), _("Selected Background Extraction AI-Model is not installed. Should I download it now?")):
673
+ return False
674
+ else:
675
+ eventbus.emit(AppEvents.AI_DOWNLOAD_BEGIN)
676
+
677
+ def callback(p):
678
+ eventbus.emit(AppEvents.AI_DOWNLOAD_PROGRESS, {"progress": p})
679
+
680
+ download_version(bge_ai_models_dir, bge_bucket_name, self.prefs.bge_ai_version, progress=callback)
681
+ eventbus.emit(AppEvents.AI_DOWNLOAD_END)
682
+ return True
683
+
684
+ def validate_deconvolution_ai_installation(self):
685
+
686
+ if self.prefs.deconvolution_type_option == "Object-only":
687
+
688
+ if self.prefs.deconvolution_object_ai_version is None or self.prefs.deconvolution_object_ai_version == "None":
689
+ messagebox.showerror("Error", _("No Object-only Deconvolution AI-Model selected. Please select one from the Advanced panel on the right."))
690
+ return False
691
+
692
+ if not validate_local_version(deconvolution_object_ai_models_dir, self.prefs.deconvolution_object_ai_version):
693
+ if not messagebox.askyesno(_("Install AI-Model?"), _("Selected Object-only Deconvolution AI-Model is not installed. Should I download it now?")):
694
+ return False
695
+ else:
696
+ eventbus.emit(AppEvents.AI_DOWNLOAD_BEGIN)
697
+
698
+ def callback(p):
699
+ eventbus.emit(AppEvents.AI_DOWNLOAD_PROGRESS, {"progress": p})
700
+
701
+ download_version(deconvolution_object_ai_models_dir, deconvolution_object_bucket_name, self.prefs.deconvolution_object_ai_version, progress=callback)
702
+ eventbus.emit(AppEvents.AI_DOWNLOAD_END)
703
+ return True
704
+ else:
705
+ if self.prefs.deconvolution_stars_ai_version is None or self.prefs.deconvolution_stars_ai_version == "None":
706
+ messagebox.showerror("Error", _("No Stars-only Denoising AI-Model selected. Please select one from the Advanced panel on the right."))
707
+ return False
708
+
709
+ if not validate_local_version(deconvolution_stars_ai_models_dir, self.prefs.deconvolution_stars_ai_version):
710
+ if not messagebox.askyesno(_("Install AI-Model?"), _("Selected Stars-only Deconvolution AI-Model is not installed. Should I download it now?")):
711
+ return False
712
+ else:
713
+ eventbus.emit(AppEvents.AI_DOWNLOAD_BEGIN)
714
+
715
+ def callback(p):
716
+ eventbus.emit(AppEvents.AI_DOWNLOAD_PROGRESS, {"progress": p})
717
+
718
+ download_version(deconvolution_stars_ai_models_dir, deconvolution_stars_bucket_name, self.prefs.deconvolution_stars_ai_version, progress=callback)
719
+ eventbus.emit(AppEvents.AI_DOWNLOAD_END)
720
+ return True
721
+
722
+ def validate_denoise_ai_installation(self):
723
+ if self.prefs.denoise_ai_version is None or self.prefs.denoise_ai_version == "None":
724
+ messagebox.showerror("Error", _("No Denoising AI-Model selected. Please select one from the Advanced panel on the right."))
725
+ return False
726
+
727
+ if not validate_local_version(denoise_ai_models_dir, self.prefs.denoise_ai_version):
728
+ if not messagebox.askyesno(_("Install AI-Model?"), _("Selected Denoising AI-Model is not installed. Should I download it now?")):
729
+ return False
730
+ else:
731
+ eventbus.emit(AppEvents.AI_DOWNLOAD_BEGIN)
732
+
733
+ def callback(p):
734
+ eventbus.emit(AppEvents.AI_DOWNLOAD_PROGRESS, {"progress": p})
735
+
736
+ download_version(denoise_ai_models_dir, denoise_bucket_name, self.prefs.denoise_ai_version, progress=callback)
737
+ eventbus.emit(AppEvents.AI_DOWNLOAD_END)
738
+ return True
739
+
740
+
741
+ graxpert = GraXpert()