Feature Suggestion: "Downscale to Actual Size" Before Upscaling

Hello Topaz Team and fellow forum members,

As an avid user of Topaz Photo AI, I’ve noticed an issue that affects the upscaling of photos sourced from social media, web sources, or screenshots that inherit the device resolution.

The Problem

  • Photos from these sources are often rendered larger than their actual pixel density.
  • Topaz Photo AI appears to analyze the pixel density based on the photo file data, not the actual pixels.
  • Consequently, if a photo is rendered larger than its actual pixel density, the software upscales based on the larger, rendered size, not the actual size.
  • This discrepancy can lead to less accurate upscaling and a decrease in the final image quality.

The Solution

I’ve developed a Photoshop script to address this issue in my workflow. The script analyzes the photo, downscales it to match the actual pixel density. Then I take those exports and upscale them within Topaz Photo AI.

  • The script uses color sampling to estimate the actual pixel density and resizes the image accordingly.
  • This ensures the upscaling is based on the actual pixels, not the rendered size.

I propose this “Downscale to Actual Size” process as a potential feature for Topaz Photo AI. By automatically analyzing and adjusting the photo size before upscaling, the software could provide more accurate and higher quality results, especially for photos sourced from the web or social media.

The Process

Here is a detailed breakdown of the process I use with my Photoshop script. I have found these to be the optimal data points after months of testing:

  1. Analyze the photo:

    • The script selects 20 areas of 10x10 pixels, which are proportionally spread across the photo.
    • It then samples colors from each of these areas to estimate the actual pixel density.
    • For each area, it calculates the number of unique colors in each row of pixels, resulting in 200 values.
  2. Account for outliers:

    • The script then removes the lowest 35% and the highest 20% of these values to account for outliers.
    • Low value outliers could be caused by solid colored objects, backgrounds, frames which would result in misrepresentative low values.
    • High value outliers could be caused by resampling or artifacting, resulting in falsely inflated pixel densities in certain areas.
  3. Calculate the scale factor:

    • The script then averages the remaining 90 values and divides the result by 10 to calculate the scale factor.
    • This scale factor represents the ratio between the actual pixel density and the rendered size.
  4. Resize the photo:

    • The script resizes the photo based on the calculated scale factor, effectively downscaling the photo to its actual size.
  5. Upscale with Topaz Photo AI:

    • After the photo has been downscaled to its actual size, I take these exports and upscale them within Topaz Photo AI.
    • This ensures that the upscaling is based on the actual pixels of the source, leading to more accurate and higher quality results.

Conclusion

Implementing this “Downscale to Actual Size” feature could greatly enhance the capabilities of Topaz Photo AI, particularly for photos sourced from the web or social media. By ensuring upscaling is based on actual pixel density, the software could provide more accurate and higher quality results.

I look forward to hearing the community’s thoughts on this suggestion, and look forward to continuing to use Photo AI and support this fantastic software.

(PS: Can I pls have lifetime sub if this gets implemented)

Thanks for your time,
Weston :slight_smile:

I agree that the AI seems to work better on photos where the detail density is closer to the pixel density. That’s why shrink down images sometimes helps to get better results. On the other hand it will be difficult to do this by an automated process because the upscale that happened before might be done with an interpolation like bicubic. This makes the overall image more blurry and it will probably not get back to its original state even after downscaling first.

1 Like

Appreciate the response and input! This isn’t a one size fits all approach, you’re right. While some source conditions would work better than others with this approach, my suggested solution would actually be super useful on those Bicubic Resampled Sources if you tweak the outlier compensation.

That being said, that is an edge case that I’m not really accounting for.

The main issue this seeks to solve is getting images sourced from the web primed for upscaling.

I have found this works really well with almost all images with lossy compression and photos with falsely inflated resolutions.

Here is the script I made, you should give it a shot. Just stick this in notepad and save as a .JSX file to run it in photoshop. Sharing as plain text for transparency.

#target photoshop

// Polyfills for compatibility with older JavaScript engines.
if (!Object.keys) {
    Object.keys = function(obj) {
        var keys = [];
        for (var i in obj) {
            if (obj.hasOwnProperty(i)) {
                keys.push(i);
            }
        }
        return keys;
    };
}

if (!Array.prototype.reduce) {
    Array.prototype.reduce = function(callback /*, initialValue*/) {
        if (this == null) {
            throw new TypeError('Array.prototype.reduce called on null or undefined');
        }
        if (typeof callback !== 'function') {
            throw new TypeError(callback + ' is not a function');
        }
        var o = Object(this);
        var len = o.length >>> 0;
        var k = 0;
        var value;
        if (arguments.length >= 2) {
            value = arguments[1];
        } else {
            while (k < len && !(k in o)) {
                k++;
            }
            if (k >= len) {
                throw new TypeError('Reduce of empty array with no initial value');
            }
            value = o[k++];
        }
        for (; k < len; k++) {
            if (k in o) {
                value = callback(value, o[k], k, o);
            }
        }
        return value;
    };
}

// Function to retrieve a list of files from a specified folder or from an array of selected files.
function getFilesFromSource(folder, selectedFiles) {
    if (selectedFiles) {
        // Return the array of selected files if provided.
        return selectedFiles;
    } else if (folder) {
        // If a folder is provided, return an array of files with supported image formats.
        return folder.getFiles(function(file) {
            return file instanceof File && /\.(jpg|jpeg|png|psd|tiff|tif)$/i.test(file.name);
        });
    } else {
        // If neither a folder nor selected files are provided, throw an error.
        throw new Error('No source folder or files selected.');
    }
}

var scriptSettings = {
    outputFolder: { preset: null, saved: null },
    resampleMethod: { preset: 'None', saved: 'None' },
    preserveInputFormat: { preset: false, saved: false },
    outputFormat: { preset: 'JPG', saved: 'JPG' },
    jpegQuality: { preset: 12, saved: null },
    deleteSourceFiles: { preset: false, saved: false },
    analysisAreaSize: { preset: 10, saved: 10 },
    numberOfSampleAreas: { preset: 20, saved: 20 },
    outlierThresholds: { preset: { low: 35, high: 65 }, saved: { low: 35, high: 65 } }
};

// Function to get a setting value, falling back to the preset if no saved value is set.
function getSetting(key) {
    if (scriptSettings.hasOwnProperty(key)) {
        // Return the saved value if it exists, otherwise return the preset value.
        return scriptSettings[key].saved !== null ? scriptSettings[key].saved : scriptSettings[key].preset;
    } else {
        // If the key does not exist, log an error message and return undefined.
        $.writeln('Error: Setting key "' + key + '" does not exist.');
        return undefined;
    }
}

// Function to save a setting value.
function saveSetting(key, value) {
    if (scriptSettings.hasOwnProperty(key)) {
        // Save the value to the scriptSettings object.
        scriptSettings[key].saved = value;
    } else {
        // If the key does not exist, throw an error.
        throw new Error('Setting key "' + key + '" does not exist.');
    }
}

// Function to revert settings to their preset values.
function revertToDefaultSettings() {
    for (var key in scriptSettings) {
        if (scriptSettings.hasOwnProperty(key)) {
            // Reset each setting to its preset value.
            scriptSettings[key].saved = null;
        }
    }
}

// Function to display the settings dialog
function showSettingsDialog() {
    var settingsDialog = createSettingsDialog(); // Create the settings dialog.
    settingsDialog.show(); // Display the settings dialog.
}

// Function to create the main dialog with UI elements for user interaction.
function createDialog() {
    var dialog = new Window('dialog', 'Downscale to Actual Size by @WestonCarlisle');

    // Header group for the settings button.
    var headerGroup = dialog.add('group');
    headerGroup.alignment = 'left'; // Align the settings button to the left.
    headerGroup.orientation = 'row';
    headerGroup.alignChildren = 'center';

    // Settings button to open the settings dialog.
    var settingsButton = headerGroup.add('button', undefined, 'Settings');
    settingsButton.helpTip = "Open settings"; // Tooltip text for the settings button.
    settingsButton.onClick = function() {
        showSettingsDialog(); // Call the function to display the settings dialog.
    };

    // Set the orientation and alignment for the dialog.
    dialog.orientation = 'column';
    dialog.alignChildren = 'fill';
    dialog.spacing = 10;
    dialog.preferredSize.width = 1000;

    var sourcePanel = dialog.add('panel', undefined, 'Source Files');
    sourcePanel.orientation = 'column';
    sourcePanel.alignChildren = 'fill';
    sourcePanel.preferredSize.width = dialog.preferredSize.width;
    var btnSourceFiles = sourcePanel.add('button', undefined, 'Select Files...');
    var btnSourceFolder = sourcePanel.add('button', undefined, 'Select Folder...');

    // Listbox to display the selected files with headers for file name and path.
    var fileList = sourcePanel.add('listbox', undefined, [], {
        multiselect: true,
        numberOfColumns: 2,
        showHeaders: true,
        columnTitles: ['File Name', 'Path'],
        columnWidths: [sourcePanel.preferredSize.width * 0.6, sourcePanel.preferredSize.width * 0.4]
    });
    fileList.preferredSize.height = 250;

    var outputAndSettingsPanel = dialog.add('panel', undefined, 'Output and Settings');
    outputAndSettingsPanel.orientation = 'row';
    outputAndSettingsPanel.alignChildren = 'top';
    outputAndSettingsPanel.spacing = 10;
    outputAndSettingsPanel.preferredSize.width = dialog.preferredSize.width;

    var outputPanel = outputAndSettingsPanel.add('panel', undefined, 'Output');
    outputPanel.orientation = 'column';
    outputPanel.alignChildren = 'fill';
    outputPanel.preferredSize.width = Math.round(outputAndSettingsPanel.preferredSize.width / 3);
    outputPanel.add('statictext', undefined, 'Select output folder:');
    var btnOutputFolder = outputPanel.add('button', undefined, 'Select Folder...');
    var outputText = outputPanel.add('edittext', undefined, '');
    outputText.size = [outputPanel.preferredSize.width - 30, 20];

    btnSourceFiles.onClick = function() {
        // Handler for the "Select Files..." button click
        var files = File.openDialog("Select files", "All files:*.jpg;*.jpeg;*.png;*.psd;*.tiff;*.tif", true);
        if (files) {
            dialog.sourceFiles = dialog.sourceFiles ? dialog.sourceFiles.concat(files) : files;
            updateFileListUI(fileList, dialog.sourceFiles);
        }
    };
    btnSourceFolder.onClick = function() {
        // Handler for the "Select Folder..." button click
        var folder = Folder.selectDialog("Select a folder");
        if (folder) {
            dialog.sourceFolder = folder;
            var folderFiles = getFilesFromSource(folder, null);
            updateFileListUI(fileList, folderFiles);
        }
    };
    btnOutputFolder.onClick = function() {
        // Handler for the "Select Folder..." button click in the Output panel
        var folder = Folder.selectDialog("Select output folder");
        if (folder) {
            outputText.text = folder.fsName;
            dialog.outputFolder = folder;
        }
    };

    // Function to update the file list UI
    function updateFileListUI(fileList, files) {
        fileList.removeAll(); // Clear the existing list
        for (var i = 0; i < files.length; i++) {
            var file = files[i];
            var item = fileList.add('item', file.name);
            item.subItems[0].text = file.fsName;
        }
    }

var buttonsGroup = dialog.add('group');
buttonsGroup.orientation = 'row';
buttonsGroup.alignChildren = 'center';

// OK button to start processing.
var btnOk = buttonsGroup.add('button', undefined, 'OK', {name: 'ok'});
btnOk.onClick = function() {
    // Handler for the OK button click
    dialog.close(1); // Close the dialog and proceed with processing.
};

var btnCancel = buttonsGroup.add('button', undefined, 'Cancel', {name: 'cancel'});
btnCancel.onClick = function() {
    // Handler for the Cancel button click
    dialog.close(0); // Close the dialog and cancel processing.
};
    // Assign dialog properties
    dialog.sourceFolder = null; // Initially, no source folder is selected
    dialog.outputFolder = null; // Initially, no output folder is selected
    dialog.preserveInputFormat = true; // Initially, preserve the input format
    dialog.outputFormat = 'JPG'; // Default output format
    dialog.resampleMethod = 'None'; // Default resample method
    dialog.deleteSourceFiles = false; // Default setting for deleting source files

    return dialog;
}

function createSettingsDialog() {
    var settingsDialog = new Window('dialog', 'Settings');
    settingsDialog.orientation = 'column';
    settingsDialog.alignChildren = 'center';
    settingsDialog.spacing = 10;
    settingsDialog.margins = 10;

    var exportSettingsPanel = settingsDialog.add('panel', undefined, 'Export Settings');
    exportSettingsPanel.orientation = 'column';
    exportSettingsPanel.alignChildren = 'fill';
    exportSettingsPanel.spacing = 5;

    var outputFolderGroup = exportSettingsPanel.add('group');
    outputFolderGroup.orientation = 'row';
    outputFolderGroup.add('statictext', undefined, 'Output Folder:');
    var outputFolderEditText = outputFolderGroup.add('edittext', undefined, getSetting('outputFolder'));
    outputFolderEditText.preferredSize.width = 200;

    var outputFolderButton = outputFolderGroup.add('button', undefined, 'Browse...');
    outputFolderButton.onClick = function() {
        var selectedFolder = Folder.selectDialog("Select an output folder");
        if (selectedFolder) {
            // Set the text of the output folder edit text to the path of the selected folder.
            outputFolderEditText.text = selectedFolder.fsName;
            // Save the selected folder path as the new output folder setting.
            saveSetting('outputFolder', selectedFolder.fsName);
        }
    };

    var resampleMethodGroup = exportSettingsPanel.add('group');
    resampleMethodGroup.orientation = 'row';
    resampleMethodGroup.add('statictext', undefined, 'Resample Method:');
    var resampleMethodDropdown = resampleMethodGroup.add('dropdownlist', undefined, ['Automatic', 'Bicubic', 'Bilinear', 'Nearest Neighbor', 'None']);
    resampleMethodDropdown.selection = resampleMethodDropdown.find(getSetting('resampleMethod'));

    var preserveFormatGroup = exportSettingsPanel.add('group');
    preserveFormatGroup.orientation = 'row';
    var preserveInputFormatCheckbox = preserveFormatGroup.add('checkbox', undefined, 'Preserve Input Format');
    preserveInputFormatCheckbox.value = getSetting('preserveInputFormat');
    var outputFormatDropdown = preserveFormatGroup.add('dropdownlist', undefined, ['JPG', 'PNG', 'TIFF']);
    outputFormatDropdown.selection = outputFormatDropdown.find(getSetting('outputFormat'));
    outputFormatDropdown.enabled = !preserveInputFormatCheckbox.value;

    var jpegQualityGroup = exportSettingsPanel.add('group');
    jpegQualityGroup.orientation = 'row';
    jpegQualityGroup.add('statictext', undefined, 'JPG Quality:');
    var jpegQualitySlider = jpegQualityGroup.add('slider', undefined, getSetting('jpegQuality'), 1, 12);
    var jpegQualityValueLabel = jpegQualityGroup.add('statictext', undefined, jpegQualitySlider.value.toString());
    jpegQualitySlider.onChanging = function() {
        jpegQualityValueLabel.text = Math.round(this.value).toString();
    };
    jpegQualityGroup.visible = preserveInputFormatCheckbox.value || getSetting('outputFormat') === 'JPG';

    var exportSettingsButtonsGroup = exportSettingsPanel.add('group');
    exportSettingsButtonsGroup.orientation = 'row';
    var saveExportSettingsButton = exportSettingsButtonsGroup.add('button', undefined, 'Save Export Settings');
    var revertExportSettingsButton = exportSettingsButtonsGroup.add('button', undefined, 'Revert to Default Settings');

    var advancedSettingsPanel = settingsDialog.add('panel', undefined, 'Advanced Settings');
    advancedSettingsPanel.orientation = 'column';
    advancedSettingsPanel.alignChildren = 'fill';
    advancedSettingsPanel.spacing = 5;

 // Analysis Area Size with description
var analysisAreaSizeGroup = advancedSettingsPanel.add('group');
analysisAreaSizeGroup.orientation = 'row';
analysisAreaSizeGroup.add('statictext', undefined, 'Analysis Area Size:');
var analysisAreaSizeEditText = analysisAreaSizeGroup.add('edittext', undefined, getSetting('analysisAreaSize'));
analysisAreaSizeEditText.preferredSize.width = 50;
var analysisAreaSizeDescription = advancedSettingsPanel.add('statictext', undefined, 'The size of the area (in pixels) used for color analysis.');
analysisAreaSizeDescription.graphics.font = ScriptUI.newFont("dialog", "Italic", 10);

var numberOfSampleAreasGroup = advancedSettingsPanel.add('group');
numberOfSampleAreasGroup.orientation = 'row';
numberOfSampleAreasGroup.add('statictext', undefined, 'Number of Sample Areas:');
var numberOfSampleAreasEditText = numberOfSampleAreasGroup.add('edittext', undefined, getSetting('numberOfSampleAreas'));
numberOfSampleAreasEditText.preferredSize.width = 50;
var numberOfSampleAreasDescription = advancedSettingsPanel.add('statictext', undefined, 'The number of areas sampled for color analysis across the image.');
numberOfSampleAreasDescription.graphics.font = ScriptUI.newFont("dialog", "Italic", 10);

var outlierThresholdsGroup = advancedSettingsPanel.add('group');
outlierThresholdsGroup.orientation = 'row';
outlierThresholdsGroup.add('statictext', undefined, 'Outlier Thresholds:');
// ... (dual-slider code)
var outlierThresholdsDescription = advancedSettingsPanel.add('statictext', undefined, 'The low and high percentage thresholds for excluding outlier values from analysis.');
outlierThresholdsDescription.graphics.font = ScriptUI.newFont("dialog", "Italic", 10);

var lowOutlierThresholdSlider = outlierThresholdsGroup.add('slider', undefined, getSetting('outlierThresholds').low, 1, 100);
lowOutlierThresholdSlider.value = getSetting('outlierThresholds').low || 35; // Default to 35 if not set
var lowOutlierThresholdValueLabel = outlierThresholdsGroup.add('statictext', undefined, lowOutlierThresholdSlider.value.toString());

var highOutlierThresholdSlider = outlierThresholdsGroup.add('slider', undefined, getSetting('outlierThresholds').high, 1, 100);
highOutlierThresholdSlider.value = getSetting('outlierThresholds').high || 65; // Default to 65 if not set
var highOutlierThresholdValueLabel = outlierThresholdsGroup.add('statictext', undefined, highOutlierThresholdSlider.value.toString());

lowOutlierThresholdSlider.onChanging = function() {
    if (lowOutlierThresholdSlider.value > highOutlierThresholdSlider.value) {
        highOutlierThresholdSlider.value = lowOutlierThresholdSlider.value;
    }
    lowOutlierThresholdValueLabel.text = Math.round(lowOutlierThresholdSlider.value).toString();
};
highOutlierThresholdSlider.onChanging = function() {
    if (highOutlierThresholdSlider.value < lowOutlierThresholdSlider.value) {
        lowOutlierThresholdSlider.value = highOutlierThresholdSlider.value;
    }
    highOutlierThresholdValueLabel.text = Math.round(highOutlierThresholdSlider.value).toString();
};

    var advancedSettingsButtonsGroup = advancedSettingsPanel.add('group');
    advancedSettingsButtonsGroup.orientation = 'row';
    var saveAdvancedSettingsButton = advancedSettingsButtonsGroup.add('button', undefined, 'Save Advanced Settings');
    var revertAdvancedSettingsButton = advancedSettingsButtonsGroup.add('button', undefined, 'Revert to Default Settings');

    saveExportSettingsButton.onClick = function() {
        saveSetting('outputFolder', outputFolderEditText.text);
        saveSetting('resampleMethod', resampleMethodDropdown.selection.text);
        saveSetting('preserveInputFormat', preserveInputFormatCheckbox.value);
        saveSetting('outputFormat', outputFormatDropdown.selection.text);
        saveSetting('jpegQuality', parseInt(jpegQualityValueLabel.text));
        settingsDialog.close();
    };

    revertExportSettingsButton.onClick = function() {
        // Revert Export Settings to Default
        revertToDefaultSettings();
        settingsDialog.close();
    };

    saveAdvancedSettingsButton.onClick = function() {
        // Save Advanced Settings
        saveSetting('analysisAreaSize', parseInt(analysisAreaSizeEditText.text));
        saveSetting('numberOfSampleAreas', parseInt(numberOfSampleAreasEditText.text));
        saveSetting('outlierThresholds', {
            low: parseInt(lowOutlierThresholdValueLabel.text),
            high: parseInt(highOutlierThresholdValueLabel.text)
        });
        settingsDialog.close();
    };

    revertAdvancedSettingsButton.onClick = function() {
        // Revert Advanced Settings to Default
        revertToDefaultSettings();
        settingsDialog.close();
    };

    var navigationGroup = settingsDialog.add('group');
    navigationGroup.orientation = 'row';
    var returnButton = navigationGroup.add('button', undefined, 'Return to Script');
    var closeButton = navigationGroup.add('button', undefined, 'Close');

    returnButton.onClick = function() {
        settingsDialog.close();
    };

    closeButton.onClick = function() {
        // Close the settings dialog
        settingsDialog.close();
    };

    return settingsDialog;
}

function processFiles(files, progressBar, outputFolder, preserveInputFormat, outputFormat, jpegQuality, resampleMethod, analysisSize, deleteSourceFiles) {
    var totalFiles = files.length; // Total number of files to process
    progressBar.macroBar.value = 0; // Initialize the macro progress bar to 0%
    progressBar.macroBar.maxvalue = 100; // Set the maximum value of the macro progress bar to 100

// Loop through each file in the list of files to be processed
    for (var i = 0; i < totalFiles; i++) {
        var file = files[i]; // Get the current file from the list
        var fileIndex = i + 1; // Current file index (1-based for display purposes)

        // Open the current document in Photoshop
        var doc = openDocument(file);
        
        app.runMenuItem(stringIDToTypeID('fitOnScreen'));
        app.refresh(); // Refresh the Photoshop UI to reflect the zoom change

        progressBar.stMessage.text = "Analyzing Pixels of file " + fileIndex + " of " + totalFiles;
        progressBar.microBar.value = 25; // Set micro progress bar to 25% after opening the document
        progressBar.macroBar.value = ((fileIndex / totalFiles) * 100) + 0;
        app.refresh(); // Refresh the UI to show the updated progress bar and message

        analyzeAndResize(doc, analysisSize, resampleMethod);
        progressBar.microBar.value = 50; // Set micro progress bar to 50% after analysis
        progressBar.stMessage.text = "Resizing file " + fileIndex + " of " + totalFiles;
        progressBar.macroBar.value = ((fileIndex / totalFiles) * 100) + 1;
        app.refresh(); // Refresh the UI

        saveDocument(doc, file, outputFolder, preserveInputFormat, outputFormat, jpegQuality);
        progressBar.microBar.value = 75; // Set micro progress bar to 75% after saving
        progressBar.stMessage.text = "Saved file " + fileIndex + " of " + totalFiles;
        progressBar.macroBar.value = ((fileIndex / totalFiles) * 100) + 2;
        app.refresh(); // Refresh the UI

        doc.close(SaveOptions.DONOTSAVECHANGES);
        progressBar.microBar.value = 100; // Set micro progress bar to 100% after closing
        progressBar.stMessage.text = "Closed file " + fileIndex + " of " + totalFiles;
        progressBar.macroBar.value = ((fileIndex / totalFiles) * 100) + 3;
        app.refresh(); // Refresh the UI

        if (deleteSourceFiles) {
            file.remove();
        }

        progressBar.stMessage.text = "Processed file " + fileIndex + " of " + totalFiles;
        app.refresh(); // Refresh the UI after processing each file
    }

    progressBar.stMessage.text = "Processing complete.";
    progressBar.macroBar.value = 100;
    app.refresh(); // Final UI refresh after all files are processed
}

// Function to open a document in Photoshop.
function openDocument(file) {
    return app.open(file);
}

function analyzeAndResize(doc, analysisSize, resampleMethod) {
    var width = doc.width.as('px');
    var height = doc.height.as('px');
    var allRowColorCounts = analyzeDocument(doc, width, height, analysisSize);
    var scaleFactor = calculateScaleFactor(allRowColorCounts);
    resizeDocument(doc, scaleFactor, resampleMethod);
}

// Function to analyze the document and return color counts.
function analyzeDocument(doc, width, height, analysisSize) {
    var allRowColorCounts = [];
    var numSampleAreas = getSetting('numberOfSampleAreas'); // Add this line
    var positions = calculateSamplePositions(width, height, analysisSize, numSampleAreas); // Use the retrieved value here
    for (var i = 0; i < positions.length; i++) {
        var startX = positions[i][0];
        var startY = positions[i][1];
        for (var y = startY; y < startY + analysisSize; y++) {
            var uniqueColors = {};
            for (var x = startX; x < startX + analysisSize; x++) {
                var pixelLoc = [UnitValue(x, 'px'), UnitValue(y, 'px')];
                var colorSampler = doc.colorSamplers.add(pixelLoc);
                var color = colorSampler.color;
                var colorKey = color.rgb.hexValue;
                uniqueColors[colorKey] = true;
                colorSampler.remove();
            }
            var colorCount = Object.keys(uniqueColors).length;
            allRowColorCounts.push(colorCount);
        }
    }
    return allRowColorCounts;
}

// Function to calculate the scale factor based on color counts.
function calculateScaleFactor(allRowColorCounts) {
    var outlierThresholds = getSetting('outlierThresholds'); // Retrieve outlier thresholds
    var analysisSize = getSetting('analysisAreaSize'); // Retrieve analysis area size

    var lowerBound = Math.floor(allRowColorCounts.length * (outlierThresholds.low / 100));
    var upperBound = Math.ceil(allRowColorCounts.length * (outlierThresholds.high / 100));
    var validCounts = allRowColorCounts.slice(lowerBound, upperBound);
    var sum = validCounts.reduce(function(a, b) { return a + b; }, 0);
    var avgColorCount = sum / validCounts.length;
    return avgColorCount / analysisSize; // Use the retrieved analysis area size here
}

// Function to resize the document.
function resizeDocument(doc, scaleFactor, resampleMethod) {
    var resampleMethodEnum;
    switch (resampleMethod) {
        case 'Bicubic':
            resampleMethodEnum = ResampleMethod.BICUBIC;
            break;
        case 'Bilinear':
            resampleMethodEnum = ResampleMethod.BILINEAR;
            break;
        case 'Nearest Neighbor':
            resampleMethodEnum = ResampleMethod.NEARESTNEIGHBOR;
            break;
        case 'None':
            resampleMethodEnum = undefined;
            break;
        default:
            resampleMethodEnum = ResampleMethod.AUTOMATIC;
            break;
    }
    var newWidth = doc.width.as('px') * scaleFactor;
    var newHeight = doc.height.as('px') * scaleFactor;
    if (resampleMethodEnum !== undefined) {
        doc.resizeImage(UnitValue(newWidth, 'px'), UnitValue(newHeight, 'px'), doc.resolution, resampleMethodEnum);
    } else {
        doc.resizeImage(UnitValue(newWidth, 'px'), UnitValue(newHeight, 'px'), doc.resolution);
    }
}


// Function to save the document with the specified settings.
function saveDocument(doc, originalFile, outputFolder, preserveInputFormat, outputFormat, jpegQuality) {
    // Determines the file path and save options based on user settings and saves the document.
    var saveFile = determineOutputFile(originalFile, outputFolder, preserveInputFormat, outputFormat);
    var saveOptions = determineSaveOptions(saveFile, jpegQuality);
    doc.saveAs(saveFile, saveOptions, true);
}

function determineOutputFile(originalFile, outputFolder, preserveInputFormat, outputFormat) {
    var filename = originalFile.name.replace(/\.[^\.]+$/, ''); // Strip the original extension
    var extension = preserveInputFormat ? originalFile.name.split('.').pop().toLowerCase() : outputFormat.toLowerCase(); // Ensure the extension is lowercase
    var newFilePath = outputFolder + "/" + filename + "." + extension; // Construct the new file path
    return new File(newFilePath);
}

// Function to determine the save options based on the file type.
function determineSaveOptions(saveFile, jpegQuality) {
    // Selects the appropriate save options for the output file based on its format.
    var saveOptions;
    var extension = saveFile.name.split('.').pop().toLowerCase(); // Get the file extension
    switch (extension) {
        case 'jpg':
        case 'jpeg':
            saveOptions = new JPEGSaveOptions();
            saveOptions.quality = jpegQuality;
            break;
        case 'png':
            saveOptions = new PNGSaveOptions();
            break;
        case 'tiff':
            saveOptions = new TiffSaveOptions();
            break;
        default:
            saveOptions = new PhotoshopSaveOptions();
            break;
    }
    return saveOptions;
}

// Function to calculate positions for sampling squares within the document.
function calculateSamplePositions(width, height, squareSize, numSamples) {
    // Calculates the positions where color sampling will occur for analysis, based on the document dimensions.
    var positions = [];
    var cols = Math.ceil(Math.sqrt(numSamples)); // Determine the number of columns needed
    var rows = Math.ceil(numSamples / cols); // Determine the number of rows needed
    var spacingX = width / (cols + 1); // Calculate horizontal spacing between samples
    var spacingY = height / (rows + 1); // Calculate vertical spacing between samples

    // Loop through each row and column to calculate sample positions
    for (var row = 1; row <= rows; row++) {
        for (var col = 1; col <= cols; col++) {
            var posX = spacingX * col - squareSize / 2;
            var posY = spacingY * row - squareSize / 2;
            positions.push([posX, posY]); // Add the position to the array
        }
    }
    return positions.slice(0, numSamples); // Return only the number of samples needed
}

// The main entry point for the script execution.
(function main() {
    // Create and display the main dialog.
    var dialog = createDialog();
    // Show the dialog and store the result (1 for OK, 2 for Cancel).
    var result = dialog.show();

    // If the user clicked OK, proceed with processing.
    if (result === 1) {
        // Retrieve the user's selections from the dialog.
        var sourceFolder = dialog.sourceFolder;
        var outputFolder = getSetting('outputFolder');
        var preserveInputFormat = getSetting('preserveInputFormat');
        var outputFormat = getSetting('outputFormat');
        var jpegQuality = getSetting('jpegQuality');
        var resampleMethod = getSetting('resampleMethod');
        var deleteSourceFiles = getSetting('deleteSourceFiles');

        var analysisSize = getSetting('analysisAreaSize');

        var files = getFilesFromSource(sourceFolder, dialog.sourceFiles);
        // If no files are selected, alert the user and exit the script.
        if (files.length === 0) {
            alert("No files selected for processing.");
            return;
        }

        var progressBar = createProgressWindow("Processing Images", files.length);

        // Process each file.
        processFiles(files, progressBar, outputFolder, preserveInputFormat, outputFormat, jpegQuality, resampleMethod, analysisSize, deleteSourceFiles);

        progressBar.close();
    }
})();

function createProgressWindow(title, maxValue) {
    var win = new Window('palette', title);
    win.alignChildren = 'fill';
    
    // Add a static text label for the overall progress bar (macro).
    win.add('statictext', undefined, 'Overall Progress:');
    // Create the macro progress bar with a range from 0 to the total number of files.
    win.macroBar = win.add('progressbar', undefined, 0, maxValue);
    win.macroBar.preferredSize = [400, 20];
    
    // Add a static text label for the current file progress bar (micro).
    win.add('statictext', undefined, 'Current File Progress:');
    // Create the micro progress bar with a range from 0 to 100.
    win.microBar = win.add('progressbar', undefined, 0, 100);
    win.microBar.preferredSize = [400, 20];
    
    // Set the initial value of the macro progress bar to 0%.
    win.macroBar.value = 0;
    
    // Static text for displaying messages about the current operation.
    win.stMessage = win.add('statictext');
    win.stMessage.preferredSize.width = 400;
    
    // Function to update the progress bars and message text.
    win.updateProgress = function (macroValue, microValue, message) {
        // Update the macro progress bar's value.
        this.macroBar.value = macroValue;
        // Update the micro progress bar's value.
        this.microBar.value = microValue;
        // Update the message text to inform the user of the current operation.
        this.stMessage.text = message;
        // Refresh the window to reflect the changes.
        this.update();
    };
    
    // Show the progress window.
    win.show();
    return win;
}

2 Likes

@Imo Fyi. If you get an error on export. You may need to define the output folder in the settings Sub-Menu as well as in the main menu within the script. Haven’t figured out what’s causing that bug.

CleanShot 2024-03-17 at 17.15.34

Hey Weston,

Thanks for sharing this feature request. We have something like this in the works to enable this workflow.

First, we are allowing upscale/resize anywhere in the editing stack. Then, we are allowing multiple upscales/resizes so you can downscale an image (if the details are sparse to condense details) then upscale later in the stack all in Topaz Photo AI.

We don’t have plans for a detection model about this yet. That would be useful for automatically detecting images with this issue and fixing it.

Does that fit the feature that you are describing in your post?

3 Likes

Hey Lingyu,

Thanks for your response. While the upcoming features you mentioned are certainly beneficial, I believe the real game-changer would be a detection model.

In my testing, even images at the correct resolution but with excessive noise (the color sampling method I created picks this up as well) greatly benefit from a slight downscale before Topaz works its magic. The detection model would also be super beneficial for batches of varying sources.

Also with the right outlier exclusion parameters, the color sampling method in my script is non destructive. Photos with low noise and correct resolutions see little to no change when being processed through the script. So would be easy to use that as the detection (ie: IF detected scale factor adjustment is >.95, THEN leave unprocessed)

Appreciate the follow-up! Please let me know if there’s anything I can contribute to bringing this to life.

1 Like