Skip to content

Instantly share code, notes, and snippets.

@naixx
Created June 29, 2024 01:36
Show Gist options
  • Select an option

  • Save naixx/3627642cbf0e0083c9d0008dbe1f0d73 to your computer and use it in GitHub Desktop.

Select an option

Save naixx/3627642cbf0e0083c9d0008dbe1f0d73 to your computer and use it in GitHub Desktop.

Revisions

  1. naixx created this gist Jun 29, 2024.
    617 changes: 617 additions & 0 deletions DarkStructureEnhance2.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,617 @@
    /*
    DarkStructureEnhance v1.1
    A script for enhancement of dark structures.
    Copyright (C) 2009 Carlos Sonnenstein (PTeam) & Oriol Lehmkuhl (PTeam)
    This program is free software: you can redistribute it and/or modify it
    under the terms of the GNU General Public License as published by the
    Free Software Foundation, version 3 of the License.
    This program is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
    more details.
    You should have received a copy of the GNU General Public License along with
    this program. If not, see <http://www.gnu.org/licenses/>.
    */

    #include <pjsr/Sizer.jsh>
    #include <pjsr/FrameStyle.jsh>
    #include <pjsr/TextAlign.jsh>
    #include <pjsr/StdButton.jsh>
    #include <pjsr/StdIcon.jsh>
    #include <pjsr/UndoFlag.jsh>
    #include <pjsr/ColorSpace.jsh>
    #include <pjsr/NumericControl.jsh>

    #feature-id Utilities > DarkStructureEnhance2

    #feature-info A script for enhancement of dark structures.<br/>\
    <br/>\
    Based on an original algorithm created by Carlos Sonnenstein (PTeam). \
    JavaScript/PixInsight implementation by Oriol Lehmkuhl (PTeam).<br/> \
    <br/> \
    Copyright &copy; 2009 Carlos Sonnenstein (PTeam) & Oriol Lehmkuhl (PTeam)

    #feature-icon DarkStructureEnhance.xpm

    #define nStarMask 6

    #define VERSION "1.2"

    #define TITLE "DarkStructureEnhance Script"

    function ScalingFunction( kernel, name , size )
    {
    this.kernel = kernel;
    this.name = name;
    this.size = size;
    }

    var window = ImageWindow.activeWindow;
    if ( window.isNull )
    throw Error( "There is no active image window!" );

    var scalingFunctions = new Array;

    scalingFunctions[0] = new ScalingFunction(
    [ 0.29289322, 0.5, 0.29289322,
    0.5, 1.0, 0.5,
    0.29289322, 0.5, 0.29289322 ],
    "3x3 Linear Interpolation", 3 );

    scalingFunctions[1] = new ScalingFunction(
    [ 1/256, 1/64, 3/128, 1/64, 1/256,
    1/64, 1/16, 3/32, 1/16, 1/64,
    3/128, 3/32, 9/64, 3/32, 3/128,
    1/64, 1/16, 3/32, 1/16, 1/64,
    1/256, 1/64, 3/128, 1/64, 1/256 ],
    "5x5 B3 Spline", 5 );

    function doAuxiliarImage(data) {

    var view = data.targetView;
    var mask = new ImageWindow(view.image.width,
    view.image.height,
    view.image.numberOfChannels,
    window.bitsPerSample,
    window.isFloatSample,
    view.image.colorSpace != ColorSpace_Gray,
    "Mask");
    var maskView = mask.mainView;

    // copy data
    maskView.beginProcess(UndoFlag_NoSwapFile);
    maskView.image.apply(view.image);
    maskView.endProcess();

    // do awt

    var auxLayers = new Array(nStarMask);
    for(var i=0;i<nStarMask;++i) {
    auxLayers[i] = [true, true, 1.00, 3.00, 0.000, false, 0, 0.50, 2, 5, false, true, 1.0, 0.02000];
    }
    auxLayers[nStarMask] = [false, true, 1.00, 3.00, 0.000, false, 0, 0.50, 2, 5, false, false, 0.50, 0.02000];

    var wavlets = new ATrousWaveletTransformV1;
    with ( wavlets )
    {
    version = 257;
    layers = auxLayers;
    scaleDelta = 0;
    scalingFunctionData = scalingFunctions[data.scalingFunction].kernel;
    scalingFunctionKernelSize = scalingFunctions[data.scalingFunction].size;
    scalingFunctionName = scalingFunctions[data.scalingFunction].name;
    largeScaleFunction = NoFunction;
    curveBreakPoint = 0.75;
    noiseThresholdingAmount = 0.00;
    noiseThreshold = 3.00;
    lowRange = 0.000;
    highRange = 0.000;
    previewMode = Disabled;
    previewLayer = 0;
    toLuminance = true;
    toChrominance = true;
    linear = false;
    }
    wavlets.executeOn(maskView, false/*swapFile*/ );

    return mask;
    };

    function doMask( data ) {
    var view = data.targetView;
    var mask = new ImageWindow(view.image.width,
    view.image.height,
    view.image.numberOfChannels,
    window.bitsPerSample,
    window.isFloatSample,
    view.image.colorSpace != ColorSpace_Gray,
    "Mask");
    var maskView = mask.mainView;

    // copy data
    maskView.beginProcess(UndoFlag_NoSwapFile);
    maskView.image.apply(view.image);
    maskView.endProcess();

    // RBGWorking space

    var rgb = new RGBWorkingSpace;
    with (rgb)
    {
    channels = // Y, x, y
    [
    [0.333333, 0.648431, 0.330856],
    [0.333333, 0.321152, 0.597871],
    [0.333333, 0.155886, 0.066044]];
    gamma = 2.20;
    sRGBGamma = true;
    applyGlobalRGBWS = false;
    }
    rgb.executeOn(maskView, false/*swapFile*/ );

    // Anti deringing: Carlos Paranoia :D

    var auxMask = doAuxiliarImage(data);

    var pm = new PixelMath;

    var id = auxMask.mainView.id;

    with (pm) {
    expression = "$T -"+id;
    useSingleExpression = true;
    use64BitWorkingImage = false;
    rescale = false;
    rescaleLower = 0.0000000000;
    rescaleUpper = 1.0000000000;
    truncate = true;
    truncateLower = 0.0000000000;
    truncateUpper = 1.0000000000;
    createNewImage = false;
    }

    pm.executeOn( maskView, false/*swapFile*/ );

    //auxMask.show();

    auxMask.forceClose();

    // wavlets

    var auxLayers = new Array(data.numberOfLayers);
    for(var i=0;i<data.numberOfLayers;++i) {
    auxLayers[i] = [false, true, 1.00, 3.00, 0.000, false, 0, 0.50, 2, 5, false, false, 0.50, 0.02000];
    }
    auxLayers[data.numberOfLayers] = [true, true, 1.00, 3.00, 0.000, false, 0, 0.50, 2, 5, false, false, 0.50, 0.02000];

    var wavlets = new ATrousWaveletTransformV1;
    with ( wavlets )
    {
    version = 257;
    layers = auxLayers;
    scaleDelta = 0;
    scalingFunctionData = scalingFunctions[data.scalingFunction].kernel;
    scalingFunctionKernelSize = scalingFunctions[data.scalingFunction].size;
    scalingFunctionName = scalingFunctions[data.scalingFunction].name;
    largeScaleFunction = NoFunction;
    curveBreakPoint = 0.75;
    noiseThresholdingAmount = 0.00;
    noiseThreshold = 3.00;
    lowRange = 0.000;
    highRange = 0.000;
    previewMode = Disabled;
    previewLayer = 0;
    toLuminance = true;
    toChrominance = true;
    linear = false;
    }
    wavlets.executeOn( maskView, false/*swapFile*/ );

    // Pixel Math

    var pm = new PixelMath;
    with ( pm )
    {
    expression = maskView.id+"-"+view.id;
    expression1 = "";
    expression2 = "";
    useSingleExpression = true;
    variables = "";
    use64BitWorkingImage = false;
    rescale = false;
    rescaleLower = 0.0000000000;
    rescaleUpper = 1.0000000000;
    truncate = true;
    truncateLower = 0.0000000000;
    truncateUpper = 1.0000000000;
    createNewImage = false;
    newImageId = "";
    newImageWidth = 0;
    newImageHeight = 0;
    newImageColorSpace = SameAsTarget;
    newImageSampleFormat = SameAsTarget;
    }
    pm.executeOn( maskView, false/*swapFile*/ );

    // Convert to gray

    if(view.image.colorSpace != ColorSpace_Gray) {
    var toGray = new ConvertToGrayscale;
    toGray.executeOn( maskView, false/*swapFile*/ );
    }

    // Rescale

    var rescale = new Rescale;
    with ( rescale )
    {
    mode = RGBK;
    }
    rescale.executeOn( maskView, false/*swapFile*/ );

    // Noise reduction

    var nr = new ATrousWaveletTransformV1;
    with ( nr )
    {
    version = 257;
    layers = // enabled, biasEnabled, structureDetectionThreshold, structureDetectionRange, bias, noiseReductionEnabled, noiseReductionFilter, noiseReductionAmount, noiseReductionIterations, noiseReductionKernelSize, noiseReductionProtectSignificant, deringingEnabled, deringingAmount, deringingThreshold
    [
    [false, true, 1.00, 3.00, 0.000, false, Recursive, 1.00, 5, 5, false, false, 0.50, 0.02000],
    [true, true, 1.00, 3.00, 0.000, true, Recursive, 1.00, 5, 5, false, false, 0.50, 0.02000],
    [true, true, 1.00, 3.00, 0.000, false, Recursive, 0.50, 2, 5, false, false, 0.50, 0.02000]];
    scaleDelta = 0;
    scalingFunctionData = [
    0.292893,0.5,0.292893,
    0.5,1,0.5,
    0.292893,0.5,0.292893];
    scalingFunctionKernelSize = 3;
    scalingFunctionName = "3x3 Linear Interpolation";
    largeScaleFunction = NoFunction;
    toLuminance = true;
    toChrominance = false;
    linear = false;
    }
    nr.executeOn( maskView, false/*swapFile*/ );

    return mask;
    }

    function doDark(data) {
    var hist = new HistogramTransformation;

    hist.H = [
    [0, 0.5, 1, 0, 1], // R
    [0, 0.5, 1, 0, 1], // G
    [0, 0.5, 1, 0, 1], // B
    [0, data.median,1, 0, 1], // RGB
    [0, 0.5, 1, 0, 1], // Alpha
    ];

    var mask = doMask(data);

    var pc = new ProcessContainer;

    for(var i=0;i<data.iterations;++i) {
    pc.add(hist);
    pc.setMask(i,mask,false/*invert*/);
    }

    data.targetView.beginProcess(data.swap);
    pc.executeOn( data.targetView, false/*swapFile*/ );
    data.targetView.endProcess();

    mask.removeMaskReferences();

    if ( data.viewMask )
    mask.show();
    else
    mask.forceClose();
    }

    function DarkMaskLayersData()
    {

    }

    var data = {
    targetView: window.currentView,
    numberOfLayers: 8,
    scalingFunction: 1,
    extractResidual: true,
    toLumi: false,
    median: 0.7,
    iterations: 1,
    viewMask: false,
    swap: UndoFlag_DefaultMode,

    save: function() {
    Parameters.set("targetView", this.targetView ? this.targetView.id : "");
    Parameters.set("numberOfLayers", this.numberOfLayers );
    Parameters.set("scalingFunction", this.scalingFunction);
    Parameters.set("extractResidual", this.extractResidual);
    Parameters.set("median", this.median);
    Parameters.set("iterations", this.iterations);
    Parameters.set("viewMask", this.viewMask);
    },
    load: function() {
    if (Parameters.has("targetView"))
    this.targetView = ImageWindow.windowById(Parameters.getString("targetView")).mainView;
    if (Parameters.has("numberOfLayers"))
    this.numberOfLayers = Parameters.getInteger("numberOfLayers");
    if (Parameters.has("scalingFunction"))
    this.scalingFunction = Parameters.getInteger("scalingFunction");
    if (Parameters.has("extractResidual"))
    this.extractResidual = Parameters.getBoolean("extractResidual");
    if (Parameters.has("median"))
    this.median = Parameters.getReal("median");
    if (Parameters.has("iterations"))
    this.iterations = Parameters.getInteger("iterations");
    if (Parameters.has("viewMask"))
    this.viewMask = Parameters.getBoolean("viewMask");
    }
    };

    function DarkMaskLayersDialog()
    {
    this.__base__ = Dialog;
    this.__base__();

    var emWidth = this.font.width( 'M' );
    var labelWidth1 = this.font.width( "Layers to remove:" + 'T' );

    //

    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 =
    "<p><b>DarkStructureEnhance v" + VERSION + "</b> &mdash; A script for "
    + "enhancement of dark image structures.</p>"
    + "<p>The script can also provide the mask used in the DSE process. To include "
    + "larger structures in the mask, increase the number of layers to remove. "
    + "To increase enhancement of dark structures, increase the value of the "
    + "<i>Amount</i> parameter.</p>"
    + "<p>Copyright &copy; 2009 Carlos Sonnenstein and Oriol Lehmkuhl (Pteam)</p>";

    this.targetImage_Label = new Label( this );
    this.targetImage_Label.minWidth = labelWidth1 + this.logicalPixelsToPhysical( 6+1 ); // align with labels inside group boxes below
    this.targetImage_Label.text = "Target image:";
    this.targetImage_Label.textAlignment = TextAlign_Right|TextAlign_VertCenter;

    this.targetImage_ViewList = new ViewList( this );
    this.targetImage_ViewList.scaledMinWidth = 200;
    this.targetImage_ViewList.getAll(); // include main views as well as previews
    this.targetImage_ViewList.currentView = data.targetView;
    this.targetImage_ViewList.toolTip = "Select the image to perform the Dark Structure Enhancement.";
    this.targetImage_ViewList.onViewSelected = function( view )
    {
    data.targetView = view;
    };

    this.targetImage_Sizer = new HorizontalSizer;
    this.targetImage_Sizer.spacing = 4;
    this.targetImage_Sizer.add( this.targetImage_Label );
    this.targetImage_Sizer.add( this.targetImage_ViewList, 100 );

    // Dark Mask parameters
    this.numberOfLayers_Label = new Label( this );
    this.numberOfLayers_Label.minWidth = labelWidth1;
    this.numberOfLayers_Label.text = "Layers to remove:";
    this.numberOfLayers_Label.textAlignment = TextAlign_Right|TextAlign_VertCenter;

    this.numberOfLayers_SpinBox = new SpinBox( this );
    this.numberOfLayers_SpinBox.minValue = 7;
    this.numberOfLayers_SpinBox.maxValue = 12;
    this.numberOfLayers_SpinBox.value = data.numberOfLayers;
    this.numberOfLayers_SpinBox.toolTip = this.numberOfLayers_Label.toolTip =
    "<b>Number of wavelet layers that will be removed to build an enhancement mask.</b>";
    this.numberOfLayers_SpinBox.onValueUpdated = function( value )
    {
    data.numberOfLayers = value;
    };

    this.extractResidual_CheckBox = new CheckBox( this );
    this.extractResidual_CheckBox.text = "Extract mask";
    this.extractResidual_CheckBox.checked = data.viewMask;
    this.extractResidual_CheckBox.toolTip =
    "<p>If this option is selected, the script will create an image window "
    + "with the mask used to perform dark structure enhancement.</p>";
    this.extractResidual_CheckBox.onCheck = function( checked )
    {
    data.viewMask = checked;
    };

    this.numberOfLayers_Sizer = new HorizontalSizer;
    this.numberOfLayers_Sizer.spacing = 4;
    this.numberOfLayers_Sizer.add( this.numberOfLayers_Label );
    this.numberOfLayers_Sizer.add( this.numberOfLayers_SpinBox );
    this.numberOfLayers_Sizer.addSpacing( 12 );
    this.numberOfLayers_Sizer.add( this.extractResidual_CheckBox );
    this.numberOfLayers_Sizer.addStretch();

    this.scalingFunction_Label = new Label( this );
    this.scalingFunction_Label.minWidth = labelWidth1;
    this.scalingFunction_Label.text = "Scaling function:";
    this.scalingFunction_Label.textAlignment = TextAlign_Right|TextAlign_VertCenter;

    this.scalingFunction_ComboBox = new ComboBox( this );
    for ( var i = 0; i < scalingFunctions.length; ++i )
    this.scalingFunction_ComboBox.addItem( scalingFunctions[i].name );
    this.scalingFunction_ComboBox.currentItem = data.scalingFunction;
    this.scalingFunction_ComboBox.toolTip = this.scalingFunction_Label.toolTip =
    "<p>Select a scaling function to perform the wavelet decomposition.</p>";
    this.scalingFunction_ComboBox.onItemSelected = function( index )
    {
    data.scalingFunction = index;
    };

    this.scalingFunction_Sizer = new HorizontalSizer;
    this.scalingFunction_Sizer.spacing = 4;
    this.scalingFunction_Sizer.add( this.scalingFunction_Label );
    this.scalingFunction_Sizer.add( this.scalingFunction_ComboBox );
    this.scalingFunction_Sizer.addStretch();

    this.dmParGroupBox = new GroupBox( this );
    this.dmParGroupBox.title = "Mask Parameters";
    this.dmParGroupBox.sizer = new VerticalSizer;
    this.dmParGroupBox.sizer.margin = 6;
    this.dmParGroupBox.sizer.spacing = 4;
    this.dmParGroupBox.sizer.add( this.numberOfLayers_Sizer );
    this.dmParGroupBox.sizer.add( this.scalingFunction_Sizer );

    // DSE parameters
    this.median_NC = new NumericControl (this);
    with ( this.median_NC ) {
    label.text = "Amount:";
    label.minWidth = labelWidth1;
    setRange (0.0, 0.99);
    slider.setRange (0, 1000);
    slider.scaledMinWidth = 250;
    setPrecision (2);
    setValue ((data.median-0.5)*2);
    toolTip = "<p>DSE intensity for each iteration.</p>";
    onValueUpdated = function (value) { data.median = (0.5*value)+0.5; };
    }

    this.iter_Label = new Label( this );
    this.iter_Label.minWidth = labelWidth1;
    this.iter_Label.text = "Iterations:";
    this.iter_Label.textAlignment = TextAlign_Right|TextAlign_VertCenter;

    this.iter_SpinBox = new SpinBox( this );
    this.iter_SpinBox.minValue = 1;
    this.iter_SpinBox.maxValue = 10;
    this.iter_SpinBox.value = data.iterations;
    this.iter_SpinBox.toolTip = "<p>Number of midtones balance transformations.</p>";
    this.iter_SpinBox.onValueUpdated = function( value )
    {
    data.iterations = value;
    };

    this.iter_Sizer = new HorizontalSizer;
    this.iter_Sizer.spacing = 4;
    this.iter_Sizer.add( this.iter_Label );
    this.iter_Sizer.add( this.iter_SpinBox );
    this.iter_Sizer.addStretch();

    this.dseParGroupBox = new GroupBox( this );
    this.dseParGroupBox.title = "DSE Parameters";
    this.dseParGroupBox.sizer = new VerticalSizer;
    this.dseParGroupBox.sizer.margin = 6;
    this.dseParGroupBox.sizer.spacing = 4;
    this.dseParGroupBox.sizer.add( this.median_NC );
    this.dseParGroupBox.sizer.add( this.iter_Sizer );

    // usual control buttons
    this.ok_Button = new PushButton( this );
    this.ok_Button.text = "OK";
    this.ok_Button.icon = this.scaledResource( ":/icons/ok.png" );
    this.ok_Button.onClick = function()
    {
    this.dialog.ok();
    };

    // Button setup
    this.newInstanceButton = new ToolButton(this);
    this.newInstanceButton.icon = this.scaledResource(":/process-interface/new-instance.png");
    this.newInstanceButton.setScaledFixedSize(24, 24);
    this.newInstanceButton.toolTip = "New Instance";
    this.newInstanceButton.onMousePress = () => {
    data.save();
    this.newInstance();
    };

    this.cancel_Button = new PushButton( this );
    this.cancel_Button.text = "Cancel";
    this.cancel_Button.icon = this.scaledResource( ":/icons/cancel.png" );
    this.cancel_Button.onClick = function()
    {
    this.dialog.cancel();
    };

    this.buttons_Sizer = new HorizontalSizer;
    this.buttons_Sizer.add(this.newInstanceButton);
    this.buttons_Sizer.spacing = 4;
    this.buttons_Sizer.addStretch();
    this.buttons_Sizer.add( this.ok_Button );
    this.buttons_Sizer.add( this.cancel_Button );


    this.sizer = new VerticalSizer;
    this.sizer.margin = 8;
    this.sizer.spacing = 6;
    this.sizer.add( this.helpLabel );
    this.sizer.addSpacing( 4 );
    this.sizer.add( this.targetImage_Sizer );
    this.sizer.add( this.dmParGroupBox);
    this.sizer.add( this.dseParGroupBox);
    this.sizer.addSpacing( 4 );
    this.sizer.add( this.buttons_Sizer );

    this.windowTitle = TITLE;
    this.adjustToContents();
    this.setFixedSize();
    }

    DarkMaskLayersDialog.prototype = new Dialog;

    /*
    * Script entry point.
    */
    function main()
    {
    console.hide();

    if (Parameters.isViewTarget){
    data.load();
    data.swap = UndoFlag_NoSwapFile;
    data.targetView = Parameters.targetView;
    doDark( data );
    return;
    }

    else if (Parameters.isGlobalTarget) {
    // then load the parameters from the instance and continue
    data.load();
    }

    if ( !data.targetView )
    {
    var msg = new MessageBox( "There is no active image window!",
    "DarkMaskLayers Script",
    StdIcon_Error, StdButton_Ok );
    msg.execute();
    return;
    }

    var dialog = new DarkMaskLayersDialog();
    for ( ;; )
    {
    if ( !dialog.execute() )
    break;
    data.save();
    // A view must be selected.
    if ( data.targetView.isNull )
    {
    var msg = new MessageBox( "You must select a view to apply this script.",
    "DarkMaskLayers Script", StdIcon_Error, StdButton_Ok );
    msg.execute();
    continue;
    }

    console.show();
    doDark( data );
    data.save();
    break;
    }
    }

    main();