diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 22a422b..c56c111 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -7,6 +7,8 @@ env: ai_s3_bucket_name: ${{ secrets.AI_S3_BUCKET_NAME }} ai_s3_bge_bucket_name: ${{ secrets.AI_S3_BGE_BUCKET_NAME }} ai_s3_denoise_bucket_name: ${{ secrets.AI_S3_DENOISE_BUCKET_NAME }} + ai_s3_deconvolution_object_bucket_name: ${{ secrets.AI_S3_DECONVOLUTION_OBJECT_BUCKET_NAME }} + ai_s3_deconvolution_stars_bucket_name: ${{ secrets.AI_S3_DECONVOLUTION_STARS_BUCKET_NAME }} on: push: @@ -14,14 +16,14 @@ on: - "*.*.*" jobs: - + # build-linux-deb: # runs-on: ubuntu-20.04 # steps: # - name: setup python - # uses: actions/setup-python@v2 + # uses: actions/setup-python@v5 # with: - # python-version: '3.10' + # python-version: '3.11' # - name: checkout repository # uses: actions/checkout@v3 # - name: configure ai s3 secrets @@ -45,19 +47,19 @@ jobs: # - name: create GraXpert-linux bundle # run: python ./setup.py bdist_deb # - name: store artifacts - # uses: actions/upload-artifact@v2 + # uses: actions/upload-artifact@v4 # with: # name: graxpert_${{github.ref_name}}-1_amd64.deb # path: ./dist/graxpert_${{github.ref_name}}-1_amd64.deb # retention-days: 5 - + build-linux-zip: runs-on: ubuntu-20.04 steps: - name: setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.11' - name: checkout repository uses: actions/checkout@v3 - name: configure ai s3 secrets @@ -67,11 +69,13 @@ jobs: echo "ro_secret_key = \"$ai_s3_secret_key\"" >> ./graxpert/s3_secrets.py && \ echo "bucket_name = \"$ai_s3_bucket_name\"" >> ./graxpert/s3_secrets.py && \ echo "bge_bucket_name = \"$ai_s3_bge_bucket_name\"" >> ./graxpert/s3_secrets.py && \ - echo "denoise_bucket_name = \"$ai_s3_denoise_bucket_name\"" >> ./graxpert/s3_secrets.py + echo "denoise_bucket_name = \"$ai_s3_denoise_bucket_name\"" >> ./graxpert/s3_secrets.py && \ + echo "deconvolution_object_bucket_name = \"$ai_s3_deconvolution_object_bucket_name\"" >> ./graxpert/s3_secrets.py && \ + echo "deconvolution_stars_bucket_name = \"$ai_s3_deconvolution_stars_bucket_name\"" >> ./graxpert/s3_secrets.py - name: install dependencies run: | - pip install setuptools wheel cx_freeze && \ - pip install onnxruntime-gpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/ && \ + pip install setuptools wheel cx_freeze==7.2.3 && \ + pip install onnxruntime-gpu && \ pip install -r requirements.txt - name: patch version run: | @@ -84,7 +88,7 @@ jobs: cd ./dist && \ zip -r ./GraXpert-linux.zip ./GraXpert-linux - name: store artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: GraXpert-linux.zip path: ./dist/GraXpert-linux.zip @@ -94,9 +98,9 @@ jobs: runs-on: windows-latest steps: - name: setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.11' - name: checkout repository uses: actions/checkout@v3 - name: configure ai s3 secrets @@ -107,10 +111,12 @@ jobs: "ro_secret_key = `"$env:ai_s3_secret_key`"" | Out-File -Append .\graxpert\s3_secrets.py ; ` "bucket_name = `"$env:ai_s3_bucket_name`"" | Out-File -Append .\graxpert\s3_secrets.py ; ` "bge_bucket_name = `"$env:ai_s3_bge_bucket_name`"" | Out-File -Append .\graxpert\s3_secrets.py ; ` - "denoise_bucket_name = `"$env:ai_s3_denoise_bucket_name`"" | Out-File -Append .\graxpert\s3_secrets.py + "denoise_bucket_name = `"$env:ai_s3_denoise_bucket_name`"" | Out-File -Append .\graxpert\s3_secrets.py ; ` + "deconvolution_object_bucket_name = `"$env:ai_s3_deconvolution_object_bucket_name`"" | Out-File -Append .\graxpert\s3_secrets.py ; ` + "deconvolution_stars_bucket_name = `"$env:ai_s3_deconvolution_stars_bucket_name`"" | Out-File -Append .\graxpert\s3_secrets.py - name: install dependencies run: | - pip install setuptools wheel cx_freeze; ` + pip install setuptools wheel cx_freeze==7.2.3; ` pip install onnxruntime-directml; ` pip install -r requirements.txt - name: patch version @@ -118,14 +124,68 @@ jobs: - name: create GraXpert-win64 bundle run: python ./setup.py bdist_msi - name: store artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: GraXpert-${{github.ref_name}}-win64.msi path: ./dist/GraXpert-${{github.ref_name}}-win64.msi retention-days: 5 + build-macos-arm64: + runs-on: macos-14 + steps: + - name: checkout repository + uses: actions/checkout@v3 + - name: configure ai s3 secrets + run: | + echo "endpoint = \"$ai_s3_endpoint\"" >> ./graxpert/s3_secrets.py && \ + echo "ro_access_key = \"$ai_s3_access_key\"" >> ./graxpert/s3_secrets.py && \ + echo "ro_secret_key = \"$ai_s3_secret_key\"" >> ./graxpert/s3_secrets.py && \ + echo "bucket_name = \"$ai_s3_bucket_name\"" >> ./graxpert/s3_secrets.py && \ + echo "bge_bucket_name = \"$ai_s3_bge_bucket_name\"" >> ./graxpert/s3_secrets.py && \ + echo "denoise_bucket_name = \"$ai_s3_denoise_bucket_name\"" >> ./graxpert/s3_secrets.py && \ + echo "deconvolution_object_bucket_name = \"$ai_s3_deconvolution_object_bucket_name\"" >> ./graxpert/s3_secrets.py && \ + echo "deconvolution_stars_bucket_name = \"$ai_s3_deconvolution_stars_bucket_name\"" >> ./graxpert/s3_secrets.py + - name: setup python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: install dependencies + run: | + brew install python-tk && \ + pip3 install setuptools wheel pyinstaller --break-system-packages && \ + pip3 install onnxruntime --break-system-packages && \ + pip3 install -r requirements.txt --break-system-packages + - name: patch version + run: | + chmod u+x ./releng/patch_version.sh && \ + ./releng/patch_version.sh + - name: create GraXpert-macos-arm64 bundle + # TODO migrate to cx_freeze + run: | + pyinstaller \ + ./GraXpert-macos-arm64.spec + - name: install create-dmg + run: brew install create-dmg + - name: create .dmg + run: | + create-dmg \ + --volname "GraXpert-macos-arm64" \ + --window-pos 50 50 \ + --window-size 1920 1080 \ + --icon-size 100 \ + --icon "GraXpert.app" 200 190 \ + --app-drop-link 400 190 \ + "dist/GraXpert-macos-arm64.dmg" \ + "dist/GraXpert.app/" + - name: store artifacts + uses: actions/upload-artifact@v4 + with: + name: GraXpert-macos-arm64.dmg + path: ./dist/GraXpert-macos-arm64.dmg + retention-days: 5 + build-macos-x86_64: - runs-on: macos-11 + runs-on: macos-13 steps: - name: checkout repository uses: actions/checkout@v3 @@ -136,25 +196,31 @@ jobs: echo "ro_secret_key = \"$ai_s3_secret_key\"" >> ./graxpert/s3_secrets.py && \ echo "bucket_name = \"$ai_s3_bucket_name\"" >> ./graxpert/s3_secrets.py && \ echo "bge_bucket_name = \"$ai_s3_bge_bucket_name\"" >> ./graxpert/s3_secrets.py && \ - echo "denoise_bucket_name = \"$ai_s3_denoise_bucket_name\"" >> ./graxpert/s3_secrets.py + echo "denoise_bucket_name = \"$ai_s3_denoise_bucket_name\"" >> ./graxpert/s3_secrets.py && \ + echo "deconvolution_object_bucket_name = \"$ai_s3_deconvolution_object_bucket_name\"" >> ./graxpert/s3_secrets.py && \ + echo "deconvolution_stars_bucket_name = \"$ai_s3_deconvolution_stars_bucket_name\"" >> ./graxpert/s3_secrets.py # github actions overwrites brew's python. Force it to reassert itself, by running in a separate step. - - name: unbreak python in github actions - run: | - find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete - sudo rm -rf /Library/Frameworks/Python.framework/ - brew install --force python3 && brew unlink python3 && brew unlink python3 && brew link --overwrite python3 + # - name: unbreak python in github actions + # run: | + # find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete + # sudo rm -rf /Library/Frameworks/Python.framework/ + # brew install --force python3 && brew unlink python3 && brew unlink python3 && brew link --overwrite python3 + - name: setup python + uses: actions/setup-python@v5 + with: + python-version: '3.11' - name: install dependencies run: | brew install python-tk && \ - pip3 install setuptools wheel pyinstaller && \ - pip3 install onnxruntime && \ - pip3 install -r requirements.txt + pip3 install setuptools wheel pyinstaller --break-system-packages && \ + pip3 install onnxruntime --break-system-packages && \ + pip3 install -r requirements.txt --break-system-packages - name: patch version run: | chmod u+x ./releng/patch_version.sh && \ ./releng/patch_version.sh - name: create GraXpert-macos-x86_64 bundle - # TODO migrato to cx_freeze + # TODO migrate to cx_freeze run: | pyinstaller \ ./GraXpert-macos-x86_64.spec @@ -172,7 +238,7 @@ jobs: "dist/GraXpert-macos-x86_64.dmg" \ "dist/GraXpert.app/" - name: store artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: GraXpert-macos-x86_64.dmg path: ./dist/GraXpert-macos-x86_64.dmg @@ -181,29 +247,34 @@ jobs: release: runs-on: ubuntu-latest # needs: [build-linux-deb, build-linux-zip, build-windows, build-macos-x86_64] - needs: [build-linux-zip, build-windows, build-macos-x86_64] + needs: [build-linux-zip, build-windows, build-macos-x86_64, build-macos-arm64] steps: # - name: download linux deb - # uses: actions/download-artifact@v2 + # uses: actions/download-artifact@v4 # with: # name: graxpert_${{github.ref_name}}-1_amd64.deb - name: download linux zip - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: GraXpert-linux.zip - name: download windows exe - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: GraXpert-${{github.ref_name}}-win64.msi - - name: download macos artifacts - uses: actions/download-artifact@v2 + - name: download macos x86_64 artifacts + uses: actions/download-artifact@v4 with: name: GraXpert-macos-x86_64.dmg + - name: download macos arm artifacts + uses: actions/download-artifact@v4 + with: + name: GraXpert-macos-arm64.dmg - name: create release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: | GraXpert-linux.zip GraXpert-${{github.ref_name}}-win64.msi GraXpert-macos-x86_64.dmg - # graxpert_${{github.ref_name}}-1_amd64.deb + GraXpert-macos-arm64.dmg + # graxpert_${{github.ref_name}}-1_amd64.deb diff --git a/.gitignore b/.gitignore index 430a76d..37cad96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ *-scaled.png __pycache__ graxpert/s3_secrets.py -python-venv build dist +.idea +.venv +python-venv +env_graxpert/ diff --git a/GraXpert-macos-arm64.spec b/GraXpert-macos-arm64.spec index 3abb2e0..fe6683b 100644 --- a/GraXpert-macos-arm64.spec +++ b/GraXpert-macos-arm64.spec @@ -2,12 +2,12 @@ block_cipher = None - +from PyInstaller.utils.hooks import copy_metadata a = Analysis(['./graxpert/main.py'], pathex=[], binaries=[], - datas=[('./img/*', './img/'), ('./graxpert-dark-blue.json', './')], + datas=[('./img/*', './img/'), ('./graxpert-dark-blue.json', './')] + copy_metadata('xisf'), hiddenimports=['PIL._tkinter_finder', 'tkinter'], hookspath=['./releng'], hooksconfig={}, @@ -17,7 +17,7 @@ a = Analysis(['./graxpert/main.py'], win_private_assemblies=False, cipher=block_cipher, noarchive=False) -pyz = PYZ(a.pure, a.zipped_data, +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, diff --git a/graxpert/AstroImageRepository.py b/graxpert/AstroImageRepository.py index 9c29c15..21dc39f 100644 --- a/graxpert/AstroImageRepository.py +++ b/graxpert/AstroImageRepository.py @@ -1,84 +1,114 @@ -from graxpert.astroimage import AstroImage -from graxpert.stretch import StretchParameters, stretch_all, calculate_mtf_stretch_parameters_for_image +from enum import StrEnum from typing import Dict +from graxpert.astroimage import AstroImage +from graxpert.stretch import StretchParameters, calculate_mtf_stretch_parameters_for_image, stretch_all + + +class ImageTypes(StrEnum): + Original = "Original" + Gradient_Corrected = "Gradient-Corrected" + Background = "Background" + Deconvolved_Object_only = "Deconvolved Object-only" + Deconvolved_Stars_only = "Deconvolved Stars-only" + Denoised = "Denoised" + + class AstroImageRepository: - images: Dict = {"Original": None, "Gradient-Corrected": None, "Background": None, "Denoised": None} - - def set(self, type:str, image:AstroImage): + + images: Dict = { + ImageTypes.Original: None, + ImageTypes.Gradient_Corrected: None, + ImageTypes.Background: None, + ImageTypes.Deconvolved_Object_only: None, + ImageTypes.Deconvolved_Stars_only: None, + ImageTypes.Denoised: None, + } + + def set(self, type: ImageTypes, image: AstroImage): self.images[type] = image - - def get(self, type:str): + + def get(self, type: ImageTypes): return self.images[type] - - def stretch_all(self, stretch_params:StretchParameters, saturation:float): - - if self.get("Original") is None: + + def stretch_all(self, stretch_params: StretchParameters, saturation: float): + + if self.get(ImageTypes.Original) is None: return - + stretches = [] - + if not stretch_params.do_stretch: for key, image in self.images.items(): if image is not None: stretches.append(image.img_array) - + else: - + all_image_arrays = [] all_mtf_stretch_params = [] - - all_image_arrays.append(self.get("Original").img_array) - all_mtf_stretch_params.append(calculate_mtf_stretch_parameters_for_image(stretch_params, self.get("Original").img_array)) - - if self.get("Gradient-Corrected") is not None and self.get("Background") is not None: - all_image_arrays.append(self.get("Gradient-Corrected").img_array) - all_mtf_stretch_params.append(calculate_mtf_stretch_parameters_for_image(stretch_params, self.get("Gradient-Corrected").img_array)) - - all_image_arrays.append(self.get("Background").img_array) + + all_image_arrays.append(self.get(ImageTypes.Original).img_array) + all_mtf_stretch_params.append(calculate_mtf_stretch_parameters_for_image(stretch_params, self.get(ImageTypes.Original).img_array)) + + if self.get(ImageTypes.Gradient_Corrected) is not None and self.get(ImageTypes.Background) is not None: + all_image_arrays.append(self.get(ImageTypes.Gradient_Corrected).img_array) + all_mtf_stretch_params.append(calculate_mtf_stretch_parameters_for_image(stretch_params, self.get(ImageTypes.Gradient_Corrected).img_array)) + + all_image_arrays.append(self.get(ImageTypes.Background).img_array) all_mtf_stretch_params.append(all_mtf_stretch_params[0]) - - - if self.get("Denoised") is not None and self.get("Gradient-Corrected") is None: - all_image_arrays.append(self.get("Denoised").img_array) + + if self.get(ImageTypes.Deconvolved_Object_only) is not None and self.get(ImageTypes.Gradient_Corrected) is None: + all_image_arrays.append(self.get(ImageTypes.Deconvolved_Object_only).img_array) all_mtf_stretch_params.append(all_mtf_stretch_params[0]) - - elif self.get("Denoised") is not None and self.get("Gradient-Corrected") is not None: - all_image_arrays.append(self.get("Denoised").img_array) + + elif self.get(ImageTypes.Deconvolved_Object_only) is not None and self.get(ImageTypes.Gradient_Corrected) is not None: + all_image_arrays.append(self.get(ImageTypes.Deconvolved_Object_only).img_array) all_mtf_stretch_params.append(all_mtf_stretch_params[1]) - - + + if self.get(ImageTypes.Deconvolved_Stars_only) is not None and self.get(ImageTypes.Gradient_Corrected) is None: + all_image_arrays.append(self.get(ImageTypes.Deconvolved_Stars_only).img_array) + all_mtf_stretch_params.append(all_mtf_stretch_params[0]) + + elif self.get(ImageTypes.Deconvolved_Stars_only) is not None and self.get(ImageTypes.Gradient_Corrected) is not None: + all_image_arrays.append(self.get(ImageTypes.Deconvolved_Stars_only).img_array) + all_mtf_stretch_params.append(all_mtf_stretch_params[1]) + + if self.get(ImageTypes.Denoised) is not None and self.get(ImageTypes.Gradient_Corrected) is None: + all_image_arrays.append(self.get(ImageTypes.Denoised).img_array) + all_mtf_stretch_params.append(all_mtf_stretch_params[0]) + + elif self.get(ImageTypes.Denoised) is not None and self.get(ImageTypes.Gradient_Corrected) is not None: + all_image_arrays.append(self.get(ImageTypes.Denoised).img_array) + all_mtf_stretch_params.append(all_mtf_stretch_params[1]) + stretches = stretch_all(all_image_arrays, all_mtf_stretch_params) - i = 0 for key, image in self.images.items(): if image is not None: image.update_display_from_array(stretches[i], saturation) i = i + 1 - - def crop_all(self, start_x:float, end_x:float, start_y:float, end_y:float): + + def crop_all(self, start_x: float, end_x: float, start_y: float, end_y: float): for key, astroimg in self.images.items(): if astroimg is not None: astroimg.crop(start_x, end_x, start_y, end_y) - + def update_saturation(self, saturation): for key, value in self.images.items(): - if (value is not None): + if value is not None: value.update_saturation(saturation) - + def reset(self): for key, value in self.images.items(): self.images[key] = None - + def display_options(self): display_options = [] - + for key, value in self.images.items(): - if (self.images[key] is not None): + if self.images[key] is not None: display_options.append(key) - + return display_options - - - \ No newline at end of file diff --git a/graxpert/ai_model_handling.py b/graxpert/ai_model_handling.py index c5dab1d..ea743e8 100644 --- a/graxpert/ai_model_handling.py +++ b/graxpert/ai_model_handling.py @@ -32,6 +32,10 @@ os.makedirs(bge_ai_models_dir, exist_ok=True) +deconvolution_object_ai_models_dir = os.path.join(user_data_dir(appname="GraXpert"), "deconvolution-object-ai-models") +os.makedirs(deconvolution_object_ai_models_dir, exist_ok=True) +deconvolution_stars_ai_models_dir = os.path.join(user_data_dir(appname="GraXpert"), "deconvolution-stars-ai-models") +os.makedirs(deconvolution_stars_ai_models_dir, exist_ok=True) denoise_ai_models_dir = os.path.join(user_data_dir(appname="GraXpert"), "denoise-ai-models") os.makedirs(denoise_ai_models_dir, exist_ok=True) diff --git a/graxpert/application/app.py b/graxpert/application/app.py index 9e10b61..b1dd463 100644 --- a/graxpert/application/app.py +++ b/graxpert/application/app.py @@ -6,24 +6,34 @@ import numpy as np from appdirs import user_config_dir -from graxpert.ai_model_handling import ai_model_path_from_version, bge_ai_models_dir, denoise_ai_models_dir, download_version, validate_local_version +from graxpert.ai_model_handling import ( + ai_model_path_from_version, + bge_ai_models_dir, + deconvolution_object_ai_models_dir, + deconvolution_stars_ai_models_dir, + denoise_ai_models_dir, + download_version, + validate_local_version, +) from graxpert.app_state import INITIAL_STATE from graxpert.application.app_events import AppEvents from graxpert.application.eventbus import eventbus from graxpert.astroimage import AstroImage -from graxpert.AstroImageRepository import AstroImageRepository +from graxpert.AstroImageRepository import AstroImageRepository, ImageTypes from graxpert.background_extraction import extract_background from graxpert.commands import INIT_HANDLER, RESET_POINTS_HANDLER, RM_POINT_HANDLER, SEL_POINTS_HANDLER, Command +from graxpert.deconvolution import deconvolve from graxpert.denoising import denoise from graxpert.localization import _ from graxpert.mp_logging import logfile_name from graxpert.preferences import fitsheader_2_app_state, load_preferences, prefs_2_app_state -from graxpert.s3_secrets import bge_bucket_name, denoise_bucket_name +from graxpert.s3_secrets import bge_bucket_name, deconvolution_object_bucket_name, deconvolution_stars_bucket_name, denoise_bucket_name from graxpert.stretch import StretchParameters, stretch_all from graxpert.ui.loadingframe import DynamicProgressThread class GraXpert: + def __init__(self): self.initialize() @@ -36,7 +46,7 @@ def initialize(self): self.data_type = "" self.images = AstroImageRepository() - self.display_type = "Original" + self.display_type = ImageTypes.Original self.mat_affine = np.eye(3) @@ -66,14 +76,18 @@ def initialize(self): eventbus.add_listener(AppEvents.INTERPOL_TYPE_CHANGED, self.on_interpol_type_changed) eventbus.add_listener(AppEvents.SMOTTHING_CHANGED, self.on_smoothing_changed) eventbus.add_listener(AppEvents.CALCULATE_REQUEST, self.on_calculate_request) + # deconvolution + eventbus.add_listener(AppEvents.DECONVOLUTION_TYPE_CHANGED, self.on_deconvolution_type_changed) + eventbus.add_listener(AppEvents.DECONVOLUTION_STRENGTH_CHANGED, self.on_deconvolution_strength_changed) + eventbus.add_listener(AppEvents.DECONVOLUTION_PSFSIZE_CHANGED, self.on_deconvolution_psfsize_changed) + eventbus.add_listener(AppEvents.DECONVOLUTION_REQUEST, self.on_deconvolution_request) # denoising eventbus.add_listener(AppEvents.DENOISE_STRENGTH_CHANGED, self.on_denoise_strength_changed) eventbus.add_listener(AppEvents.DENOISE_REQUEST, self.on_denoise_request) # saving eventbus.add_listener(AppEvents.SAVE_AS_CHANGED, self.on_save_as_changed) + eventbus.add_listener(AppEvents.SAVE_STRETCHED_CHANGED, self.on_save_stretched_changed) eventbus.add_listener(AppEvents.SAVE_REQUEST, self.on_save_request) - eventbus.add_listener(AppEvents.SAVE_BACKGROUND_REQUEST, self.on_save_background_request) - eventbus.add_listener(AppEvents.SAVE_STRETCHED_REQUEST, self.on_save_stretched_request) # advanced settings eventbus.add_listener(AppEvents.SAMPLE_SIZE_CHANGED, self.on_sample_size_changed) eventbus.add_listener(AppEvents.SAMPLE_COLOR_CHANGED, self.on_sample_color_changed) @@ -82,6 +96,8 @@ def initialize(self): eventbus.add_listener(AppEvents.CORRECTION_TYPE_CHANGED, self.on_correction_type_changed) eventbus.add_listener(AppEvents.LANGUAGE_CHANGED, self.on_language_selected) eventbus.add_listener(AppEvents.BGE_AI_VERSION_CHANGED, self.on_bge_ai_version_changed) + eventbus.add_listener(AppEvents.DECONVOLUTION_OBJECT_AI_VERSION_CHANGED, self.on_deconvolution_object_ai_version_changed) + eventbus.add_listener(AppEvents.DECONVOLUTION_STARS_AI_VERSION_CHANGED, self.on_deconvolution_stars_ai_version_changed) eventbus.add_listener(AppEvents.DENOISE_AI_VERSION_CHANGED, self.on_denoise_ai_version_changed) eventbus.add_listener(AppEvents.SCALING_CHANGED, self.on_scaling_changed) eventbus.add_listener(AppEvents.AI_BATCH_SIZE_CHANGED, self.on_ai_batch_size_changed) @@ -107,7 +123,7 @@ def on_bg_tol_changed(self, event): self.prefs.bg_tol_option = event["bg_tol_option"] def on_calculate_request(self, event=None): - if self.images.get("Original") is None: + if self.images.get(ImageTypes.Original) is None: messagebox.showerror("Error", _("Please load your picture first.")) return @@ -142,7 +158,7 @@ def on_calculate_request(self, event=None): try: self.prefs.images_linked_option = False - img_array_to_be_processed = np.copy(self.images.get("Original").img_array) + img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Original).img_array) background = AstroImage() background.set_from_array( @@ -167,14 +183,14 @@ def on_calculate_request(self, event=None): # Update fits header and metadata background_mean = np.mean(background.img_array) - gradient_corrected.update_fits_header(self.images.get("Original").fits_header, background_mean, self.prefs, self.cmd.app_state) - gradient_corrected.update_fits_header(self.images.get("Original").fits_header, background_mean, self.prefs, self.cmd.app_state) + gradient_corrected.update_fits_header(self.images.get(ImageTypes.Original).fits_header, background_mean, self.prefs, self.cmd.app_state) + gradient_corrected.update_fits_header(self.images.get(ImageTypes.Original).fits_header, background_mean, self.prefs, self.cmd.app_state) - gradient_corrected.copy_metadata(self.images.get("Original")) - background.copy_metadata(self.images.get("Original")) + gradient_corrected.copy_metadata(self.images.get(ImageTypes.Original)) + background.copy_metadata(self.images.get(ImageTypes.Original)) - self.images.set("Gradient-Corrected", gradient_corrected) - self.images.set("Background", background) + self.images.set(ImageTypes.Gradient_Corrected, gradient_corrected) + self.images.set(ImageTypes.Background, background) self.images.stretch_all(StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option), self.prefs.saturation) @@ -190,7 +206,7 @@ def on_calculate_request(self, event=None): eventbus.emit(AppEvents.CALCULATE_END) def on_change_saturation_request(self, event): - if self.images.get("Original") is None: + if self.images.get(ImageTypes.Original) is None: return self.prefs.saturation = event["saturation"] @@ -205,17 +221,95 @@ def on_correction_type_changed(self, event): self.prefs.corr_type = event["corr_type"] def on_create_grid_request(self, event=None): - if self.images.get("Original") is None: + if self.images.get(ImageTypes.Original) is None: messagebox.showerror("Error", _("Please load your picture first.")) return eventbus.emit(AppEvents.CREATE_GRID_BEGIN) - self.cmd = Command(SEL_POINTS_HANDLER, self.cmd, data=self.images.get("Original").img_array, num_pts=self.prefs.bg_pts_option, tol=self.prefs.bg_tol_option, sample_size=self.prefs.sample_size) + self.cmd = Command( + 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 + ) self.cmd.execute() eventbus.emit(AppEvents.CREATE_GRID_END) + def on_deconvolution_type_changed(self, event): + self.prefs.deconvolution_type_option = event["deconvolution_type_option"] + + def on_deconvolution_strength_changed(self, event): + self.prefs.deconvolution_strength = event["deconvolution_strength"] + + def on_deconvolution_psfsize_changed(self, event): + self.prefs.deconvolution_psfsize = event["deconvolution_psfsize"] + + def on_deconvolution_object_ai_version_changed(self, event): + self.prefs.deconvolution_object_ai_version = event["deconvolution_object_ai_version"] + + def on_deconvolution_stars_ai_version_changed(self, event): + self.prefs.deconvolution_stars_ai_version = event["deconvolution_stars_ai_version"] + + def on_deconvolution_request(self, event): + if self.images.get(ImageTypes.Original) is None: + messagebox.showerror("Error", _("Please load your picture first.")) + return + + if not self.validate_deconvolution_ai_installation(): + return + + eventbus.emit(AppEvents.DECONVOLUTION_BEGIN) + + progress = DynamicProgressThread(callback=lambda p: eventbus.emit(AppEvents.DECONVOLUTION_PROGRESS, {"progress": p})) + + deconvolution_type_option = self.prefs.deconvolution_type_option + + try: + img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Original).img_array) + if self.images.get(ImageTypes.Gradient_Corrected) is not None: + img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Gradient_Corrected).img_array) + + self.prefs.images_linked_option = True + + if deconvolution_type_option == "Object-only": + ai_model_path = ai_model_path_from_version(deconvolution_object_ai_models_dir, self.prefs.deconvolution_object_ai_version) + else: + ai_model_path = ai_model_path_from_version(deconvolution_stars_ai_models_dir, self.prefs.deconvolution_stars_ai_version) + imarray = deconvolve( + img_array_to_be_processed, + ai_model_path, + self.prefs.deconvolution_strength, + self.prefs.deconvolution_psfsize, + batch_size=self.prefs.ai_batch_size, + progress=progress, + ai_gpu_acceleration=self.prefs.ai_gpu_acceleration, + ) + + if imarray is not None: + + deconvolved = AstroImage() + deconvolved.set_from_array(imarray) + + # Update fits header and metadata + background_mean = np.mean(self.images.get(ImageTypes.Original).img_array) + deconvolved.update_fits_header(self.images.get(ImageTypes.Original).fits_header, background_mean, self.prefs, self.cmd.app_state) + + deconvolved.copy_metadata(self.images.get(ImageTypes.Original)) + + self.images.set(f"Deconvolved {deconvolution_type_option}", deconvolved) + + self.images.stretch_all(StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option, self.prefs.images_linked_option), self.prefs.saturation) + + eventbus.emit(AppEvents.DECONVOLUTION_SUCCESS, {"deconvolution_type_option": f"Deconvolved {deconvolution_type_option}"}) + eventbus.emit(AppEvents.UPDATE_DISPLAY_TYPE_REEQUEST, {"display_type": f"Deconvolved {deconvolution_type_option}"}) + + except Exception as e: + logging.exception(e) + eventbus.emit(AppEvents.DECONVOLUTION_ERROR) + messagebox.showerror("Error", _("An error occured during deconvolution. Please see the log at {}.".format(logfile_name))) + finally: + progress.done_progress() + eventbus.emit(AppEvents.DECONVOLUTION_END) + def on_denoise_ai_version_changed(self, event): self.prefs.denoise_ai_version = event["denoise_ai_version"] @@ -238,7 +332,7 @@ def on_language_selected(self, event): def on_load_image(self, event): eventbus.emit(AppEvents.LOAD_IMAGE_BEGIN) filename = event["filename"] - self.display_type = "Original" + self.display_type = ImageTypes.Original try: image = AstroImage() @@ -255,13 +349,13 @@ def on_load_image(self, event): self.data_type = os.path.splitext(filename)[1] self.images.reset() - self.images.set("Original", image) + self.images.set(ImageTypes.Original, image) self.prefs.working_dir = os.path.dirname(filename) os.chdir(os.path.dirname(filename)) - width = self.images.get("Original").img_display.width - height = self.images.get("Original").img_display.height + width = self.images.get(ImageTypes.Original).img_display.width + height = self.images.get(ImageTypes.Original).img_display.height if self.prefs.width != width or self.prefs.height != height: self.reset_backgroundpts() @@ -269,7 +363,7 @@ def on_load_image(self, event): self.prefs.width = width self.prefs.height = height - tmp_state = fitsheader_2_app_state(self, self.cmd.app_state, self.images.get("Original").fits_header) + tmp_state = fitsheader_2_app_state(self, self.cmd.app_state, self.images.get(ImageTypes.Original).fits_header) self.cmd: Command = Command(INIT_HANDLER, background_points=tmp_state.background_points) self.cmd.execute() @@ -322,6 +416,9 @@ def on_sample_size_changed(self, event): def on_save_as_changed(self, event): self.prefs.saveas_option = event["saveas_option"] + def on_save_stretched_changed(self, event): + self.prefs.saveas_stretched = event["saveas_stretched"] + def on_smoothing_changed(self, event): self.prefs.smoothing_option = event["smoothing_option"] @@ -329,7 +426,7 @@ def on_denoise_strength_changed(self, event): self.prefs.denoise_strength = event["denoise_strength"] def on_denoise_request(self, event): - if self.images.get("Original") is None: + if self.images.get(ImageTypes.Original) is None: messagebox.showerror("Error", _("Please load your picture first.")) return @@ -341,9 +438,13 @@ def on_denoise_request(self, event): progress = DynamicProgressThread(callback=lambda p: eventbus.emit(AppEvents.DENOISE_PROGRESS, {"progress": p})) try: - img_array_to_be_processed = np.copy(self.images.get("Original").img_array) - if self.images.get("Gradient-Corrected") is not None: - img_array_to_be_processed = np.copy(self.images.get("Gradient-Corrected").img_array) + + if self.images.get(ImageTypes.Deconvolved_Object_only) is not None: + img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Deconvolved_Object_only).img_array) + elif self.images.get(ImageTypes.Gradient_Corrected) is not None: + img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Gradient_Corrected).img_array) + else: + img_array_to_be_processed = np.copy(self.images.get(ImageTypes.Original).img_array) self.prefs.images_linked_option = True ai_model_path = ai_model_path_from_version(denoise_ai_models_dir, self.prefs.denoise_ai_version) @@ -362,12 +463,12 @@ def on_denoise_request(self, event): denoised.set_from_array(imarray) # Update fits header and metadata - background_mean = np.mean(self.images.get("Original").img_array) - denoised.update_fits_header(self.images.get("Original").fits_header, background_mean, self.prefs, self.cmd.app_state) + background_mean = np.mean(self.images.get(ImageTypes.Original).img_array) + denoised.update_fits_header(self.images.get(ImageTypes.Original).fits_header, background_mean, self.prefs, self.cmd.app_state) - denoised.copy_metadata(self.images.get("Original")) + denoised.copy_metadata(self.images.get(ImageTypes.Original)) - self.images.set("Denoised", denoised) + self.images.set(ImageTypes.Denoised, denoised) self.images.stretch_all(StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option, self.prefs.images_linked_option), self.prefs.saturation) @@ -383,62 +484,41 @@ def on_denoise_request(self, event): eventbus.emit(AppEvents.DENOISE_END) def on_save_request(self, event): - if self.prefs.saveas_option == "16 bit Tiff" or self.prefs.saveas_option == "32 bit Tiff": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs.working_dir) - elif self.prefs.saveas_option == "16 bit XISF" or self.prefs.saveas_option == "32 bit XISF": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs.working_dir) - else: - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_GraXpert.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=self.prefs.working_dir) - - if dir == "": - return - - eventbus.emit(AppEvents.SAVE_BEGIN) - - try: - if self.images.get("Denoised") is not None: - self.images.get("Denoised").save(dir, self.prefs.saveas_option) - elif self.images.get("Gradient-Corrected") is not None: - self.images.get("Gradient-Corrected").save(dir, self.prefs.saveas_option) - else: - self.images.get("Original").save(dir, self.prefs.saveas_option) - except Exception as e: - logging.exception(e) - eventbus.emit(AppEvents.SAVE_ERROR) - messagebox.showerror("Error", _("Error occured when saving the image.")) - - eventbus.emit(AppEvents.SAVE_END) + suffix_1 = "_graxpert" + + match self.display_type: + case ImageTypes.Gradient_Corrected: + suffix_2 = "_bge" + case ImageTypes.Background: + suffix_2 = "_background" + case ImageTypes.Deconvolved_Object_only: + suffix_2 = "_obj_decon" + case ImageTypes.Deconvolved_Stars_only: + suffix_2 = "_stars_decon" + case ImageTypes.Denoised: + suffix_2 = "_denoised" + case _: + suffix_2 = "" + + match self.prefs.saveas_stretched: + case True: + suffix_3 = "_stretched" + case _: + suffix_3 = "" - def on_save_background_request(self, event): if self.prefs.saveas_option == "16 bit Tiff" or self.prefs.saveas_option == "32 bit Tiff": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs.working_dir) - elif self.prefs.saveas_option == "16 bit XISF" or self.prefs.saveas_option == "32 bit XISF": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs.working_dir) - else: - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_background.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=os.getcwd()) - - if dir == "": - return - - eventbus.emit(AppEvents.SAVE_BEGIN) - - try: - self.images.get("Background").save(dir, self.prefs.saveas_option) - except Exception as e: - logging.exception(e) - eventbus.emit(AppEvents.SAVE_ERROR) - messagebox.showerror("Error", _("Error occured when saving the image.")) - - eventbus.emit(AppEvents.SAVE_END) - - def on_save_stretched_request(self, event): - if self.prefs.saveas_option == "16 bit Tiff" or self.prefs.saveas_option == "32 bit Tiff": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs.working_dir) + dir = tk.filedialog.asksaveasfilename( + initialfile=self.filename + f"{suffix_1}{suffix_2}{suffix_3}.tiff", filetypes=[("Tiff", ".tiff")], defaultextension=".tiff", initialdir=self.prefs.working_dir + ) elif self.prefs.saveas_option == "16 bit XISF" or self.prefs.saveas_option == "32 bit XISF": - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs.working_dir) + dir = tk.filedialog.asksaveasfilename( + initialfile=self.filename + f"{suffix_1}{suffix_2}{suffix_3}.xisf", filetypes=[("XISF", ".xisf")], defaultextension=".xisf", initialdir=self.prefs.working_dir + ) else: - dir = tk.filedialog.asksaveasfilename(initialfile=self.filename + "_stretched_GraXpert.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=self.prefs.working_dir) + dir = tk.filedialog.asksaveasfilename( + initialfile=self.filename + f"{suffix_1}{suffix_2}{suffix_3}.fits", filetypes=[("Fits", ".fits")], defaultextension=".fits", initialdir=self.prefs.working_dir + ) if dir == "": return @@ -446,16 +526,14 @@ def on_save_stretched_request(self, event): eventbus.emit(AppEvents.SAVE_BEGIN) try: - if self.images.get("Denoised") is not None: - self.images.get("Denoised").save_stretched(dir, self.prefs.saveas_option, StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option)) - elif self.images.get("Gradient-Corrected") is not None: - self.images.get("Gradient-Corrected").save_stretched(dir, self.prefs.saveas_option, StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option)) + if self.prefs.saveas_stretched: + self.images.get(self.display_type).save_stretched(dir, self.prefs.saveas_option, StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option)) else: - self.images.get("Original").save_stretched(dir, self.prefs.saveas_option, StretchParameters(self.prefs.stretch_option, self.prefs.channels_linked_option)) + self.images.get(self.display_type).save(dir, self.prefs.saveas_option) except Exception as e: - eventbus.emit(AppEvents.SAVE_ERROR) logging.exception(e) + eventbus.emit(AppEvents.SAVE_ERROR) messagebox.showerror("Error", _("Error occured when saving the image.")) eventbus.emit(AppEvents.SAVE_END) @@ -603,6 +681,44 @@ def callback(p): eventbus.emit(AppEvents.AI_DOWNLOAD_END) return True + def validate_deconvolution_ai_installation(self): + + if self.prefs.deconvolution_type_option == "Object-only": + + if self.prefs.deconvolution_object_ai_version is None or self.prefs.deconvolution_object_ai_version == "None": + messagebox.showerror("Error", _("No Object-only Deconvolution AI-Model selected. Please select one from the Advanced panel on the right.")) + return False + + if not validate_local_version(deconvolution_object_ai_models_dir, self.prefs.deconvolution_object_ai_version): + if not messagebox.askyesno(_("Install AI-Model?"), _("Selected Object-only Deconvolution AI-Model is not installed. Should I download it now?")): + return False + else: + eventbus.emit(AppEvents.AI_DOWNLOAD_BEGIN) + + def callback(p): + eventbus.emit(AppEvents.AI_DOWNLOAD_PROGRESS, {"progress": p}) + + download_version(deconvolution_object_ai_models_dir, deconvolution_object_bucket_name, self.prefs.deconvolution_object_ai_version, progress=callback) + eventbus.emit(AppEvents.AI_DOWNLOAD_END) + return True + else: + if self.prefs.deconvolution_stars_ai_version is None or self.prefs.deconvolution_stars_ai_version == "None": + messagebox.showerror("Error", _("No Stars-only Denoising AI-Model selected. Please select one from the Advanced panel on the right.")) + return False + + if not validate_local_version(deconvolution_stars_ai_models_dir, self.prefs.deconvolution_stars_ai_version): + if not messagebox.askyesno(_("Install AI-Model?"), _("Selected Stars-only Deconvolution AI-Model is not installed. Should I download it now?")): + return False + else: + eventbus.emit(AppEvents.AI_DOWNLOAD_BEGIN) + + def callback(p): + eventbus.emit(AppEvents.AI_DOWNLOAD_PROGRESS, {"progress": p}) + + download_version(deconvolution_stars_ai_models_dir, deconvolution_stars_bucket_name, self.prefs.deconvolution_stars_ai_version, progress=callback) + eventbus.emit(AppEvents.AI_DOWNLOAD_END) + return True + def validate_denoise_ai_installation(self): if self.prefs.denoise_ai_version is None or self.prefs.denoise_ai_version == "None": messagebox.showerror("Error", _("No Denoising AI-Model selected. Please select one from the Advanced panel on the right.")) diff --git a/graxpert/application/app_events.py b/graxpert/application/app_events.py index b8d0a84..aa2179b 100644 --- a/graxpert/application/app_events.py +++ b/graxpert/application/app_events.py @@ -43,6 +43,17 @@ class AppEvents(Enum): CALCULATE_END = auto() CALCULATE_SUCCESS = auto() CALCULATE_ERROR = auto() + # deconvolution + DECONVOLUTION_TYPE_CHANGED = auto() + DECONVOLUTION_STRENGTH_CHANGED = auto() + DECONVOLUTION_PSFSIZE_CHANGED = auto() + DECONVOLUTION_OPERATION_CHANGED = auto() + DECONVOLUTION_REQUEST = auto() + DECONVOLUTION_BEGIN = auto() + DECONVOLUTION_PROGRESS = auto() + DECONVOLUTION_END = auto() + DECONVOLUTION_SUCCESS = auto() + DECONVOLUTION_ERROR = auto() # denoising DENOISE_STRENGTH_CHANGED = auto() DENOISE_THRESHOLD_CHANGED = auto() @@ -54,9 +65,8 @@ class AppEvents(Enum): DENOISE_ERROR = auto() # saving SAVE_AS_CHANGED = auto() + SAVE_STRETCHED_CHANGED = auto() SAVE_REQUEST = auto() - SAVE_BACKGROUND_REQUEST = auto() - SAVE_STRETCHED_REQUEST = auto() SAVE_BEGIN = auto() SAVE_END = auto() SAVE_ERROR = auto() @@ -68,6 +78,9 @@ class AppEvents(Enum): # bge ai model handling BGE_AI_VERSION_CHANGED = auto() # denoise ai model handling + DECONVOLUTION_OBJECT_AI_VERSION_CHANGED = auto() + DECONVOLUTION_STARS_AI_VERSION_CHANGED = auto() + # denoise ai model handling DENOISE_AI_VERSION_CHANGED = auto() # advanced settings SAMPLE_SIZE_CHANGED = auto() diff --git a/graxpert/astroimage.py b/graxpert/astroimage.py index 0486371..ad405ad 100644 --- a/graxpert/astroimage.py +++ b/graxpert/astroimage.py @@ -4,7 +4,6 @@ import numpy as np from astropy.io import fits -from astropy.stats import sigma_clipped_stats from PIL import Image, ImageEnhance from skimage import exposure, img_as_float32, io from skimage.util import img_as_uint @@ -180,7 +179,8 @@ def save_stretched(self, dir, saveas_type, stretch_params): if self.img_array is None: return - self.fits_header["STRETCH"] = stretch_params.stretch_option + if self.fits_header is not None: + self.fits_header["STRETCH"] = stretch_params.stretch_option stretched_img = self.stretch(stretch_params) diff --git a/graxpert/background_grid_selection.py b/graxpert/background_grid_selection.py index 954576b..2089edb 100644 --- a/graxpert/background_grid_selection.py +++ b/graxpert/background_grid_selection.py @@ -1,6 +1,5 @@ import numpy as np from skimage import color -from skimage.transform import rescale import graxpert.skyall import graxpert.stretch diff --git a/graxpert/cmdline_tools.py b/graxpert/cmdline_tools.py index 3bb97d4..7b9c98e 100644 --- a/graxpert/cmdline_tools.py +++ b/graxpert/cmdline_tools.py @@ -7,12 +7,13 @@ import numpy as np from appdirs import user_config_dir -from graxpert.ai_model_handling import ai_model_path_from_version, bge_ai_models_dir, denoise_ai_models_dir, download_version, latest_version, list_local_versions +from graxpert.ai_model_handling import ai_model_path_from_version, bge_ai_models_dir, denoise_ai_models_dir, deconvolution_object_ai_models_dir, download_version, latest_version, list_local_versions from graxpert.astroimage import AstroImage from graxpert.background_extraction import extract_background from graxpert.denoising import denoise +from graxpert.deconvolution import deconvolve from graxpert.preferences import Prefs, load_preferences, save_preferences -from graxpert.s3_secrets import bge_bucket_name, denoise_bucket_name +from graxpert.s3_secrets import bge_bucket_name, denoise_bucket_name, deconvolution_object_bucket_name user_preferences_filename = os.path.join(user_config_dir(appname="GraXpert"), "preferences.json") @@ -303,3 +304,119 @@ def get_ai_version(self, prefs): save_preferences(user_preferences_filename, user_preferences) return ai_version + + +class DeconvObjCmdlineTool(CmdlineToolBase): + def __init__(self, args): + super().__init__(args) + self.args = args + + def execute(self): + astro_Image = AstroImage(do_update_display=False) + astro_Image.set_from_file(self.args.filename, None, None) + + processed_Astro_Image = AstroImage(do_update_display=False) + + processed_Astro_Image.fits_header = astro_Image.fits_header + + if self.args.preferences_file is not None: + preferences = Prefs() + try: + preferences_file = os.path.abspath(self.args.preferences_file) + if os.path.isfile(preferences_file): + with open(preferences_file, "r") as f: + json_prefs = json.load(f) + if "ai_version" in json_prefs: + preferences.ai_version = json_prefs["ai_version"] + if "deconvolution_strength" in json_prefs: + preferences.deconvolution_strength = json_prefs["deconvolution_strength"] + if "deconvolution_psfsize" in json_prefs: + preferences.deconvolution_psfsize = json_prefs["deconvolution_psfsize"] + if "ai_batch_size" in json_prefs: + preferences.ai_batch_size = json_prefs["ai_batch_size"] + if "ai_gpu_acceleration" in json_prefs: + preferences.ai_gpu_acceleration = json_prefs["ai_gpu_acceleration"] + + except Exception as e: + logging.exception(e) + logging.shutdown() + sys.exit(1) + else: + preferences = Prefs() + + if self.args.deconvolution_strength is not None: + preferences.deconvolution_strength = self.args.deconvolution_strength + logging.info(f"Using user-supplied deconvolution strength value {preferences.deconvolution_strength}.") + else: + logging.info(f"Using stored deconvolution strength value {preferences.deconvolution_strength}.") + + if self.args.deconvolution_psfsize is not None: + preferences.deconvolution_psfsize = self.args.deconvolution_psfsize + logging.info(f"Using user-supplied deconvolution psfsize value {preferences.deconvolution_psfsize}.") + else: + logging.info(f"Using stored deconvolution psfsize value {preferences.deconvolution_psfsize}.") + + if self.args.ai_batch_size is not None: + preferences.ai_batch_size = self.args.ai_batch_size + logging.info(f"Using user-supplied batch size value {preferences.ai_batch_size}.") + else: + logging.info(f"Using stored batch size value {preferences.ai_batch_size}.") + + if self.args.gpu_acceleration is not None: + preferences.ai_gpu_acceleration = True if self.args.gpu_acceleration == "true" else False + logging.info(f"Using user-supplied gpu acceleration setting {preferences.ai_gpu_acceleration}.") + else: + logging.info(f"Using stored gpu acceleration setting {preferences.ai_gpu_acceleration}.") + + ai_model_path = ai_model_path_from_version(deconvolution_object_ai_models_dir, self.get_ai_version(preferences)) + + logging.info( + dedent( + f"""\ + Excecuting deconvolution on objects with the following parameters: + AI model path - {ai_model_path} + deconvolution strength - {preferences.deconvolution_strength} + deconvolution psfsize - {preferences.deconvolution_psfsize}""" + ) + ) + + processed_Astro_Image.set_from_array( + deconvolve( + astro_Image.img_array, + ai_model_path, + preferences.deconvolution_strength, + preferences.deconvolution_psfsize, + batch_size=preferences.ai_batch_size, + ai_gpu_acceleration=preferences.ai_gpu_acceleration, + ) + ) + processed_Astro_Image.save(self.get_save_path(), self.get_output_file_format()) + + def get_ai_version(self, prefs): + user_preferences = load_preferences(user_preferences_filename) + + ai_version = None + if self.args.ai_version: + ai_version = self.args.ai_version + logging.info(f"Using user-supplied AI version {ai_version}.") + else: + ai_version = prefs.deconvolution_object_ai_version + + if ai_version is None: + ai_version = latest_version(deconvolution_object_ai_models_dir, deconvolution_object_bucket_name) + logging.info(f"Using AI version {ai_version}. You can overwrite this by providing the argument '-ai_version'") + + if not ai_version in [v["version"] for v in list_local_versions(deconvolution_object_ai_models_dir)]: + try: + logging.info(f"AI version {ai_version} not found locally, downloading...") + download_version(deconvolution_object_ai_models_dir, deconvolution_object_bucket_name, ai_version) + logging.info("download successful") + except Exception as e: + logging.exception(e) + logging.shutdown() + sys.exit(1) + + user_preferences.ai_version = ai_version + save_preferences(user_preferences_filename, user_preferences) + + return ai_version diff --git a/graxpert/deconvolution.py b/graxpert/deconvolution.py new file mode 100644 index 0000000..db51a4e --- /dev/null +++ b/graxpert/deconvolution.py @@ -0,0 +1,161 @@ +import copy +import logging + +import numpy as np +import onnxruntime as ort + +from graxpert.ai_model_handling import get_execution_providers_ordered +from graxpert.application.app_events import AppEvents +from graxpert.application.eventbus import eventbus + + +def deconvolve(image, ai_path, strength, psfsize, batch_size=4, window_size=512, stride=448, progress=None, ai_gpu_acceleration=True): + print("Starting deconvolution") + strength = 0.95 * strength # TODO : strenght of exactly 1.0 brings no results, to fix + psfsize = np.clip((psfsize / 2.355 - 1.0) / 5.0, 0.05, 0.95) + + logging.info(f"Calculated normalized PSFsize value: {psfsize}") + + if batch_size < 1: + logging.info(f"mapping batch_size of {batch_size} to 1") + batch_size = 1 + elif batch_size > 32: + logging.info(f"mapping batch_size of {batch_size} to 32") + batch_size = 32 + elif not (batch_size & (batch_size - 1) == 0): # check if batch_size is power of two + logging.info(f"mapping batch_size of {batch_size} to {2 ** (batch_size).bit_length() // 2}") + batch_size = 2 ** (batch_size).bit_length() // 2 # map batch_size to power of two + + if batch_size >= 4 and image.shape[-1] == 3: + batch_size = batch_size // 4 + + num_colors = image.shape[-1] + + H, W, _ = image.shape + offset = int((window_size - stride) / 2) + + h, w, _ = image.shape + + ith = int(h / stride) + 1 + itw = int(w / stride) + 1 + + dh = ith * stride - h + dw = itw * stride - w + + image = np.concatenate((image, image[(h - dh) :, :, :]), axis=0) + image = np.concatenate((image, image[:, (w - dw) :, :]), axis=1) + + h, w, _ = image.shape + image = np.concatenate((image, image[(h - offset) :, :, :]), axis=0) + image = np.concatenate((image[:offset, :, :], image), axis=0) + image = np.concatenate((image, image[:, (w - offset) :, :]), axis=1) + image = np.concatenate((image[:, :offset, :], image), axis=1) + + output = copy.deepcopy(image) + + providers = get_execution_providers_ordered(ai_gpu_acceleration) + session = ort.InferenceSession(ai_path, providers=providers) + + logging.info(f"Available inference providers : {providers}") + logging.info(f"Used inference providers : {session.get_providers()}") + + cancel_flag = False + + def cancel_listener(event): + nonlocal cancel_flag + cancel_flag = True + + eventbus.add_listener(AppEvents.CANCEL_PROCESSING, cancel_listener) + + last_progress = 0 + for b in range(0, ith * itw + batch_size, batch_size): + + if cancel_flag: + logging.info("Deconvolution cancelled") + eventbus.remove_listener(AppEvents.CANCEL_PROCESSING, cancel_listener) + return None + + input_tiles = [] + input_tile_copies = [] + params = [] + for t_idx in range(0, batch_size): + + index = b + t_idx + i = index % ith + j = index // ith + + if i >= ith or j >= itw: + break + + x = stride * i + y = stride * j + + tile = image[x : x + window_size, y : y + window_size, :] + + _min = np.min(tile, axis=(0, 1)) + tile = tile - _min + 1e-5 + tile = np.log(tile) + + _mean = tile.mean() + _std = tile.std() + _mean, _std = _mean.astype(np.float32), _std.astype(np.float32) + tile = (tile - _mean) / _std * 0.1 + params.append([_mean, _std, _min]) + + input_tile_copies.append(np.copy(tile)) + + input_tiles.append(tile) + + if not input_tiles: + continue + + input_tiles = np.array(input_tiles) + input_tiles = np.moveaxis(input_tiles, -1, 1) + input_tiles = np.reshape(input_tiles, [input_tiles.shape[0] * num_colors, 1, window_size, window_size]) + + output_tiles = [] + sigma = np.full(shape=(input_tiles.shape[0], 1), fill_value=psfsize, dtype=np.float32) + strenght_p = np.full(shape=(input_tiles.shape[0], 1), fill_value=strength, dtype=np.float32) + session_result = session.run(None, {"gen_input_image": input_tiles, "sigma": sigma, "strenght": strenght_p})[0] + for e in session_result: + output_tiles.append(e) + + output_tiles = np.array(output_tiles) + output_tiles = input_tiles - output_tiles + output_tiles = np.reshape(output_tiles, [output_tiles.shape[0] // num_colors, num_colors, window_size, window_size]) + output_tiles = np.moveaxis(output_tiles, 1, -1) + + for idx in range(len(params)): + output_tiles[idx] = output_tiles[idx] * params[idx][1] / 0.1 + params[idx][0] + output_tiles[idx] = np.exp(output_tiles[idx]) + output_tiles[idx] = output_tiles[idx] + params[idx][2] - 1e-5 + + for t_idx, tile in enumerate(output_tiles): + + index = b + t_idx + i = index % ith + j = index // ith + + if i >= ith or j >= itw: + break + + x = stride * i + y = stride * j + tile = tile[offset : offset + stride, offset : offset + stride, :] + output[x + offset : stride * (i + 1) + offset, y + offset : stride * (j + 1) + offset, :] = tile + + p = int(b / (ith * itw + batch_size) * 100) + if p > last_progress: + if progress is not None: + progress.update(p - last_progress) + else: + logging.info(f"Progress: {p}%") + last_progress = p + + output = output[offset : H + offset, offset : W + offset, :] + output = np.clip(output, 0.0, 1.0) + + eventbus.remove_listener(AppEvents.CANCEL_PROCESSING, cancel_listener) + logging.info("Finished deconvolution") + + return output diff --git a/graxpert/main.py b/graxpert/main.py index dbc5ae4..26cf395 100644 --- a/graxpert/main.py +++ b/graxpert/main.py @@ -15,9 +15,9 @@ from packaging import version -from graxpert.ai_model_handling import bge_ai_models_dir, denoise_ai_models_dir, list_local_versions, list_remote_versions +from graxpert.ai_model_handling import bge_ai_models_dir, denoise_ai_models_dir, deconvolution_object_ai_models_dir, list_local_versions, list_remote_versions from graxpert.mp_logging import configure_logging -from graxpert.s3_secrets import bge_bucket_name, denoise_bucket_name +from graxpert.s3_secrets import bge_bucket_name, denoise_bucket_name, deconvolution_object_bucket_name from graxpert.version import release as graxpert_release from graxpert.version import version as graxpert_version @@ -54,6 +54,10 @@ def denoise_version_type(arg_value, pat=re.compile(r"^\d+\.\d+\.\d+$")): return version_type(denoise_ai_models_dir, denoise_bucket_name, arg_value, pat=re.compile(r"^\d+\.\d+\.\d+$")) +def deconv_obj_dersion_type(arg_value, pat=re.compile(r"^\d+\.\d+\.\d+$")): + return version_type(deconvolution_object_ai_models_dir, deconvolution_object_bucket_name, arg_value, pat=re.compile(r"^\d+\.\d+\.\d+$")) + + def version_type(ai_models_dir, bucket_name, arg_value, pat=re.compile(r"^\d+\.\d+\.\d+$")): available_versions = collect_available_versions(ai_models_dir, bucket_name) @@ -183,6 +187,7 @@ def main(): available_bge_versions = collect_available_versions(bge_ai_models_dir, bge_bucket_name) available_denoise_versions = collect_available_versions(denoise_ai_models_dir, denoise_bucket_name) + available_deconv_obj_versions = collect_available_versions(deconvolution_object_ai_models_dir, deconvolution_object_bucket_name) parser = argparse.ArgumentParser(add_help=False) parser.add_argument("-cli", "--cli", required=False, action="store_true", help="Has to be added when using the command line integration of GraXpert") @@ -191,9 +196,9 @@ def main(): "--command", required=False, default="background-extraction", - choices=["background-extraction", "denoising"], + choices=["background-extraction", "denoising", "deconv-obj"], type=str, - help="Choose the image operation to execute: Background Extraction or Denoising", + help="Choose the image operation to execute: Background Extraction or Denoising or Deconvolution", ) parser.add_argument("filename", type=str, help="Path of the unprocessed image") parser.add_argument("-output", "--output", nargs="?", required=False, type=str, help="Filename of the processed image") @@ -256,9 +261,51 @@ def main(): help='Number of image tiles which Graxpert will denoise in parallel. Be careful: increasing this value might result in out-of-memory errors. Valid Range: 1..32, default: "4"', ) + deconv_obj_parser = argparse.ArgumentParser("GraXpert Deconvolution Object", parents=[parser], description="GraXpert, the astronomical deconvolution tool") + deconv_obj_parser.add_argument( + "-ai_version", + "--ai_version", + nargs="?", + required=False, + default=None, + type=deconv_obj_dersion_type, + help='Version of the Deconvolution Obj AI model, default: "latest"; available locally: [{}], available remotely: [{}]'.format( + ", ".join(available_deconv_obj_versions[0]), ", ".join(available_deconv_obj_versions[1]) + ), + ) + deconv_obj_parser.add_argument( + "-strength", + "--deconvolution_strength", + nargs="?", + required=False, + default=None, + type=float, + help='Strength of the desired deconvolution effect, default: "0.5"', + ) + deconv_obj_parser.add_argument( + "-psfsize", + "--deconvolution_psfsize", + nargs="?", + required=False, + default=None, + type=float, + help='Size of the seeing you want to deconvolve, default: "0.3"', + ) + deconv_obj_parser.add_argument( + "-batch_size", + "--ai_batch_size", + nargs="?", + required=False, + default=None, + type=int, + help='Number of image tiles which Graxpert will denoise in parallel. Be careful: increasing this value might result in out-of-memory errors. Valid Range: 1..32, default: "4"', + ) + if "-h" in sys.argv or "--help" in sys.argv: if "denoising" in sys.argv: denoise_parser.print_help() + elif "deconv-obj" in sys.argv: + deconv_obj_parser.print_help() else: bge_parser.print_help() sys.exit(0) @@ -267,6 +314,8 @@ def main(): if args.command == "background-extraction": args = bge_parser.parse_args() + elif args.command == "deconv-obj": + args = deconv_obj_parser.parse_args() else: args = denoise_parser.parse_args() @@ -284,6 +333,13 @@ def main(): clt = DenoiseCmdlineTool(args) clt.execute() logging.shutdown() + elif args.cli and args.command == "deconv-obj": + from graxpert.cmdline_tools import DeconvObjCmdlineTool + + logging.info(f"Starting GraXpert CLI, Deconvolution Obj, version: {graxpert_version} release: {graxpert_release}") + clt = DeconvObjCmdlineTool(args) + clt.execute() + logging.shutdown() else: logging.info(f"Starting GraXpert UI, version: {graxpert_version} release: {graxpert_release}") ui_main(args.filename) diff --git a/graxpert/preferences.py b/graxpert/preferences.py index 94ba3e3..32443ce 100644 --- a/graxpert/preferences.py +++ b/graxpert/preferences.py @@ -29,6 +29,7 @@ class Prefs: interpol_type_option: AnyStr = "RBF" smoothing_option: float = 0.0 saveas_option: AnyStr = "32 bit Tiff" + saveas_stretched: bool = False sample_size: int = 25 sample_color: int = 55 RBF_kernel: AnyStr = "thin_plate" @@ -37,8 +38,13 @@ class Prefs: corr_type: AnyStr = "Subtraction" scaling: float = 1.0 bge_ai_version: AnyStr = None + deconvolution_type_option: AnyStr = "Object-only" + deconvolution_object_ai_version: AnyStr = None + deconvolution_stars_ai_version: AnyStr = None denoise_ai_version: AnyStr = None graxpert_version: AnyStr = graxpert_version + deconvolution_strength: float = 0.5 + deconvolution_psfsize: float = 5.0 denoise_strength: float = 0.5 ai_batch_size: int = 4 ai_gpu_acceleration: bool = True @@ -70,10 +76,10 @@ def load_preferences(prefs_filename) -> Prefs: if "ai_version" in json_prefs: logging.warning(f"Obsolete key 'ai_version' found in {prefs_filename}. Renaming it to 'bge_ai_version.") - json_prefs = {"bge_ai_version" if k == "ai_version" else k:v for k,v in json_prefs.items()} - + json_prefs = {"bge_ai_version" if k == "ai_version" else k: v for k, v in json_prefs.items()} + prefs = merge_json(prefs, json_prefs) - + if not "graxpert_version" in json_prefs: # reset scaling in case we start from GraXpert < 2.1.0 prefs.scaling = 1.0 else: diff --git a/graxpert/skyall.py b/graxpert/skyall.py index 3620de8..f5d42e4 100644 --- a/graxpert/skyall.py +++ b/graxpert/skyall.py @@ -1,7 +1,6 @@ import logging import numpy as np -from skimage import img_as_float32, io """ Find the mode of a distribution using a route based on the SKYALL as described in @@ -144,10 +143,10 @@ def f(x, coeff): # Testing +# from skimage import img_as_float32, io #y=3100 #x=483 #size=25 #im = img_as_float32(io.imread("../../milchstr.tif")[x-size:x+size,y-size:y+size,0]) #mode(im) - diff --git a/graxpert/ui/canvas.py b/graxpert/ui/canvas.py index 747806b..e6cd56e 100644 --- a/graxpert/ui/canvas.py +++ b/graxpert/ui/canvas.py @@ -9,6 +9,7 @@ from graxpert.application.app import graxpert from graxpert.application.app_events import AppEvents from graxpert.application.eventbus import eventbus +from graxpert.AstroImageRepository import ImageTypes from graxpert.commands import ADD_POINT_HANDLER, ADD_POINTS_HANDLER, MOVE_POINT_HANDLER, Command from graxpert.localization import _ from graxpert.resource_utils import resource_path @@ -103,6 +104,11 @@ def register_events(self): eventbus.add_listener(AppEvents.CALCULATE_END, self.on_calculate_end) eventbus.add_listener(AppEvents.CALCULATE_SUCCESS, self.on_calculate_success) eventbus.add_listener(AppEvents.CALCULATE_ERROR, self.on_calculate_end) + eventbus.add_listener(AppEvents.DECONVOLUTION_BEGIN, self.on_deconvolution_begin) + eventbus.add_listener(AppEvents.DECONVOLUTION_PROGRESS, self.on_deconvolution_progress) + eventbus.add_listener(AppEvents.DECONVOLUTION_END, self.on_deconvolution_end) + eventbus.add_listener(AppEvents.DECONVOLUTION_SUCCESS, self.on_deconvolution_success) + eventbus.add_listener(AppEvents.DECONVOLUTION_ERROR, self.on_deconvolution_end) eventbus.add_listener(AppEvents.DENOISE_BEGIN, self.on_denoise_begin) eventbus.add_listener(AppEvents.DENOISE_PROGRESS, self.on_denoise_progress) eventbus.add_listener(AppEvents.DENOISE_END, self.on_denoise_end) @@ -143,11 +149,11 @@ def on_apply_crop_request(self, event=None): return graxpert.images.crop_all(self.startx, self.endx, self.starty, self.endy) - + self.startx = 0 self.starty = 0 - self.endx = graxpert.images.get("Original").width - self.endy = graxpert.images.get("Original").height + self.endx = graxpert.images.get(ImageTypes.Original).width + self.endy = graxpert.images.get(ImageTypes.Original).height eventbus.emit(AppEvents.RESET_POITS_REQUEST) self.zoom_fit(graxpert.images.get(self.display_type.get()).width, graxpert.images.get(self.display_type.get()).height) @@ -162,7 +168,7 @@ def on_calculate_begin(self, event=None): def on_calculate_progress(self, event=None): self.dynamic_progress_frame.update_progress(event["progress"]) - + def on_calculate_success(self, event=None): if not "Gradient-Corrected" in self.display_options: self.display_options.append("Gradient-Corrected") @@ -181,6 +187,29 @@ def on_calculate_end(self, event=None): self.show_progress_frame(False) self.redraw_image() + def on_deconvolution_begin(self, event=None): + self.dynamic_progress_frame.text.set(_("Deconvolving")) + self.dynamic_progress_frame.cancellable = True + self.show_progress_frame(True) + + def on_deconvolution_progress(self, event=None): + self.dynamic_progress_frame.update_progress(event["progress"]) + + def on_deconvolution_success(self, event=None): + self.dynamic_progress_frame.cancellable = False + if not event["deconvolution_type_option"] in self.display_options: + self.display_options.append(event["deconvolution_type_option"]) + self.display_menu.grid_forget() + self.display_menu = CTkOptionMenu(self, variable=self.display_type, values=self.display_options) + self.display_menu.grid(column=0, row=0, sticky=tk.N) + + def on_deconvolution_end(self, event=None): + self.dynamic_progress_frame.cancellable = False + self.dynamic_progress_frame.text.set("") + self.dynamic_progress_frame.variable.set(0.0) + self.show_progress_frame(False) + self.redraw_image() + def on_denoise_begin(self, event=None): self.dynamic_progress_frame.text.set(_("Denoising")) self.dynamic_progress_frame.cancellable = True @@ -188,7 +217,7 @@ def on_denoise_begin(self, event=None): def on_denoise_progress(self, event=None): self.dynamic_progress_frame.update_progress(event["progress"]) - + def on_denoise_success(self, event=None): self.dynamic_progress_frame.cancellable = False if not "Denoised" in self.display_options: @@ -231,13 +260,13 @@ def on_load_image_end(self, event=None): if self.display_menu is not None: self.display_menu.grid_forget() - self.display_options = ["Original"] + self.display_options = [ImageTypes.Original] self.display_type.set(self.display_options[0]) self.display_menu = CTkOptionMenu(self, variable=self.display_type, values=self.display_options) self.display_menu.grid(column=0, row=0, sticky=tk.N) - width = graxpert.images.get("Original").img_display.width - height = graxpert.images.get("Original").img_display.height + width = graxpert.images.get(ImageTypes.Original).img_display.width + height = graxpert.images.get(ImageTypes.Original).img_display.height self.zoom_fit(width, height) self.redraw_image() @@ -249,7 +278,7 @@ def on_load_image_error(self, event=None): def on_mouse_down_left(self, event=None): self.left_drag_timer = -1 - if graxpert.images.get("Original") is None: + if graxpert.images.get(ImageTypes.Original) is None: return self.clicked_inside_pt = False @@ -289,7 +318,7 @@ def on_mouse_down_left(self, event=None): self.__old_event = event def on_mouse_down_right(self, event=None): - if graxpert.images.get("Original") is None or not graxpert.prefs.display_pts: + if graxpert.images.get(ImageTypes.Original) is None or not graxpert.prefs.display_pts: return graxpert.remove_pt(event) @@ -300,7 +329,7 @@ def on_mouse_move(self, event=None): eventbus.emit(UiEvents.MOUSE_MOVED, {"mouse_event": event}) def on_mouse_move_left(self, event=None): - if graxpert.images.get("Original") is None: + if graxpert.images.get(ImageTypes.Original) is None: return if graxpert.images.get(graxpert.display_type) is None: @@ -342,7 +371,7 @@ def on_mouse_move_left(self, event=None): return def on_mouse_release_left(self, event=None): - if graxpert.images.get("Original") is None or not graxpert.prefs.display_pts: + if graxpert.images.get(ImageTypes.Original) is None or not graxpert.prefs.display_pts: return if self.clicked_inside_pt and not self.crop_mode: @@ -364,7 +393,7 @@ def on_mouse_release_left(self, event=None): tol=graxpert.prefs.bg_tol_option, bg_pts=graxpert.prefs.bg_pts_option, sample_size=graxpert.prefs.sample_size, - image=graxpert.images.get("Original"), + image=graxpert.images.get(ImageTypes.Original), ) graxpert.cmd.execute() @@ -403,36 +432,35 @@ def on_stretch_image_end(self, event=None): def on_stretch_image_error(self, event=None): self.show_loading_frame(False) - + def on_turn_on_crop_mode(self, event=None): if self.crop_mode: return - - if graxpert.images.get("Original") is None: + + if graxpert.images.get(ImageTypes.Original) is None: messagebox.showerror("Error", _("Please load your picture first.")) return self.startx = 0 self.starty = 0 - self.endx = graxpert.images.get("Original").width - self.endy = graxpert.images.get("Original").height - + self.endx = graxpert.images.get(ImageTypes.Original).width + self.endy = graxpert.images.get(ImageTypes.Original).height + self.crop_mode = True self.redraw_points() - + def on_turn_off_crop_mode(self, event=None): if not self.crop_mode: return - + self.startx = 0 self.starty = 0 - self.endx = graxpert.images.get("Original").width - self.endy = graxpert.images.get("Original").height - + self.endx = graxpert.images.get(ImageTypes.Original).width + self.endy = graxpert.images.get(ImageTypes.Original).height + self.crop_mode = False self.redraw_points() - # widget logic def draw_image(self, pil_image, tags=None): if pil_image is None: @@ -460,7 +488,7 @@ def redraw_image(self, event=None): self.draw_image(graxpert.images.get(self.display_type.get()).img_display_saturated) def redraw_points(self, event=None): - if graxpert.images.get("Original") is None: + if graxpert.images.get(ImageTypes.Original) is None: return color = hls_to_rgb(graxpert.prefs.sample_color / 360, 0.5, 1.0) diff --git a/graxpert/ui/left_menu.py b/graxpert/ui/left_menu.py index 9fe3035..b7a503a 100644 --- a/graxpert/ui/left_menu.py +++ b/graxpert/ui/left_menu.py @@ -217,9 +217,87 @@ def toggle(self): eventbus.emit(UiEvents.SHOW_MENU_REQUEST, "BGE") +class DeconvolutionMenu(CollapsibleMenuFrame): + def __init__(self, parent, **kwargs): + super().__init__(parent, title=_("Deconvolution"), show=False, number=4, **kwargs) + + # method selection + self.deconvolution_options = ["Object-only"] # , "Stars-only" + self.deconvolution_type = tk.StringVar() + self.deconvolution_type.set(graxpert.prefs.deconvolution_type_option) + self.deconvolution_type.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.DECONVOLUTION_TYPE_CHANGED, {"deconvolution_type_option": self.deconvolution_type.get()})) + + self.deconvolution_strength = tk.DoubleVar() + self.deconvolution_strength.set(graxpert.prefs.deconvolution_strength) + self.deconvolution_strength.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.DECONVOLUTION_STRENGTH_CHANGED, {"deconvolution_strength": self.deconvolution_strength.get()})) + + self.deconvolution_psfsize = tk.DoubleVar() + self.deconvolution_psfsize.set(graxpert.prefs.deconvolution_psfsize) + self.deconvolution_psfsize.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.DECONVOLUTION_PSFSIZE_CHANGED, {"deconvolution_psfsize": self.deconvolution_psfsize.get()})) + + self.create_children() + self.setup_layout() + self.place_children() + + eventbus.add_listener(UiEvents.SHOW_MENU_REQUEST, lambda e: self.hide() if not e == "DECONVOLUTION" else None) + + def create_children(self): + super().create_children() + + # method selection + self.deconvolution_type_title = ProcessingStep(self.sub_frame, number=0, title=_("Deconvolution Method:")) + self.deconvolution_menu = GraXpertOptionMenu(self.sub_frame, variable=self.deconvolution_type, values=self.deconvolution_options) + tooltip.Tooltip(self.deconvolution_menu, text=tooltip.deconvolution_type_text) + + self.deconvolution_button = GraXpertButton( + self.sub_frame, + text=_("Deconvolve Image"), + fg_color=ThemeManager.theme["Accent.CTkButton"]["fg_color"], + hover_color=ThemeManager.theme["Accent.CTkButton"]["hover_color"], + command=lambda: eventbus.emit(AppEvents.DECONVOLUTION_REQUEST), + ) + self.tt_load = tooltip.Tooltip(self.deconvolution_button, text=tooltip.deconvolution_text) + + self.deconvolution_strength_slider = ValueSlider( + self.sub_frame, width=default_label_width, variable_name=_("Deconvolution Strength"), variable=self.deconvolution_strength, min_value=0.0, max_value=1.0, precision=1 + ) + tooltip.Tooltip(self.deconvolution_strength_slider, text=tooltip.deconvolution_strength_text) + + self.deconvolution_psfsize_slider = ValueSlider( + self.sub_frame, width=default_label_width, variable_name=_("Image FHWM (in pixels)"), variable=self.deconvolution_psfsize, min_value=0.0, max_value=14.0, precision=1 + ) + tooltip.Tooltip(self.deconvolution_psfsize_slider, text=tooltip.deconvolution_psfsize_text) + + def setup_layout(self): + super().setup_layout() + + def place_children(self): + super().place_children() + + row = -1 + + def next_row(): + nonlocal row + row += 1 + return row + + # method selection + self.deconvolution_type_title.grid(column=1, row=next_row(), pady=pady, sticky=tk.EW) + self.deconvolution_menu.grid(column=1, row=next_row(), pady=pady, sticky=tk.EW) + + self.deconvolution_strength_slider.grid(column=1, row=next_row(), pady=pady, sticky=tk.EW) + self.deconvolution_psfsize_slider.grid(column=1, row=next_row(), pady=pady, sticky=tk.EW) + self.deconvolution_button.grid(column=1, row=next_row(), pady=pady, sticky=tk.EW) + + def toggle(self): + super().toggle() + if self.show: + eventbus.emit(UiEvents.SHOW_MENU_REQUEST, "DECONVOLUTION") + + class DenoiseMenu(CollapsibleMenuFrame): def __init__(self, parent, **kwargs): - super().__init__(parent, title=_("Denoising"), show=False, number=4, **kwargs) + super().__init__(parent, title=_("Denoising"), show=False, number=5, **kwargs) self.denoise_strength = tk.DoubleVar() self.denoise_strength.set(graxpert.prefs.denoise_strength) @@ -265,13 +343,16 @@ def toggle(self): class SaveMenu(CollapsibleMenuFrame): def __init__(self, parent, **kwargs): - super().__init__(parent, title=_("Saving"), show=False, number=5, **kwargs) + super().__init__(parent, title=_("Saving"), show=False, number=6, **kwargs) # saving self.saveas_options = ["16 bit Tiff", "32 bit Tiff", "16 bit Fits", "32 bit Fits", "16 bit XISF", "32 bit XISF"] self.saveas_type = tk.StringVar() self.saveas_type.set(graxpert.prefs.saveas_option) self.saveas_type.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SAVE_AS_CHANGED, {"saveas_option": self.saveas_type.get()})) + self.saveas_stretched = tk.BooleanVar() + self.saveas_stretched.set(graxpert.prefs.saveas_stretched) + self.saveas_stretched.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.SAVE_STRETCHED_CHANGED, {"saveas_stretched": self.saveas_stretched.get()})) self.create_children() self.setup_layout() @@ -287,16 +368,14 @@ def create_children(self): tooltip.Tooltip(self.saveas_menu, text=tooltip.saveas_text) self.save_button = GraXpertButton( self.sub_frame, - text=_("Save Processed"), + text=_("Save Selected"), fg_color=ThemeManager.theme["Accent.CTkButton"]["fg_color"], hover_color=ThemeManager.theme["Accent.CTkButton"]["hover_color"], command=lambda: eventbus.emit(AppEvents.SAVE_REQUEST), ) tooltip.Tooltip(self.save_button, text=tooltip.save_pic_text) - self.save_background_button = GraXpertButton(self.sub_frame, text=_("Save Background"), command=lambda: eventbus.emit(AppEvents.SAVE_BACKGROUND_REQUEST)) - tooltip.Tooltip(self.save_background_button, text=tooltip.save_bg_text) - self.save_stretched_button = GraXpertButton(self.sub_frame, text=_("Save Stretched & Processed"), command=lambda: eventbus.emit(AppEvents.SAVE_STRETCHED_REQUEST)) - tooltip.Tooltip(self.save_stretched_button, text=tooltip.save_stretched_pic_text) + self.saveas_stretched_checkbox = GraXpertCheckbox(self.sub_frame, width=default_label_width, text=_("Save Stretched"), variable=self.saveas_stretched) + tooltip.Tooltip(self.saveas_stretched_checkbox, text=tooltip.saveas_stretched_text) def setup_layout(self): super().setup_layout() @@ -314,8 +393,7 @@ def next_row(): # saving self.saveas_menu.grid(column=1, row=next_row(), pady=pady, sticky=tk.EW) self.save_button.grid(column=1, row=next_row(), pady=pady, sticky=tk.EW) - self.save_background_button.grid(column=1, row=next_row(), pady=pady, sticky=tk.EW) - self.save_stretched_button.grid(column=1, row=next_row(), pady=pady, sticky=tk.EW) + self.saveas_stretched_checkbox.grid(column=1, row=next_row(), pady=pady, sticky=tk.EW) def toggle(self): super().toggle() @@ -334,6 +412,7 @@ def create_children(self): self.load_menu = LoadMenu(self, fg_color="transparent") self.crop_menu = CropMenu(self, fg_color="transparent") self.extraction_menu = ExtractionMenu(self, fg_color="transparent") + self.deconvolution_menu = DeconvolutionMenu(self, fg_color="transparent") self.denoise_menu = DenoiseMenu(self, fg_color="transparent") self.save_menu = SaveMenu(self, fg_color="transparent") @@ -353,5 +432,6 @@ def next_row(): self.load_menu.grid(column=0, row=next_row(), ipadx=padx, sticky=tk.N) self.crop_menu.grid(column=0, row=next_row(), ipadx=padx, sticky=tk.N) self.extraction_menu.grid(column=0, row=next_row(), ipadx=padx, sticky=tk.N) + self.deconvolution_menu.grid(column=0, row=next_row(), ipadx=padx, sticky=tk.N) self.denoise_menu.grid(column=0, row=next_row(), ipadx=padx, sticky=tk.N) self.save_menu.grid(column=0, row=next_row(), ipadx=padx, sticky=tk.N) diff --git a/graxpert/ui/right_menu.py b/graxpert/ui/right_menu.py index 27d8111..e9af259 100644 --- a/graxpert/ui/right_menu.py +++ b/graxpert/ui/right_menu.py @@ -7,13 +7,13 @@ from packaging import version from PIL import Image -from graxpert.ai_model_handling import bge_ai_models_dir, denoise_ai_models_dir, list_local_versions, list_remote_versions +from graxpert.ai_model_handling import bge_ai_models_dir, deconvolution_object_ai_models_dir, deconvolution_stars_ai_models_dir, denoise_ai_models_dir, list_local_versions, list_remote_versions from graxpert.application.app import graxpert from graxpert.application.app_events import AppEvents from graxpert.application.eventbus import eventbus from graxpert.localization import _, lang from graxpert.resource_utils import resource_path -from graxpert.s3_secrets import bge_bucket_name, denoise_bucket_name +from graxpert.s3_secrets import bge_bucket_name, deconvolution_object_bucket_name, deconvolution_stars_bucket_name, denoise_bucket_name from graxpert.ui.widgets import GraXpertOptionMenu, GraXpertScrollableFrame, ProcessingStep, ValueSlider, padx, pady @@ -103,6 +103,13 @@ def callback(url): url_label_2.grid(column=0, row=row, padx=padx, sticky=tk.E) url_label_2.bind("", lambda e: callback(url_link_2)) + row = self.nrow() + HelpText(self, rows=2, text=_("Deconvolution AI models are licensed under CC BY-NC-SA:")).grid(column=0, row=row, padx=padx, pady=pady, sticky=tk.W) + url_link_3 = "https://raw.githubusercontent.com/Steffenhir/GraXpert/main/licenses/Deconvolution-Model-LICENSE.html" + url_label_3 = CTkLabel(self, text="", text_color="dodger blue") + url_label_3.grid(column=0, row=row, padx=padx, sticky=tk.E) + url_label_3.bind("", lambda e: callback(url_link_3)) + row = self.nrow() HelpText(self, rows=2, text=_("Denoising AI models are licensed under CC BY-NC-SA:")).grid(column=0, row=row, padx=padx, pady=pady, sticky=tk.W) url_link_3 = "https://raw.githubusercontent.com/Steffenhir/GraXpert/main/licenses/Denoise-Model-LICENSE.html" @@ -170,6 +177,42 @@ def __init__(self, master, **kwargs): self.bge_ai_options.insert(0, "None") self.bge_ai_version.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.BGE_AI_VERSION_CHANGED, {"bge_ai_version": self.bge_ai_version.get()})) + # object deconvolution ai models + deconvolution_object_ai_remote_versions = list_remote_versions(deconvolution_object_bucket_name) + deconvolution_object_ai_local_versions = list_local_versions(deconvolution_object_ai_models_dir) + self.deconvolution_object_ai_options = set([]) + self.deconvolution_object_ai_options.update([rv["version"] for rv in deconvolution_object_ai_remote_versions]) + self.deconvolution_object_ai_options.update([lv["version"] for lv in deconvolution_object_ai_local_versions]) + self.deconvolution_object_ai_options = sorted(self.deconvolution_object_ai_options, key=lambda k: version.parse(k), reverse=True) + + self.deconvolution_object_ai_version = tk.StringVar(master) + self.deconvolution_object_ai_version.set("None") # default value + if graxpert.prefs.deconvolution_object_ai_version is not None: + self.deconvolution_object_ai_version.set(graxpert.prefs.deconvolution_object_ai_version) + else: + self.deconvolution_object_ai_options.insert(0, "None") + self.deconvolution_object_ai_version.trace_add( + "write", lambda a, b, c: eventbus.emit(AppEvents.DECONVOLUTION_OBJECT_AI_VERSION_CHANGED, {"deconvolution_object_ai_version": self.deconvolution_object_ai_version.get()}) + ) + + # stars deconvolution ai models + # deconvolution_stars_ai_remote_versions = list_remote_versions(deconvolution_stars_bucket_name) + # deconvolution_stars_ai_local_versions = list_local_versions(deconvolution_stars_ai_models_dir) + # self.deconvolution_stars_ai_options = set([]) + # self.deconvolution_stars_ai_options.update([rv["version"] for rv in deconvolution_stars_ai_remote_versions]) + # self.deconvolution_stars_ai_options.update([lv["version"] for lv in deconvolution_stars_ai_local_versions]) + # self.deconvolution_stars_ai_options = sorted(self.deconvolution_stars_ai_options, key=lambda k: version.parse(k), reverse=True) + + # self.deconvolution_stars_ai_version = tk.StringVar(master) + # self.deconvolution_stars_ai_version.set("None") # default value + # if graxpert.prefs.deconvolution_stars_ai_version is not None: + # self.deconvolution_stars_ai_version.set(graxpert.prefs.deconvolution_stars_ai_version) + # else: + # self.deconvolution_stars_ai_options.insert(0, "None") + # self.deconvolution_stars_ai_version.trace_add( + # "write", lambda a, b, c: eventbus.emit(AppEvents.DECONVOLUTION_STARS_AI_VERSION_CHANGED, {"deconvolution_stars_ai_version": self.deconvolution_stars_ai_version.get()}) + # ) + # denoise ai model denoise_remote_versions = list_remote_versions(denoise_bucket_name) denoise_local_versions = list_local_versions(denoise_ai_models_dir) @@ -239,6 +282,14 @@ def lang_change(lang): CTkLabel(self, text=_("Background Extraction AI-Model"), font=self.heading_font2).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) GraXpertOptionMenu(self, variable=self.bge_ai_version, values=self.bge_ai_options).grid(**self.default_grid()) + # object-deconvolution ai model + CTkLabel(self, text=_("Object Deconvolution AI-Model"), font=self.heading_font2).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + GraXpertOptionMenu(self, variable=self.deconvolution_object_ai_version, values=self.deconvolution_object_ai_options).grid(**self.default_grid()) + + # stars-deconvolution ai model + # CTkLabel(self, text=_("Stars Deconvolution AI-Model"), font=self.heading_font2).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) + # GraXpertOptionMenu(self, variable=self.deconvolution_stars_ai_version, values=self.deconvolution_stars_ai_options).grid(**self.default_grid()) + # denoise ai model CTkLabel(self, text=_("Denoising AI-Model"), font=self.heading_font2).grid(column=0, row=self.nrow(), pady=pady, sticky=tk.N) GraXpertOptionMenu(self, variable=self.denoise_ai_version, values=self.denoise_ai_options).grid(**self.default_grid()) diff --git a/graxpert/ui/statusbar.py b/graxpert/ui/statusbar.py index 1344806..d191bc5 100644 --- a/graxpert/ui/statusbar.py +++ b/graxpert/ui/statusbar.py @@ -2,18 +2,20 @@ from customtkinter import CTkFrame, CTkLabel, StringVar +import graxpert.ui.tooltip as tooltip from graxpert.application.app import graxpert from graxpert.application.app_events import AppEvents from graxpert.application.eventbus import eventbus -from graxpert.ui.ui_events import UiEvents +from graxpert.AstroImageRepository import ImageTypes from graxpert.localization import _ -import graxpert.ui.tooltip as tooltip +from graxpert.ui.ui_events import UiEvents from graxpert.ui.widgets import GraXpertCheckbox, GraXpertOptionMenu, ProcessingStep, ValueSlider, default_label_width, padx, pady + class StatusBar(CTkFrame): def __init__(self, master, **kwargs): super().__init__(master, **kwargs) - + self.stretch_options = ["No Stretch", "10% Bg, 3 sigma", "15% Bg, 3 sigma", "20% Bg, 3 sigma", "30% Bg, 2 sigma"] self.stretch_option_current = StringVar() self.stretch_option_current.set(graxpert.prefs.stretch_option) @@ -26,8 +28,7 @@ def __init__(self, master, **kwargs): self.channels_linked = tk.BooleanVar() self.channels_linked.set(graxpert.prefs.channels_linked_option) self.channels_linked.trace_add("write", lambda a, b, c: eventbus.emit(AppEvents.CHANNELS_LINKED_CHANGED, {"channels_linked": self.channels_linked.get()})) - - + self.create_children() self.setup_layout() self.place_children() @@ -37,9 +38,9 @@ def __init__(self, master, **kwargs): def create_children(self): self.label_image_info = CTkLabel(self, text="image info") self.label_image_pixel = CTkLabel(self, text="(x, y)") - + self.stretch_option_frame = CTkFrame(self) - + self.stretch_options_title = ProcessingStep(self.stretch_option_frame, number=0, indent=2, title=_(" Stretch Options")) self.stretch_menu = GraXpertOptionMenu( self.stretch_option_frame, @@ -57,19 +58,16 @@ def create_children(self): precision=1, ) self.channels_linked_switch = GraXpertCheckbox(self.stretch_option_frame, width=default_label_width, text=_("Channels linked"), variable=self.channels_linked) - def setup_layout(self): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - - def place_children(self): self.stretch_option_frame.grid(column=0, row=0, sticky=tk.NS) self.label_image_info.grid(column=0, row=0, padx=padx, sticky=tk.W) self.label_image_pixel.grid(column=0, row=0, padx=padx, sticky=tk.E) - + self.stretch_menu.grid(column=0, row=0, padx=padx, pady=pady, sticky=tk.E) self.saturation_slider.grid(column=1, row=0, padx=padx, pady=pady, sticky=tk.EW) self.channels_linked_switch.grid(column=2, row=0, padx=padx, pady=pady, sticky=tk.W) @@ -81,7 +79,7 @@ def register_events(self): # event handling def on_load_image_end(self, event): self.label_image_info.configure( - text=f'{graxpert.data_type} : {graxpert.images.get("Original").img_display.width} x {graxpert.images.get("Original").img_display.height} {graxpert.images.get("Original").img_display.mode}' + text=f"{graxpert.data_type} : {graxpert.images.get(ImageTypes.Original).img_display.width} x {graxpert.images.get(ImageTypes.Original).img_display.height} {graxpert.images.get(ImageTypes.Original).img_display.mode}" ) def on_mouse_move(self, event): diff --git a/graxpert/ui/tooltip.py b/graxpert/ui/tooltip.py index 4cb53f6..0506331 100644 --- a/graxpert/ui/tooltip.py +++ b/graxpert/ui/tooltip.py @@ -144,10 +144,10 @@ def hide(self): if tw: tw.destroy() self.tw = None - + def enable(self): self.enable_tt = True - + def disable(self): self.enable_tt = False self.hide() @@ -178,13 +178,17 @@ def disable(self): calculate_text = _("Use the specified interpolation method to calculate a background model " "and subtract it from the picture. This may take a while.") +deconvolution_type_text = _("Choose between different deconvolution methods.") +deconvolution_text = _("Use GraXpert's deconvolution AI model to reduce the blur in your image. This may take a while") +deconvolution_strength_text = _("Determines strength of deconvolution.") +deconvolution_psfsize_text = _("Informs the AI on how much blur to expect in the image. The right parameters is found when all artifacts disappear.") + denoise_text = _("Use GraXpert's denoising AI model to reduce the noise in your image. This may take a while") denoise_strength_text = _("Determines strength of denoising.") denoise_threshold_text = _("Determines the upper bound up to which pixels are denoised. Pixels above this threshold are not denoised and taken from the original image.") saveas_text = _("Choose the bitdepth of the saved pictures and the file format. " "If you are working with a .fits image the fits header will " "be preserved.") -save_bg_text = _("Save the background model") -save_pic_text = _("Save the processed picture") -save_stretched_pic_text = _("Save the stretched and processed picture. The color saturation is not changed.") +saveas_stretched_text = _("Enable to save the stretched image instead of the linear one. The color saturation is not changed.") +save_pic_text = _("Save the currently selected picture") display_text = _("Switch display between \n" "\n" "Original: Your original picture \n" "Processed: Picture with subtracted background model \n" "Background: The background model") diff --git a/licenses/Deconvolution-Model-LICENSE.html b/licenses/Deconvolution-Model-LICENSE.html new file mode 100644 index 0000000..09b7cce --- /dev/null +++ b/licenses/Deconvolution-Model-LICENSE.html @@ -0,0 +1,11 @@ + + + + +

The provided GraXpert Deconvolution Models by GraXpert Development Team are licensed under CC BY-NC-SA 4.0

+

We would like to thank the following people who contributed to this model by submitting training images:

+ + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8855640..59adc5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,11 +3,11 @@ astropy customtkinter minio ml_dtypes -numpy<=1.24.3,>=1.22 +numpy Pillow pykrige opencv-Python requests -scikit-image == 0.21.0 +scikit-image scipy xisf