From 2343be3c04872a9fa70b4c0cd8e00ba2d562eae9 Mon Sep 17 00:00:00 2001
From: p7ayfu77
Date: Sat, 28 Sep 2024 18:26:16 +0100
Subject: [PATCH] Preserve image histogram range, when loading, processing and
saving. Update cli interface to simplify and support PixInsight script
integration Add PyxInsight cli script
---
.gitignore | 3 +
astrodenoise/applayout.py | 26 ++-
astrodenoise/cli.py | 50 ++++--
astrodenoise/imagelayout.py | 219 ++++++++++++------------
astrodenoise/version.py | 2 +-
csbdeep/data/prepare.py | 17 +-
pixinsight/AstroDN.js | 42 +++++
pixinsight/AstroDNCLI.js | 326 ++++++++++++++++++++++++++++++++++++
pixinsight/AstroDNDialog.js | 313 ++++++++++++++++++++++++++++++++++
9 files changed, 857 insertions(+), 141 deletions(-)
create mode 100644 pixinsight/AstroDN.js
create mode 100644 pixinsight/AstroDNCLI.js
create mode 100644 pixinsight/AstroDNDialog.js
diff --git a/.gitignore b/.gitignore
index 40e8897..14048c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,6 @@ models/**
*.tif*
*.zip
*.tfevents.*
+pjsr/
+safe/
+updates/
\ No newline at end of file
diff --git a/astrodenoise/applayout.py b/astrodenoise/applayout.py
index c793af0..242466e 100644
--- a/astrodenoise/applayout.py
+++ b/astrodenoise/applayout.py
@@ -296,7 +296,7 @@ def save(self, path):
self.dismiss_popup()
filepath = Path(path)
- #AstroDeNoiseApp()
+
get_app(App.get_running_app()).lastpath = str(filepath.parent)
if self.fits_headers is None:
@@ -317,14 +317,19 @@ def save(self, path):
else:
result_tosave = np.array(self.currentimage.get_data('pre')[0])
+ # Clip to range [0,1] before save
+ result_tosave = np.clip(result_tosave, 0, 1)
+
try:
extension = filepath.suffix.lower()
if extension in supported_save_formats_fits:
- result_forsave = np.moveaxis(np.transpose(result_tosave),1,2)
- write_fits(filepath,result_forsave,headers=self.fits_headers)
+ write_fits(
+ filepath,
+ np.moveaxis(np.transpose(result_tosave),1,2),
+ headers=self.fits_headers)
elif extension in supported_save_formats_tiff:
- result_tosave = (result_tosave - np.min(result_tosave)) / (np.max(result_tosave) - np.min(result_tosave))
+ # Scale to unit16 for tiff
result_tosave = (result_tosave * np.iinfo(np.uint16).max).astype(np.uint16)
imsave(filepath,data=result_tosave)
else:
@@ -459,7 +464,11 @@ def denoise(self, data, C=-2.8,B=0.25):
self.update_progress(0)
expand_low_actual = 0.5 - (self.expand_low/2)
- normalizer = STFNormalizer(C=C,B=B,expand_low=expand_low_actual,do_after=False) if self.normalize_enabled else NoNormalizer(expand_low=expand_low_actual)
+ # Strength shifts pixel values to right of histogram by up to 0.5
+ #Strength=1 => Image + 0
+ #Strength=0 => Image + 0.5
+ normalizer = STFNormalizer(C=C,B=B,expand_low=expand_low_actual,do_after=False) if self.normalize_enabled else NoNormalizer(expand_low=expand_low_actual,do_after=False)
+
if self.denoise_enabled:
with tf.device(f"/{self.selected_device}:0"):
@@ -478,7 +487,7 @@ def denoise(self, data, C=-2.8,B=0.25):
result = normalizer.before(np.moveaxis(np.transpose(data),0,1),'YX')
self.update_progress(1)
- return result
+ return result - expand_low_actual
@mainthread
def update_progress(self,progress):
@@ -526,8 +535,9 @@ def get_label_data(self):
}
def get_texture(self, result):
- image = (result - np.min(result)) / (np.max(result) - np.min(result))
- image = (image * 255).astype('uint8')
+
+ result = np.clip(result, 0, 1)
+ image = (result * 255).astype('uint8')
colorfmt='rgb'
if image.shape[2] == 1:
diff --git a/astrodenoise/cli.py b/astrodenoise/cli.py
index 7613c46..9333036 100644
--- a/astrodenoise/cli.py
+++ b/astrodenoise/cli.py
@@ -1,7 +1,9 @@
import os
+import sys
import argparse
import tensorflow as tf
from tifffile import imread
+from xisf import XISF
import numpy as npp
from pathlib import Path
from os.path import join as path_join
@@ -11,6 +13,13 @@
from astrodeep.utils.fits import read_fits, write_fits
from astrodenoise.version import modelversion
+def get_exepath():
+ if getattr(sys, "frozen", False):
+ datadir = os.path.dirname(sys.executable)
+ else:
+ datadir = Path(os.path.dirname(__file__)).parent.as_posix()
+ return datadir
+
def cli():
parser = argparse.ArgumentParser()
@@ -18,13 +27,13 @@ def cli():
parser.add_argument('input', type=str, nargs=1, help='Input image path, either tif or debayered fits file with data stored as 32bit float.')
parser.add_argument('--model','-m', type=str, default=modelversion, help='Alternative model name to use for de-noising.')
parser.add_argument('--models_folder', type=str, default='models', help='Alternative models folder root path.')
- parser.add_argument('--tiles','-t', type=int, default=0, help='Use number of tiling slices when de-noising, useful for large images and limited memory.')
+ parser.add_argument('--tiles','-t', type=int, default=3, help='Use number of tiling slices when de-noising, useful for large images and limited memory.')
parser.add_argument('--overwrite','-o', action='store_true', help='Allow overwrite of existing output file. Default: False when not specified.')
parser.add_argument('--device','-d', choices=['GPU','CPU'], default='CPU', help='Optional select processing to target CPU or GCP. Default: CPU')
parser.add_argument('--normalize','-n', action='store_true', help='Enable STFNormalization before de-noising. Default: False when not specified.')
parser.add_argument('--norm-C', type=float, default=-2.8, help='C parameter for STF Normalization. Default: -2.8')
- parser.add_argument('--norm-B', type=float, default=0.25, help='B parameter for STF Normalization, Higher B results in stronger stretch providing the ability target de-noising more effectively. . Default: 0.25, Range: 0 < B < 1')
- parser.add_argument('--norm-restore', action='store_true', help='Restores output image to original data range after processing. Default: False when not specified.')
+ parser.add_argument('--norm-B', type=float, default=0.25, help='B parameter for STF Normalization, Higher B results in stronger stretch providing the ability target de-noising more effectively. . Default: 0.25, Range: 0 < B < 1')
+ parser.add_argument('--strength', type=float, default=0.5, help='The denoise strength applied. Default: 0.5')
args = parser.parse_args()
@@ -35,7 +44,9 @@ def predict(path,model):
if path.suffix in ['.fit','.fits']:
data, headers = read_fits(path)
elif path.suffix in ['.tif','.tiff']:
- data, headers = npp.moveaxis(imread(path),-1,0), None
+ data, headers = npp.moveaxis(imread(path),-1,0), None
+ elif path.suffix in ['.xisf']:
+ data, headers = npp.moveaxis(XISF(path).read_image(0),-1,0), None
else:
print("Skipping unsupported format. Allowed formats: .tiff/.tif/.fits/.fit")
return
@@ -46,33 +57,38 @@ def predict(path,model):
if data.ndim == 2:
data = data[npp.newaxis,...]
- print("Processing file:",path)
- print("Image Dimensions:",data.shape)
+ print(f"Processing file:{path}\n")
+ print(f"Image Dimensions: {data.shape}\n")
- n_tiles = None if args.tiles == 0 else (args.tiles,args.tiles)
+ n_tiles = None if args.tiles == 0 else (args.tiles, args.tiles)
if n_tiles is not None:
print("Processing with tilling:",n_tiles)
- output_denoised = []
+
axes = 'YX'
- normalizer = STFNormalizer(C=args.norm_C,B=args.norm_B,do_after=args.norm_restore) if args.normalize is True else NoNormalizer()
- print("Using Normalization:",normalizer.params)
+ expand_low_actual = 0.5 - (args.strength/2)
+ normalizer = STFNormalizer(C=args.norm_C,B=args.norm_B,expand_low=expand_low_actual,do_after=True) if args.normalize is True else NoNormalizer(expand_low=expand_low_actual,do_after=True)
+ print(f"Using Normalization: {normalizer.params}\n")
+ print(f"Using Strength: {args.strength}\n")
+ output_denoised = []
for c in data:
output_denoised.append(
- model.predict(c, axes, normalizer=normalizer,resizer=PadAndCropResizer(), n_tiles=n_tiles)
+ model.predict(c, axes, normalizer=normalizer, resizer=PadAndCropResizer(), n_tiles=n_tiles)
)
+ output_denoised_arr = npp.asarray(output_denoised)
+ # Clip to [0,1] range
+ output = output_denoised_arr.clip(0,1)
+
output_file_name = path.stem + f"_denoised.fits"
- output_path = path_join(path.parent, 'denoised')
- Path(output_path).mkdir(exist_ok=True)
- output_file_path = path_join(output_path, output_file_name)
- write_fits(output_file_path, output_denoised, headers, args.overwrite)
- print("Output file saved:", output_file_path)
+ output_file_path = path_join(path.parent, output_file_name)
+ write_fits(output_file_path, output, headers, args.overwrite)
+ print("Output file saved:", output_file_path)
print("Loading model:", args.model)
- model = CARE(config=None, name=args.model, basedir=args.models_folder)
+ model = CARE(config=None, name=args.model, basedir=Path(get_exepath()).joinpath(args.models_folder).as_posix())
file_or_path = args.input[0]
if os.path.isfile(file_or_path):
diff --git a/astrodenoise/imagelayout.py b/astrodenoise/imagelayout.py
index bcdffd3..23b53ed 100644
--- a/astrodenoise/imagelayout.py
+++ b/astrodenoise/imagelayout.py
@@ -2,7 +2,7 @@
from kivy.properties import ObjectProperty, NumericProperty
from kivy.graphics.texture import Texture
from kivy.uix.label import Label
-
+from kivy.input.motionevent import MotionEvent
class ImageViewLayout(AnchorLayout):
region_x = NumericProperty(0)
region_y = NumericProperty(0)
@@ -52,124 +52,33 @@ def on_touch_down(self, touch):
if self.imageout is None:
return True
-
+
if touch.is_mouse_scrolling:
- if touch.button == 'scrolldown':
- if self.scale < 10:
- prev_w = self.imageout.width / self.scale
- prev_h = self.imageout.height / self.scale
- self.scale *= 1.1
- self.region_w = self.imageout.width / self.scale
- self.region_h = self.imageout.height / self.scale
- self.region_x += (prev_w-self.region_w) // 2
- self.region_y += (prev_h-self.region_h) // 2
- elif touch.button == 'scrollup':
- if self.scale > 1:
- prev_w = self.imageout.width / self.scale
- prev_h = self.imageout.height / self.scale
- self.scale /= 1.1
- self.region_w = self.imageout.width / self.scale
- self.region_h = self.imageout.height / self.scale
-
- if (self.region_w > self.imageout.width) or (self.region_h > self.imageout.height):
- self.region_w = self.imageout.width
- self.region_h = self.imageout.height
-
- new_x = self.region_x + (prev_w-self.region_w) // 2
- new_y = self.region_y + (prev_h-self.region_h) // 2
-
- if (new_x + self.region_w) > self.imageout.width:
- self.region_x = self.imageout.width - self.region_w
- elif new_x < 0:
- self.region_x = 0
- else:
- self.region_x += (prev_w-self.region_w) // 2
-
- if (new_y + self.region_h) > self.imageout.height:
- self.region_y = self.imageout.height - self.region_h
- elif new_y < 0:
- self.region_y = 0
- else:
- self.region_y += (prev_h-self.region_h) // 2
- else:
- self.scale = 1
-
- self.displayimage.texture = self.imageout.get_region(self.region_x, self.region_y, self.region_w, self.region_h)
-
- else:
- touch.grab(self)
-
- if touch.button == 'right' \
+ self.render_scroll_zoom(touch)
+ elif touch.button == 'right' \
and self.imageorig is not None:
self.setlabels()
self.showlabels(True)
-
+ self.render_slider_preview(touch)
+ touch.grab(self)
+ elif touch.button == 'left':
+ touch.grab(self)
+
return True
def on_touch_move(self, touch):
if not self.collide_point(*touch.pos):
return super().on_touch_move(touch)
- if self.imageout is None:
+ if self.imageout is None \
+ or touch.grab_current is not self:
return True
- if touch.grab_current is self \
- and touch.button == 'left':
-
- imx, imy = self.displayimage.get_norm_image_size()
- deltax = -touch.dx * (self.region_w/imx)
- deltay = -touch.dy * (self.region_h/imy)
-
- new_x = self.region_x + deltax
- new_y = self.region_y + deltay
- if (new_x >= 0) and (new_x + self.region_w <= self.imageout.width):
- self.region_x += deltax
-
- if (new_y >= 0) and (new_y + self.region_h <= self.imageout.height):
- self.region_y += deltay
-
- self.displayimage.texture = self.imageout.get_region(self.region_x, self.region_y, self.region_w, self.region_h)
-
- #https://stackoverflow.com/questions/74543030/get-location-of-pixel-upon-click-in-kivy
- if touch.grab_current is self \
- and touch.button == 'right' \
+ if touch.button == 'left':
+ self.render_pan(touch)
+ elif touch.button == 'right' \
and self.imageorig is not None:
- #touch.sync_with_dispatch = True
- childImageNormImageSize_x = self.displayimage.norm_image_size[0]
- #childImageNormImageSize_y = childImage.norm_image_size[1]
- lr_space = (self.width - childImageNormImageSize_x) / 2 # empty space in Image widget left and right of actual image
- #tb_space = (self.height - childImageNormImageSize_y) / 2 # empty space in Image widget above and below actual image
-
- pixel_x = touch.x - lr_space - self.x # x coordinate of touch measured from lower left of actual image
- #pixel_y = touch.y - tb_space - self.y # y coordinate of touch measured from lower left of actual image
-
- if pixel_x > 0 and pixel_x < childImageNormImageSize_x:
- #clicked inside image, coords: pixel_x, pixel_y
-
- image_x = int(pixel_x * self.region_w / childImageNormImageSize_x)
- #image_y = pixel_y * self.region_h / childImageNormImageSize_y
-
- if image_x > 0 and image_x < self.region_w:
- mixtexture = Texture.create(size=(self.region_w, self.region_h), colorfmt=self.imageout.colorfmt)
-
- mixtexture.blit_buffer(
- self.imageout.get_region(self.region_x + image_x, self.region_y, self.region_w - image_x, self.region_h).pixels,
- pos=(image_x, 0),
- size=(self.region_w - image_x, self.region_h),
- bufferfmt='ubyte',
- colorfmt='rgba')
-
- mixtexture.blit_buffer(
- self.imageorig.get_region(self.region_x, self.region_y, image_x, self.region_h).pixels,
- pos=(0,0),
- size=(image_x, self.region_h),
- bufferfmt='ubyte',
- colorfmt='rgba')
-
- mixtexture.flip_vertical()
- mixtexture.mag_filter = 'linear'
- mixtexture.min_filter = 'linear'
- self.displayimage.texture = mixtexture
+ self.render_slider_preview(touch)
return True
@@ -185,3 +94,101 @@ def on_touch_up(self, touch):
self.displayimage.texture = self.imageout.get_region(self.region_x, self.region_y, self.region_w, self.region_h)
return True
+
+ def render_pan(self, touch):
+ imx, imy = self.displayimage.get_norm_image_size()
+ deltax = -1 * touch.dx * (self.region_w/imx)
+ deltay = -1 * touch.dy * (self.region_h/imy)
+
+ new_x = self.region_x + deltax
+ new_y = self.region_y + deltay
+ if (new_x >= 0) and (new_x + self.region_w <= self.imageout.width):
+ self.region_x += deltax
+
+ if (new_y >= 0) and (new_y + self.region_h <= self.imageout.height):
+ self.region_y += deltay
+
+ self.displayimage.texture = self.imageout.get_region(self.region_x, self.region_y, self.region_w, self.region_h)
+
+ def render_scroll_zoom(self, touch: MotionEvent):
+
+ if touch.button == 'scrolldown':
+ if self.scale < 10:
+ prev_w = self.imageout.width / self.scale
+ prev_h = self.imageout.height / self.scale
+ self.scale *= 1.1
+ self.region_w = self.imageout.width / self.scale
+ self.region_h = self.imageout.height / self.scale
+ self.region_x += (prev_w-self.region_w) // 2
+ self.region_y += (prev_h-self.region_h) // 2
+ elif touch.button == 'scrollup':
+ if self.scale > 1:
+ prev_w = self.imageout.width / self.scale
+ prev_h = self.imageout.height / self.scale
+ self.scale /= 1.1
+ self.region_w = self.imageout.width / self.scale
+ self.region_h = self.imageout.height / self.scale
+
+ if (self.region_w > self.imageout.width) or (self.region_h > self.imageout.height):
+ self.region_w = self.imageout.width
+ self.region_h = self.imageout.height
+
+ new_x = self.region_x + (prev_w-self.region_w) // 2
+ new_y = self.region_y + (prev_h-self.region_h) // 2
+
+ if (new_x + self.region_w) > self.imageout.width:
+ self.region_x = self.imageout.width - self.region_w
+ elif new_x < 0:
+ self.region_x = 0
+ else:
+ self.region_x += (prev_w-self.region_w) // 2
+
+ if (new_y + self.region_h) > self.imageout.height:
+ self.region_y = self.imageout.height - self.region_h
+ elif new_y < 0:
+ self.region_y = 0
+ else:
+ self.region_y += (prev_h-self.region_h) // 2
+ else:
+ self.scale = 1
+
+ self.displayimage.texture = self.imageout.get_region(self.region_x, self.region_y, self.region_w, self.region_h)
+
+ def render_slider_preview(self, touch: MotionEvent):
+ #https://stackoverflow.com/questions/74543030/get-location-of-pixel-upon-click-in-kivy
+
+ childImageNormImageSize_x = self.displayimage.norm_image_size[0]
+ #childImageNormImageSize_y = childImage.norm_image_size[1]
+ lr_space = (self.width - childImageNormImageSize_x) / 2 # empty space in Image widget left and right of actual image
+ #tb_space = (self.height - childImageNormImageSize_y) / 2 # empty space in Image widget above and below actual image
+
+ pixel_x = touch.x - lr_space - self.x # x coordinate of touch measured from lower left of actual image
+ #pixel_y = touch.y - tb_space - self.y # y coordinate of touch measured from lower left of actual image
+
+ if pixel_x > 0 and pixel_x < childImageNormImageSize_x:
+ #clicked inside image, coords: pixel_x, pixel_y
+
+ image_x = int(pixel_x * self.region_w / childImageNormImageSize_x)
+ #image_y = pixel_y * self.region_h / childImageNormImageSize_y
+
+ if image_x <= 0 or image_x > self.region_w - 1:
+ return
+
+ mixtexture = Texture.create(size=(self.region_w, self.region_h), colorfmt=self.imageout.colorfmt)
+ mixtexture.blit_buffer(
+ self.imageout.get_region(self.region_x + image_x, self.region_y, self.region_w - image_x, self.region_h).pixels,
+ pos=(image_x, 0),
+ size=(self.region_w - image_x, self.region_h),
+ bufferfmt='ubyte',
+ colorfmt='rgba')
+ mixtexture.blit_buffer(
+ self.imageorig.get_region(self.region_x, self.region_y, image_x, self.region_h).pixels,
+ pos=(0,0),
+ size=(image_x, self.region_h),
+ bufferfmt='ubyte',
+ colorfmt='rgba')
+ mixtexture.flip_vertical()
+ mixtexture.mag_filter = 'linear'
+ mixtexture.min_filter = 'linear'
+ self.displayimage.texture = mixtexture
+
diff --git a/astrodenoise/version.py b/astrodenoise/version.py
index 2c0a5a4..ece9371 100644
--- a/astrodenoise/version.py
+++ b/astrodenoise/version.py
@@ -1,2 +1,2 @@
-version = '0.5.2'
+version = '0.5.8'
modelversion = 'dist/v0.4.0-01'
\ No newline at end of file
diff --git a/csbdeep/data/prepare.py b/csbdeep/data/prepare.py
index cf949cb..5400c66 100644
--- a/csbdeep/data/prepare.py
+++ b/csbdeep/data/prepare.py
@@ -85,7 +85,9 @@ def before(self, x, axes):
def after(self, mean, scale, axes):
self.do_after or _raise(ValueError())
- return mean, scale
+
+ x = mean - self.expand_low
+ return x, scale
@property
def do_after(self):
@@ -196,16 +198,13 @@ def before(self, x, axes):
_x, self.m, self.c = STFPreProcessor.stf(x, self.C, self.B, axis)
return _x + self.expand_low
- def norm(self,data):
- return (data - np.min(data)) / (np.max(data) - np.min(data))
-
def after(self, mean, scale, axes):
- self.do_after or _raise(ValueError())
+ self.do_after or _raise(ValueError())
- # Mean requires normalising to [0,1] range else produces harsh clipping
- mean = self.norm(mean)
-
- x_ = STFPreProcessor.rev_stf(mean,self.m,self.c)
+ x_ = mean - self.expand_low
+ # Clip mean to [0,1] range
+ x_ = np.clip(x_,0,1)
+ x_ = STFPreProcessor.rev_stf(x_,self.m,self.c)
return x_, scale
diff --git a/pixinsight/AstroDN.js b/pixinsight/AstroDN.js
new file mode 100644
index 0000000..85b034b
--- /dev/null
+++ b/pixinsight/AstroDN.js
@@ -0,0 +1,42 @@
+#feature-id AstroDenoise : Toolbox > AstroDenoise
+#feature-info A script to run AstroDenoise from within PixInsight.
+
+#include "AstroDNCLI.js"
+#include "AstroDNDialog.js"
+
+function main() {
+ console.hide();
+
+ if (Parameters.isViewTarget) {
+ var targetView = Parameters.targetView
+ }
+ else {
+ var targetView = ImageWindow.activeWindow.currentView;
+ }
+
+ if ( !targetView || !targetView.id ) {
+ let mb = new MessageBox(
+ "No valid view is selected.
",
+ TITLE,
+ StdIcon_NoIcon,
+ StdButton_Ok
+ );
+ mb.execute()
+ return
+ }
+
+ astrodnParameters.init();
+
+ let astroDenoiseCLI = new AstroDenoiseCLI();
+ astroDenoiseCLI.targetView = targetView;
+
+ if (Parameters.isViewTarget) {
+ astroDenoiseCLI.process();
+ }
+ else {
+ let dialog = new AstroDNDialog(astroDenoiseCLI);
+ dialog.execute();
+ };
+}
+
+main();
\ No newline at end of file
diff --git a/pixinsight/AstroDNCLI.js b/pixinsight/AstroDNCLI.js
new file mode 100644
index 0000000..c5429d3
--- /dev/null
+++ b/pixinsight/AstroDNCLI.js
@@ -0,0 +1,326 @@
+//https://pixinsight.com/forum/index.php?threads/scripting-documentation.10086/
+
+#include
+#include <../src/scripts/AdP/WCSmetadata.jsh>
+
+#ifeq __PI_PLATFORM__ MACOSX
+#define ASTRODNSCRPT_DIR File.homeDirectory + "/Library/Application Support/AstroDNScript"
+#endif
+#ifeq __PI_PLATFORM__ MSWINDOWS
+#define ASTRODNSCRPT_DIR File.homeDirectory + "/AppData/Local/AstroDNScript"
+#endif
+#ifeq __PI_PLATFORM__ LINUX
+#define ASTRODNSCRPT_DIR File.homeDirectory + "/.local/share/AstroDNScript"
+#endif
+
+#define SCRIPT_CONFIG ASTRODNSCRPT_DIR + "/AstroDNScript.json"
+
+let astrodnParameters = {
+ defaults: function() {
+ return {
+ model: "dist/v0.4.0-01",
+ strength: 0.5,
+ replaceTarget: true,
+ device: "GPU",
+ tiles: 3,
+ stf: false,
+ stfC: -2.8,
+ stfB: 0.25
+ }
+ },
+
+ isstf: function() {
+ return astrodnParameters.stf;
+ },
+
+ saveToInstance: function() {
+ Parameters.set("model", astrodnParameters.model);
+ Parameters.set("strength", astrodnParameters.strength);
+ Parameters.set("replaceTarget", astrodnParameters.replaceTarget);
+ Parameters.set("device", astrodnParameters.device);
+ Parameters.set("tiles", astrodnParameters.tiles);
+ Parameters.set("stf", astrodnParameters.stf);
+ Parameters.set("stfC", astrodnParameters.stfC);
+ Parameters.set("stfB", astrodnParameters.stfB);
+ },
+
+ loadFromInstance: function() {
+ if (Parameters.has("model"))
+ astrodnParameters.model = Parameters.getString("model");
+ if (Parameters.has("strength"))
+ astrodnParameters.strength = Parameters.getReal("strength");
+ if (Parameters.has("replaceTarget"))
+ astrodnParameters.replaceTarget = Parameters.getBoolean("replaceTarget");
+ if (Parameters.has("device"))
+ astrodnParameters.device = Parameters.getString("device");
+ if (Parameters.has("tiles"))
+ astrodnParameters.tiles = Parameters.getInteger("tiles");
+ if (Parameters.has("stf"))
+ astrodnParameters.stf = Parameters.getBoolean("stf");
+ if (Parameters.has("stfC"))
+ astrodnParameters.stfC = Parameters.getReal("stfC");
+ if (Parameters.has("stfB"))
+ astrodnParameters.stfB = Parameters.getReal("stfB");
+ },
+
+ nullishcoales: function(a,b) {
+ return (a !== null && a !== undefined) ? a : b;
+ },
+
+ loadFromFile: function() {
+
+ var params = undefined
+ if (File.exists(SCRIPT_CONFIG)) {
+ try {
+ params = JSON.parse(File.readTextFile(SCRIPT_CONFIG));
+ } catch (error) {
+ Console.warningln("Loading AstroDN script settings failed...");
+ Console.warningln(error);
+ }
+ }
+
+ let defaults = astrodnParameters.defaults();
+ // set default params
+ if ( params == undefined ) {
+ params = defaults;
+ }
+
+ astrodnParameters.replaceTarget = this.nullishcoales(params.replaceTarget,defaults.replaceTarget);
+ astrodnParameters.strength = this.nullishcoales(params.strength,defaults.strength)
+ astrodnParameters.model = this.nullishcoales(params.model,defaults.model);
+ astrodnParameters.device = this.nullishcoales(params.device,defaults.device);
+ astrodnParameters.tiles = this.nullishcoales(params.tiles,defaults.tiles);
+ astrodnParameters.stf = this.nullishcoales(params.stf,defaults.stf);
+ astrodnParameters.stfC = this.nullishcoales(params.stfC,defaults.stfC);
+ astrodnParameters.stfB = this.nullishcoales(params.stfB,defaults.stfB);
+ },
+
+ saveToFile: function() {
+ File.writeTextFile(SCRIPT_CONFIG, JSON.stringify(astrodnParameters));
+ },
+
+ init: function() {
+
+ if (!File.directoryExists(ASTRODNSCRPT_DIR)) {
+ File.createDirectory(ASTRODNSCRPT_DIR, true);
+ }
+
+ astrodnParameters.loadFromFile();
+
+ astrodnParameters.loadFromInstance();
+ },
+
+ reset: function() {
+ if ( File.exists(SCRIPT_CONFIG) ) {
+ File.remove(SCRIPT_CONFIG);
+ };
+
+ // load preferences
+ astrodnParameters.loadFromFile();
+ }
+};
+
+function AstroDenoiseCLI() {
+
+ function executeCLICommand(cmd) {
+
+ Console.writeln("Executing CLI command " + cmd);
+
+ let noError = true;
+
+ this.process = new ExternalProcess;
+ this.process.onStarted = function() {
+ Console.noteln('Starting AstroDenoise...');
+ };
+ this.process.onError = function(code) {
+ Console.criticalln('ERROR: ' + code);
+ };
+ this.process.onFinished = function() {
+ Console.noteln('AstroDenoise completed.');
+ }
+
+ this.process.onStandardOutputDataAvailable = function() {
+ Console.writeln(this.stdout.toString());
+ };
+
+ this.process.onStandardErrorDataAvailable = function() {
+ Console.criticalln('AstroDenoise Error: ' + this.stderr.toString());
+ };
+
+ try {
+
+ this.process.start(cmd);
+ for ( ; this.process.isStarting; )
+ processEvents();
+ for ( ; this.process.isRunning; )
+ processEvents();
+
+ return true;
+ }
+ catch(error) {
+ Console.criticalln(error);
+ return false;
+ }
+ }
+
+ function getCLICommand(imagePath) {
+
+ imagePath = File.unixPathToWindows(imagePath);
+ //python -m D:\pydeep\astro-csbdeep\astrodenoise.main
+ //var cmdLine = '"AstroDenoise" ' +
+ var cmdLine = '"D:\\pydeep\\astro-csbdeep\\build\\AstroDenoise\\AstroDenoise" ' +
+ '"' + imagePath + '"';
+
+ if (astrodnParameters.strength != 0.5)
+ cmdLine += ' --strength=' + astrodnParameters.strength;
+
+ if (astrodnParameters.stf) {
+ cmdLine += ' --normalize';
+ cmdLine += ' --norm-C=' + astrodnParameters.stfC;
+ cmdLine += ' --norm-B=' + astrodnParameters.stfB;
+ }
+
+ cmdLine += ' --model=' + astrodnParameters.model;
+
+ cmdLine += ' --device=' + astrodnParameters.device;
+
+ cmdLine += ' --tiles=' + astrodnParameters.tiles;
+
+ return cmdLine;
+ }
+
+ function assign(view, toView) {
+ var P = new PixelMath;
+ P.expression = view.id;
+ P.useSingleExpression = true;
+ P.clearImageCacheAndExit = false;
+ P.cacheGeneratedImages = false;
+ P.generateOutput = true;
+ P.singleThreaded = false;
+ P.optimization = true;
+ P.use64BitWorkingImage = false;
+ P.createNewImage = false;
+ P.newImageColorSpace = PixelMath.prototype.SameAsTarget;
+ P.newImageSampleFormat = PixelMath.prototype.SameAsTarget;
+
+ P.executeOn(toView);
+ }
+
+ function cloneHidden(view, postfix, swapfile=true) {
+
+ // Pick an unused name for the imageId
+ var newId = null;
+ if (ImageWindow.windowById(view.id + postfix).isNull)
+ newId = view.id + postfix;
+ else {
+ for (var n = 1 ; n <= 99 ; n++) {
+ if (ImageWindow.windowById(view.id + postfix + n).isNull) {
+ newId = view.id + postfix + n;
+ break;
+ }
+ }
+ }
+ if (newId == null) {
+ (new MessageBox("Couldn't find a unique image name. Bailing out.",
+ TITLE, StdIcon_Error, StdButton_Ok)).execute();
+ return;
+ }
+
+ var P = new PixelMath;
+ P.expression = "$T";
+ P.useSingleExpression = true;
+ P.clearImageCacheAndExit = false;
+ P.cacheGeneratedImages = false;
+ P.generateOutput = true;
+ P.singleThreaded = false;
+ P.optimization = true;
+ P.use64BitWorkingImage = false;
+ P.rescale = false;
+ P.truncate = true;
+ P.createNewImage = true;
+ P.showNewImage = false;
+ P.newImageId = newId;
+ P.newImageWidth = 0;
+ P.newImageHeight = 0;
+ P.newImageAlpha = false;
+ P.newImageColorSpace = PixelMath.prototype.SameAsTarget;
+ P.newImageSampleFormat = PixelMath.prototype.SameAsTarget;
+ P.executeOn(view, swapfile);
+
+ return View.viewById(P.newImageId);
+ }
+
+ function copyImagePathtoView(imagePath, view) {
+ var windows = ImageWindow.open(imagePath);
+ if (windows != null && windows.length > 0) {
+ let firstWindow = windows[0];
+ assign(firstWindow.mainView, view);
+ firstWindow.forceClose();
+ }
+ }
+
+ function getTempFile() {
+ return File.systemTempDirectory + getFileSystemSeparator() + "astrodenoise_" + Math.round(Math.random()*10000)+ ".xisf";
+ }
+
+ function getFileSystemSeparator() {
+ return corePlatform == "Windows" ? "\\" : "\/";
+ }
+
+ this.process = function () {
+
+ console.show();
+
+ var imagePath = getTempFile();
+ Console.writeln('Temporary image file: ' + imagePath);
+ var tempView = cloneHidden(this.targetView, "_SaveTemp");
+ var saveResult = tempView.window.saveAs(imagePath, false, false, true, false)
+ tempView.window.forceClose();
+
+ if (!saveResult) {
+ Console.warningln("Could not write file " + imagePath + " required to call AstroDN!");
+ return;
+ }
+
+ if (executeCLICommand(getCLICommand(imagePath))) {
+
+ var processedPath = imagePath.replace(".xisf", "_denoised.fits");
+ processedPath = File.unixPathToWindows(processedPath);
+
+ try {
+ if (astrodnParameters.replaceTarget) {
+ copyImagePathtoView(processedPath, this.targetView)
+ }
+ else {
+ var newView = cloneHidden(this.targetView, "_AstroDN");
+ let metadata = new ImageMetadata("AstroDN");
+ metadata.ExtractMetadata(this.targetView.window);
+
+ copyImagePathtoView(processedPath, newView)
+
+ newView.window.keywords = this.targetView.window.keywords;
+ if (!metadata.projection || !metadata.ref_I_G) {
+ Console.writeln("The image " + newView.id + " has no astrometric solution");
+ }
+ else {
+ metadata.SaveKeywords( newView.window, false);
+ metadata.SaveProperties( newView.window, TITLE + " " + VERSION);
+ }
+ newView.window.show();
+ }
+ }
+ finally {
+ if (File.exists(processedPath))
+ File.remove(processedPath);
+ File.remove(imagePath);
+ console.hide();
+ }
+ }
+ else {
+ Console.criticalln("AstroDN failed!");
+ File.remove(imagePath);
+ console.show();
+ }
+ }
+}
+
diff --git a/pixinsight/AstroDNDialog.js b/pixinsight/AstroDNDialog.js
new file mode 100644
index 0000000..1616513
--- /dev/null
+++ b/pixinsight/AstroDNDialog.js
@@ -0,0 +1,313 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define VERSION "0.5.7"
+#define TITLE "AstroDenoise"
+#define TEXT "Remove noise from astrophotography images using AstroDenoise CLI. Please ensure script version matches AstroDenoise version!"
+
+#define FORMWIDTH 360
+
+function AstroDNDialog(cli) {
+ this.__base__ = Dialog
+ this.__base__();
+
+ this.userResizable = false;
+ this.scaledMinWidth = FORMWIDTH;
+
+ this.helpLabel = new Label(this);
+ this.helpLabel.frameStyle = FrameStyle_Box;
+ this.helpLabel.margin = 4;
+ this.helpLabel.wordWrapping = true;
+ this.helpLabel.useRichText = true;
+ this.helpLabel.text = "" + TITLE + " Script v"
+ + VERSION + " — "
+ + TEXT;
+
+ this.imageViewSelectorFrame = new Frame(this);
+ this.imageViewSelectLabel = new Label(this);
+ this.imageViewSelectLabel.text = "Image:";
+ this.imageViewSelectLabel.toolTip = "Select the image for denoising.
";
+ this.imageViewSelectLabel.textAlignment = TextAlign_Left | TextAlign_VertCenter;
+
+ this.imageViewSelector = new ViewList(this);
+ this.imageViewSelector.maxWidth = FORMWIDTH;
+ this.imageViewSelector.getMainViews();
+
+ with (this.imageViewSelectorFrame) {
+ sizer = new HorizontalSizer();
+
+ with (sizer) {
+ margin = 6;
+ add(this.imageViewSelectLabel);
+ addSpacing(8);
+ add(this.imageViewSelector);
+ adjustToContents();
+ }
+ }
+
+ this.imageViewSelector.onViewSelected = function (view) {
+ cli.targetView = view;
+ };
+
+ this.modelSelectionFrame = new Frame(this);
+ this.modelSelectionLabel = new Label(this);
+ this.modelSelectionLabel.text = "Model:";
+ this.modelSelectionLabel.tooltip = "The selected AI denoise model.
";
+ this.modelSelectionLabel.textAlignment = TextAlign_Left | TextAlign_VertCenter;
+
+ this.modelSelectionList = new ComboBox(this);
+ this.modelSelectionList.addItem("dist/v0.3.0-01");
+ this.modelSelectionList.addItem("dist/v0.4.0-01");
+ this.modelSelectionList.addItem("dist/v0.4.0-02");
+ this.modelSelectionList.addItem("dist/v0.5.0-01");
+
+ with (this.modelSelectionFrame) {
+ sizer = new HorizontalSizer();
+
+ with (sizer) {
+ margin = 6;
+
+ add(this.modelSelectionLabel);
+ addSpacing(8);
+ add(this.modelSelectionList);
+ adjustToContents();
+ }
+ }
+
+ this.modelSelectionList.currentItem = this.modelSelectionList.findItem(astrodnParameters.model);
+
+ this.modelSelectionList.onItemSelected = function (index) {
+ astrodnParameters.model = this.itemText(index);
+ }
+
+ // Strength
+ this.strengthSlider = new NumericControl(this);
+ this.strengthSlider.label.text = "Strength:";
+ this.strengthSlider.toolTip = "Increase or decrease the denoise strength.
";
+ this.strengthSlider.setRange(0.0, 1.0);
+ this.strengthSlider.slider.setRange(0.0, 1000.0);
+ this.strengthSlider.setPrecision(3);
+ this.strengthSlider.setReal(true);
+ this.strengthSlider.setValue(astrodnParameters.strength);
+
+ this.strengthSlider.onValueUpdated = function (t) {
+ astrodnParameters.strength = t;
+ }
+
+ this.resetStrengthButton = new ToolButton(this);
+ this.resetStrengthButton.icon = this.scaledResource(":/icons/clear-inverted.png");
+ this.resetStrengthButton.setScaledFixedSize(24, 24);
+ this.resetStrengthButton.toolTip = "Reset denoising strength.
";
+ this.resetStrengthButton.onClick = () => {
+ astrodnParameters.strength = 0.5;
+ this.strengthSlider.setValue(0.5);
+ }
+
+ this.strengthControl = new HorizontalSizer();
+ this.strengthControl.maxWidth = FORMWIDTH;
+ this.strengthControl.margin = 6;
+ this.strengthControl.add(this.strengthSlider);
+ this.strengthControl.add(this.resetStrengthButton);
+
+ ////////////////////////
+
+ this.stfCSlider = new NumericControl(this);
+ this.stfCSlider.label.text = "STF Low Clipping:";
+ this.stfCSlider.toolTip = "STF Stretch C (Low Clipping)
";
+ this.stfCSlider.setRange(-4.0, 0.0);
+ this.stfCSlider.slider.setRange(0.0, 1000.0);
+ this.stfCSlider.setPrecision(3);
+ this.stfCSlider.setReal(true);
+ this.stfCSlider.setValue(astrodnParameters.stfC);
+
+ this.stfCSlider.onValueUpdated = function (t) {
+ astrodnParameters.stfC = t;
+ }
+
+ this.stfCResetButton = new ToolButton(this);
+ this.stfCResetButton.icon = this.scaledResource(":/icons/clear-inverted.png");
+ this.stfCResetButton.setScaledFixedSize(24, 24);
+ this.stfCResetButton.toolTip = "Reset denoising strength.
";
+ this.stfCResetButton.onClick = () => {
+ astrodnParameters.stfC = -2.8;
+ this.stfCSlider.setValue(-2.8);
+ }
+
+ this.stfCControl = new Control( this );
+ this.stfCControl.sizer = new HorizontalSizer();
+ this.stfCControl.sizer.maxWidth = FORMWIDTH;
+ this.stfCControl.sizer.margin = 6;
+ this.stfCControl.sizer.add(this.stfCSlider);
+ this.stfCControl.sizer.add(this.stfCResetButton);
+ this.stfCControl.enabled = astrodnParameters.isstf();
+
+ ///////////////////////
+
+ this.stfBSlider = new NumericControl(this);
+ this.stfBSlider.label.text = "STF Strength:";
+ this.stfBSlider.toolTip = "STF Stretch B (Strength)
";
+ this.stfBSlider.setRange(0.0, 1.0);
+ this.stfBSlider.slider.setRange(0.0, 1000.0);
+ this.stfBSlider.setPrecision(3);
+ this.stfBSlider.setReal(true);
+ this.stfBSlider.setValue(astrodnParameters.stfB);
+
+ this.stfBSlider.onValueUpdated = function (t) {
+ astrodnParameters.stfB = t;
+ }
+
+ this.stfBResetButton = new ToolButton(this);
+ this.stfBResetButton.icon = this.scaledResource(":/icons/clear-inverted.png");
+ this.stfBResetButton.setScaledFixedSize(24, 24);
+ this.stfBResetButton.toolTip = "Reset denoising strength.
";
+ this.stfBResetButton.onClick = () => {
+ astrodnParameters.stfB = 0.25;
+ this.stfBSlider.setValue(0.25);
+ }
+
+ this.stfBControl = new Control( this );
+ this.stfBControl.sizer = new HorizontalSizer();
+ this.stfBControl.sizer.maxWidth = FORMWIDTH;
+ this.stfBControl.sizer.margin = 6;
+ this.stfBControl.sizer.add(this.stfBSlider);
+ this.stfBControl.sizer.add(this.stfBResetButton);
+ this.stfBControl.enabled = astrodnParameters.isstf();
+
+ ////////////////////////
+ // Process with STF
+ this.processSTF = new Frame;
+ this.processSTF.sizer = new HorizontalSizer;
+ this.processSTF.sizer.margin = 6;
+ this.processSTF.sizer.spacing = 6;
+
+ this.processSTF.sizer.addStretch();
+
+ this.processSTFCheckbox = new CheckBox(this);
+ this.processSTFCheckbox.text = "Pre-process with STF";
+ this.processSTFCheckbox.checked = astrodnParameters.stf;
+ this.processSTFCheckbox.toolTip = "For linear images, pre-process the image to denoise with STF stretch. The denoise process is best executed on non-linear images.
";
+ this.processSTF.sizer.add(this.processSTFCheckbox);
+
+ this.processSTFCheckbox.onCheck = function (checked) {
+ astrodnParameters.stf = checked;
+ this.dialog.stfCControl.enabled = astrodnParameters.isstf();
+ this.dialog.stfBControl.enabled = astrodnParameters.isstf();
+ }
+
+ // Replace target view
+ this.replaceTargetFrame = new Frame;
+ this.replaceTargetFrame.sizer = new HorizontalSizer;
+ this.replaceTargetFrame.sizer.margin = 6;
+ this.replaceTargetFrame.sizer.spacing = 6;
+
+ this.replaceTargetFrame.sizer.addStretch();
+
+ this.replaceTargetCheckbox = new CheckBox(this);
+ this.replaceTargetCheckbox.text = "Replace the target view";
+ this.replaceTargetCheckbox.checked = astrodnParameters.replaceTarget;
+ this.replaceTargetCheckbox.toolTip = "Replaces the target view with the processed image, if checked. Otherwise, a new image will be created.
";
+ this.replaceTargetFrame.sizer.add(this.replaceTargetCheckbox);
+
+ this.replaceTargetCheckbox.onCheck = function (checked) {
+ astrodnParameters.replaceTarget = checked;
+ }
+
+ this.buttonFrame = new Frame;
+
+ this.buttonFrame.sizer = new HorizontalSizer;
+ this.buttonFrame.sizer.margin = 6;
+ this.buttonFrame.sizer.spacing = 6;
+
+ this.newInstanceButton = new ToolButton(this);
+ this.newInstanceButton.icon = this.scaledResource(":/process-interface/new-instance.png");
+ this.newInstanceButton.setScaledFixedSize(24, 24);
+ this.newInstanceButton.onMousePress = () => {
+ astrodnParameters.saveToInstance();
+ Console.hide();
+ this.newInstance();
+ }
+
+ this.ok_Button = new ToolButton(this);
+ this.ok_Button.icon = this.scaledResource(":/process-interface/execute.png");
+ this.ok_Button.setScaledFixedSize(24, 24);
+ this.ok_Button.toolTip = "Execute.
";
+ this.ok_Button.onClick = () => {
+ astrodnParameters.saveToFile();
+ this.ok();
+ cli.process();
+ };
+
+ this.cancel_Button = new ToolButton(this);
+ this.cancel_Button.icon = this.scaledResource(":/process-interface/cancel.png");
+ this.cancel_Button.setScaledFixedSize(24, 24);
+ this.cancel_Button.toolTip = "Close this dialog with no changes.
";
+ this.cancel_Button.onClick = () => {
+ this.cancel();
+ };
+
+ this.help_Button = new ToolButton(this);
+ this.help_Button.icon = this.scaledResource(":/process-interface/browse-documentation.png");
+ this.help_Button.setScaledFixedSize(24, 24);
+ this.help_Button.toolTip = "Shows the script documentation.
";
+ this.help_Button.onClick = () => {
+ Dialog.browseScriptDocumentation("AstroDN");
+ };
+
+ this.reset_Button = new ToolButton(this);
+ this.reset_Button.icon = this.scaledResource(":/process-interface/reset.png");
+ this.reset_Button.setScaledFixedSize(24, 24);
+ this.reset_Button.toolTip = "Resets all settings to their defaults.
";
+ this.reset_Button.onClick = () => {
+ astrodnParameters.reset();
+ this.dialog.modelSelectionList.currentItem = this.dialog.modelSelectionList.findItem(astrodnParameters.model);
+ this.dialog.strengthSlider.setValue(astrodnParameters.strength);
+ this.processSTFCheckbox.checked = astrodnParameters.stf;
+ this.dialog.stfCSlider.setValue(astrodnParameters.stfC);
+ this.dialog.stfBSlider.setValue(astrodnParameters.stfB);
+ this.dialog.replaceTargetCheckbox.checked = astrodnParameters.replaceTarget;
+ }
+
+ this.buttonFrame.sizer.add(this.newInstanceButton);
+ this.buttonFrame.sizer.addSpacing(8);
+ this.buttonFrame.sizer.add(this.ok_Button);
+ this.buttonFrame.sizer.addSpacing(8);
+ this.buttonFrame.sizer.add(this.cancel_Button);
+ this.buttonFrame.sizer.addSpacing(32);
+ this.buttonFrame.sizer.add(this.help_Button);
+ this.buttonFrame.sizer.addSpacing(16);
+ this.buttonFrame.sizer.add(this.reset_Button);
+
+ this.sizer = new VerticalSizer;
+ this.sizer.margin = 8;
+
+ this.sizer.add(this.helpLabel);
+ this.sizer.addSpacing(8);
+
+ this.sizer.add(this.imageViewSelectorFrame);
+ this.sizer.addSpacing(4);
+ this.sizer.add(this.modelSelectionFrame);
+ this.sizer.add(this.strengthControl);
+ this.sizer.addSpacing(4);
+ this.sizer.add(this.processSTF);
+ this.sizer.add(this.stfCControl);
+ this.sizer.add(this.stfBControl);
+ this.sizer.addSpacing(4);
+ this.sizer.add(this.replaceTargetFrame);
+ this.sizer.addSpacing(16);
+
+ this.sizer.add(this.buttonFrame);
+
+ if (cli.targetView !== undefined) {
+ this.imageViewSelector.currentView = cli.targetView;
+ }
+}
+
+AstroDNDialog.prototype = new Dialog
\ No newline at end of file