Copy link to clipboard
Copied
I have a bunch of images that I have to manually merge in stacks of 3 together every time. They are all ordered by an alphanumeric filename, so I'm wondering if it's possible to merge them stacked automatically in a batch process in a way that I can set the name of the files so they are named and numbered correctly in a folder.
The images are all the same size, 3840 x 2160 and the stacked image is always the merged size of the images one beneath the other in a stack of three, 3840 x 6480, I have some images as an example. Is it possible to do it in a batch or at least semi-automatically in an action?
3 I can do with a File renamer so it's the least important one, please if you can do it without number 3
By @HeyoThere
Here is an updated 1.1 version, saving to a "Render" sub-folder in the input folder as PNG using a prompt for the base file name with zero padding sequential numbering:
/*
https://community.adobe.com/t5/photoshop-ecosystem-discussions/how-to-merge-3-images-togehter-stacked-in-a-batch/td-p/13922876
Stack 3 Document Sets to Vertically Stacked Layers.jsx
Stephen Marsh
v1.1
...
Copy link to clipboard
Copied
You will need a script, such as here:
Simply change the default setQty variable from 2 to 3.
var setQty = 2;
This will stack one layer over the other in sets of 3 images.
A simple action can be created to make the canvas 300% in height and to move the layers into vertical position:
The script has placeholder code to run a named action set and action before saving.
Copy link to clipboard
Copied
The following script is complete for your use case, no need for an action to be run by the script:
/*
https://community.adobe.com/t5/photoshop-ecosystem-discussions/how-to-merge-3-images-togehter-stacked-in-a-batch/td-p/13922876
Stack 3 Document Sets to Vertically Stacked Layers.jsx
Stephen Marsh
9th July 2023
This script requires input files from a single folder to be alpha/numeric sorting in order to stack in the correct set quantity.
*/
#target photoshop
if (!app.documents.length) {
try {
// Save and disable dialogs
var restoreDialogMode = app.displayDialogs;
app.displayDialogs = DialogModes.NO;
// Main script function
(function () {
// Select the input folder
var inputFolder = Folder.selectDialog('Please select the folder with files to process');
if (inputFolder === null) return;
// Limit the file format input, add or remove as required
var fileList = inputFolder.getFiles(/\.(png|jpg|jpeg|tif|tiff|psd|psb)$/i);
// Force alpha-numeric list sort
// Use .sort.reverse() for the first filename in the merged file
// Use .sort() for the last filename in the merged file
fileList.sort();
var setQty = 3;
// Validate that the file list is not empty
var inputCount = fileList.length;
var cancelScript1 = (inputCount === 0);
if (cancelScript1 === true) {
alert('Zero input files found, script cancelled!');
return;
}
// Validate the input count vs. output count - Thanks to Kukurykus for the advice to test using % modulus
var cancelScript2 = !(inputCount % setQty);
alert(inputCount + ' input files stacked into sets of ' + setQty + ' will produce ' + inputCount / setQty + ' output files.');
// Test if false, then terminate the script
if (cancelScript2 === false) {
alert('Script cancelled as the quantity of input files are not evenly divisible by the set quantity.');
return;
}
// Select the output folder
var outputFolder = Folder.selectDialog("Please select the folder to save to");
if (outputFolder === null) {
alert('Script cancelled!');
return;
}
// or
/*
// Create the output sub-directory
var outputFolder = Folder(decodeURI(inputFolder + '/Output Sets Folder'));
if (!outputFolder.exists) outputFolder.create();
*/
// Set the file processing counter
var fileCounter = 0;
// Loop through and open the file sets
while (fileList.length) {
// Sets of N quantity files
for (var a = 0; a < setQty; a++) {
try {
app.open(fileList.pop());
} catch (e) { }
}
// Set the base doc layer name
app.activeDocument = documents[0];
docNameToLayerName();
// Stack all open docs to the base doc
while (app.documents.length > 1) {
app.activeDocument = documents[1];
docNameToLayerName();
app.activeDocument.activeLayer.duplicate(documents[0]);
app.activeDocument = documents[0];
app.documents[1].close(SaveOptions.DONOTSAVECHANGES);
}
////////////////////////////////// Start doing stuff //////////////////////////////////
var idcanvasSize = stringIDToTypeID( "canvasSize" );
var desc295 = new ActionDescriptor();
var idheight = stringIDToTypeID( "height" );
var idpercentUnit = stringIDToTypeID( "percentUnit" );
desc295.putUnitDouble( idheight, idpercentUnit, 300.000000 );
var idvertical = stringIDToTypeID( "vertical" );
var idverticalLocation = stringIDToTypeID( "verticalLocation" );
var idtop = stringIDToTypeID( "top" );
desc295.putEnumerated( idvertical, idverticalLocation, idtop );
executeAction(idcanvasSize, desc295, DialogModes.NO);
activeDocument.selection.selectAll();
var idselect = stringIDToTypeID( "select" );
var desc393 = new ActionDescriptor();
var idnull = stringIDToTypeID( "null" );
var ref95 = new ActionReference();
var idlayer = stringIDToTypeID( "layer" );
var idordinal = stringIDToTypeID( "ordinal" );
var idbackwardEnum = stringIDToTypeID( "backwardEnum" );
ref95.putEnumerated( idlayer, idordinal, idbackwardEnum );
desc393.putReference( idnull, ref95 );
var idmakeVisible = stringIDToTypeID( "makeVisible" );
desc393.putBoolean( idmakeVisible, false );
var idlayerID = stringIDToTypeID( "layerID" );
var list14 = new ActionList();
list14.putInteger( 3 );
desc393.putList( idlayerID, list14 );
executeAction(idselect, desc393, DialogModes.NO);
var idalign = stringIDToTypeID( "align" );
var desc396 = new ActionDescriptor();
var idnull = stringIDToTypeID( "null" );
var ref96 = new ActionReference();
var idlayer = stringIDToTypeID( "layer" );
var idordinal = stringIDToTypeID( "ordinal" );
var idtargetEnum = stringIDToTypeID( "targetEnum" );
ref96.putEnumerated( idlayer, idordinal, idtargetEnum );
desc396.putReference( idnull, ref96 );
var idusing = stringIDToTypeID( "using" );
var idalignDistributeSelector = stringIDToTypeID( "alignDistributeSelector" );
var idADSCentersV = stringIDToTypeID( "ADSCentersV" );
desc396.putEnumerated( idusing, idalignDistributeSelector, idADSCentersV );
var idalignToCanvas = stringIDToTypeID( "alignToCanvas" );
desc396.putBoolean( idalignToCanvas, false );
executeAction(idalign, desc396, DialogModes.NO);
var idselect = stringIDToTypeID( "select" );
var desc393 = new ActionDescriptor();
var idnull = stringIDToTypeID( "null" );
var ref95 = new ActionReference();
var idlayer = stringIDToTypeID( "layer" );
var idordinal = stringIDToTypeID( "ordinal" );
var idbackwardEnum = stringIDToTypeID( "backwardEnum" );
ref95.putEnumerated( idlayer, idordinal, idbackwardEnum );
desc393.putReference( idnull, ref95 );
var idmakeVisible = stringIDToTypeID( "makeVisible" );
desc393.putBoolean( idmakeVisible, false );
var idlayerID = stringIDToTypeID( "layerID" );
var list14 = new ActionList();
list14.putInteger( 3 );
desc393.putList( idlayerID, list14 );
executeAction(idselect, desc393, DialogModes.NO);
var idalign = stringIDToTypeID( "align" );
var desc399 = new ActionDescriptor();
var idnull = stringIDToTypeID( "null" );
var ref98 = new ActionReference();
var idlayer = stringIDToTypeID( "layer" );
var idordinal = stringIDToTypeID( "ordinal" );
var idtargetEnum = stringIDToTypeID( "targetEnum" );
ref98.putEnumerated( idlayer, idordinal, idtargetEnum );
desc399.putReference( idnull, ref98 );
var idusing = stringIDToTypeID( "using" );
var idalignDistributeSelector = stringIDToTypeID( "alignDistributeSelector" );
var idADSBottoms = stringIDToTypeID( "ADSBottoms" );
desc399.putEnumerated( idusing, idalignDistributeSelector, idADSBottoms );
var idalignToCanvas = stringIDToTypeID( "alignToCanvas" );
desc399.putBoolean( idalignToCanvas, false );
executeAction(idalign, desc399, DialogModes.NO);
activeDocument.selection.deselect();
////////////////////////////////// Finish doing stuff //////////////////////////////////
// Delete XMP metadata to reduce final file size of output files
removeXMP();
// Save name + suffix & save path
var Name = app.activeDocument.name.replace(/\.[^\.]+$/, '');
var saveFile = File(outputFolder + '/' + Name + '_x' + setQty + '-Sets' + '.psd');
// var saveFile = File(outputFolder + '/' + Name + '_x' + setQty + '-Sets' + '.jpg');
// Call the save function
savePSD(saveFile);
//saveTIFF(saveFile);
//saveJPEG(saveFile);
//savePNG(saveFile);
// Close all open files without saving
while (app.documents.length) {
app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);
}
// Increment the file saving counter
fileCounter++;
///// Functions /////
function savePSD(saveFile) {
psdSaveOptions = new PhotoshopSaveOptions();
psdSaveOptions.embedColorProfile = true;
psdSaveOptions.alphaChannels = true;
psdSaveOptions.layers = true;
psdSaveOptions.annotations = true;
psdSaveOptions.spotColors = true;
// Save as
app.activeDocument.saveAs(saveFile, psdSaveOptions, true, Extension.LOWERCASE);
}
/* Not currently used, a placeholder to swap in/out as needed
function saveTIFF(saveFile) {
tiffSaveOptions = new TiffSaveOptions();
tiffSaveOptions.embedColorProfile = true;
tiffSaveOptions.byteOrder = ByteOrder.IBM;
tiffSaveOptions.transparency = true;
// Change layers to false to save without layers
tiffSaveOptions.layers = true;
tiffSaveOptions.layerCompression = LayerCompression.ZIP;
tiffSaveOptions.interleaveChannels = true;
tiffSaveOptions.alphaChannels = true;
tiffSaveOptions.annotations = true;
tiffSaveOptions.spotColors = true;
tiffSaveOptions.saveImagePyramid = false;
// Image compression = NONE | JPEG | TIFFLZW | TIFFZIP
tiffSaveOptions.imageCompression = TIFFEncoding.TIFFLZW;
// Save as
app.activeDocument.saveAs(saveFile, tiffSaveOptions, true, Extension.LOWERCASE);
}
*/
/* Not currently used, a placeholder to swap in/out as needed
function saveJPEG(saveFile) {
jpgSaveOptions = new JPEGSaveOptions();
jpgSaveOptions.embedColorProfile = true;
jpgSaveOptions.formatOptions = FormatOptions.STANDARDBASELINE;
jpgSaveOptions.matte = MatteType.NONE;
jpgSaveOptions.quality = 10;
// Save as
activeDocument.saveAs(saveFile, jpgSaveOptions, true, Extension.LOWERCASE);
}
*/
/* Not currently used, a placeholder to swap in/out as needed
function savePNG(saveFile) {
var pngOptions = new PNGSaveOptions();
pngOptions.compression = 0; // 0-9
pngOptions.interlaced = false;
// Save as
app.activeDocument.saveAs(saveFile, pngOptions, true, Extension.LOWERCASE);
}
*/
function docNameToLayerName() {
var layerName = app.activeDocument.name.replace(/\.[^\.]+$/, '');
app.activeDocument.activeLayer.name = layerName;
}
function removeXMP() {
if (!documents.length) return;
if (ExternalObject.AdobeXMPScript == undefined) ExternalObject.AdobeXMPScript = new ExternalObject("lib:AdobeXMPScript");
var xmp = new XMPMeta(activeDocument.xmpMetadata.rawData);
XMPUtils.removeProperties(xmp, "", "", XMPConst.REMOVE_ALL_PROPERTIES);
app.activeDocument.xmpMetadata.rawData = xmp.serialize();
}
}
// Restore saved dialogs
app.displayDialogs = restoreDialogMode;
// End of script notification
app.beep();
alert('Script completed!' + '\n' + fileCounter + ' combined files saved to:' + '\n' + outputFolder.fsName);
// Open the output folder in the Finder or Explorer
// outputFolder.execute();
}());
} catch (e) {
// Restore saved dialogs
app.displayDialogs = restoreDialogMode;
alert("If you see this message, something went wrong!" + "\r" + e + ' ' + e.line);
}
}
else {
alert('Stack 3 Document Sets to Vertically Stacked Layers:' + '\n' + 'Please close all open documents before running this script!');
}
https://prepression.blogspot.com/2017/11/downloading-and-installing-adobe-scripts.html
I'm happy to make changes as needed if further refinement is required.
Copy link to clipboard
Copied
Thank you, this is pretty much what I needed. If I may tell you just 3 refinements that would make the script even better for me:
1- The script asks me to select a folder where I want the new files to be saved. If possible I'd like the files to automatically be sorted out in a subfolder that would be created inside the folder where the original images were. The subfolder can be called Render.
2 - The files are being saved as PSD files, if possible I'd like them to be saved as PNGs
3- The file names being choosen are the "name of the third image being stacked" + "_x3-Sets", is it possible for the script to ask me a string to name the generated files and number them with "_0001" and so on?
Basically ask me for to type a name, if I type FileS1 it will name the first file generated would be called FileS1_0001 and the second FileS1_0002 and so on...
Again thank you for the help, you're amazing!
Copy link to clipboard
Copied
Copy link to clipboard
Copied
3 I can do with a File renamer so it's the least important one, please if you can do it without number 3
Copy link to clipboard
Copied
3 I can do with a File renamer so it's the least important one, please if you can do it without number 3
By @HeyoThere
Here is an updated 1.1 version, saving to a "Render" sub-folder in the input folder as PNG using a prompt for the base file name with zero padding sequential numbering:
/*
https://community.adobe.com/t5/photoshop-ecosystem-discussions/how-to-merge-3-images-togehter-stacked-in-a-batch/td-p/13922876
Stack 3 Document Sets to Vertically Stacked Layers.jsx
Stephen Marsh
v1.1 - 13th July 2023
This script requires input files from a single folder to be alpha/numeric sorting in order to stack in the correct set quantity.
*/
#target photoshop
if (!app.documents.length) {
try {
// Save and disable dialogs
var restoreDialogMode = app.displayDialogs;
app.displayDialogs = DialogModes.NO;
// Main script function
(function () {
// Select the input folder
var inputFolder = Folder.selectDialog('Please select the folder with files to process');
if (inputFolder === null) return;
// Limit the file format input, add or remove as required
var fileList = inputFolder.getFiles(/\.(png|jpg|jpeg|tif|tiff|psd|psb)$/i);
// Force alpha-numeric list sort
// Use .sort.reverse() for the first filename in the merged file
// Use .sort() for the last filename in the merged file
fileList.sort();
var setQty = 3;
// Validate that the file list is not empty
var inputCount = fileList.length;
var cancelScript1 = (inputCount === 0);
if (cancelScript1 === true) {
alert('Zero input files found, script cancelled!');
return;
}
// Validate the input count vs. output count - Thanks to Kukurykus for the advice to test using % modulus
var cancelScript2 = !(inputCount % setQty);
alert(inputCount + ' input files stacked into sets of ' + setQty + ' will produce ' + inputCount / setQty + ' output files.');
// Test if false, then terminate the script
if (cancelScript2 === false) {
alert('Script cancelled as the quantity of input files are not evenly divisible by the set quantity.');
return;
}
// Create the output sub-directory
var outputFolder = Folder(decodeURI(inputFolder + '/Render'));
if (!outputFolder.exists) outputFolder.create();
// File name prompt
var Name = prompt("Enter the base filename", "");
// Set the file processing counter
var fileCounter = 0;
// Set the sequential numbering
var numberLength = 4; // 1 to remove zero padding
var numberNo = 1; // Start number
// Loop through and open the file sets
while (fileList.length) {
// Sets of N quantity files
for (var a = 0; a < setQty; a++) {
try {
app.open(fileList.pop());
} catch (e) { }
}
// Set the base doc layer name
app.activeDocument = documents[0];
docNameToLayerName();
// Stack all open docs to the base doc
while (app.documents.length > 1) {
app.activeDocument = documents[1];
docNameToLayerName();
app.activeDocument.activeLayer.duplicate(documents[0]);
app.activeDocument = documents[0];
app.documents[1].close(SaveOptions.DONOTSAVECHANGES);
}
////////////////////////////////// Start doing stuff //////////////////////////////////
var idcanvasSize = stringIDToTypeID( "canvasSize" );
var desc295 = new ActionDescriptor();
var idheight = stringIDToTypeID( "height" );
var idpercentUnit = stringIDToTypeID( "percentUnit" );
desc295.putUnitDouble( idheight, idpercentUnit, 300.000000 );
var idvertical = stringIDToTypeID( "vertical" );
var idverticalLocation = stringIDToTypeID( "verticalLocation" );
var idtop = stringIDToTypeID( "top" );
desc295.putEnumerated( idvertical, idverticalLocation, idtop );
executeAction(idcanvasSize, desc295, DialogModes.NO);
activeDocument.selection.selectAll();
var idselect = stringIDToTypeID( "select" );
var desc393 = new ActionDescriptor();
var idnull = stringIDToTypeID( "null" );
var ref95 = new ActionReference();
var idlayer = stringIDToTypeID( "layer" );
var idordinal = stringIDToTypeID( "ordinal" );
var idbackwardEnum = stringIDToTypeID( "backwardEnum" );
ref95.putEnumerated( idlayer, idordinal, idbackwardEnum );
desc393.putReference( idnull, ref95 );
var idmakeVisible = stringIDToTypeID( "makeVisible" );
desc393.putBoolean( idmakeVisible, false );
var idlayerID = stringIDToTypeID( "layerID" );
var list14 = new ActionList();
list14.putInteger( 3 );
desc393.putList( idlayerID, list14 );
executeAction(idselect, desc393, DialogModes.NO);
var idalign = stringIDToTypeID( "align" );
var desc396 = new ActionDescriptor();
var idnull = stringIDToTypeID( "null" );
var ref96 = new ActionReference();
var idlayer = stringIDToTypeID( "layer" );
var idordinal = stringIDToTypeID( "ordinal" );
var idtargetEnum = stringIDToTypeID( "targetEnum" );
ref96.putEnumerated( idlayer, idordinal, idtargetEnum );
desc396.putReference( idnull, ref96 );
var idusing = stringIDToTypeID( "using" );
var idalignDistributeSelector = stringIDToTypeID( "alignDistributeSelector" );
var idADSCentersV = stringIDToTypeID( "ADSCentersV" );
desc396.putEnumerated( idusing, idalignDistributeSelector, idADSCentersV );
var idalignToCanvas = stringIDToTypeID( "alignToCanvas" );
desc396.putBoolean( idalignToCanvas, false );
executeAction(idalign, desc396, DialogModes.NO);
var idselect = stringIDToTypeID( "select" );
var desc393 = new ActionDescriptor();
var idnull = stringIDToTypeID( "null" );
var ref95 = new ActionReference();
var idlayer = stringIDToTypeID( "layer" );
var idordinal = stringIDToTypeID( "ordinal" );
var idbackwardEnum = stringIDToTypeID( "backwardEnum" );
ref95.putEnumerated( idlayer, idordinal, idbackwardEnum );
desc393.putReference( idnull, ref95 );
var idmakeVisible = stringIDToTypeID( "makeVisible" );
desc393.putBoolean( idmakeVisible, false );
var idlayerID = stringIDToTypeID( "layerID" );
var list14 = new ActionList();
list14.putInteger( 3 );
desc393.putList( idlayerID, list14 );
executeAction(idselect, desc393, DialogModes.NO);
var idalign = stringIDToTypeID( "align" );
var desc399 = new ActionDescriptor();
var idnull = stringIDToTypeID( "null" );
var ref98 = new ActionReference();
var idlayer = stringIDToTypeID( "layer" );
var idordinal = stringIDToTypeID( "ordinal" );
var idtargetEnum = stringIDToTypeID( "targetEnum" );
ref98.putEnumerated( idlayer, idordinal, idtargetEnum );
desc399.putReference( idnull, ref98 );
var idusing = stringIDToTypeID( "using" );
var idalignDistributeSelector = stringIDToTypeID( "alignDistributeSelector" );
var idADSBottoms = stringIDToTypeID( "ADSBottoms" );
desc399.putEnumerated( idusing, idalignDistributeSelector, idADSBottoms );
var idalignToCanvas = stringIDToTypeID( "alignToCanvas" );
desc399.putBoolean( idalignToCanvas, false );
executeAction(idalign, desc399, DialogModes.NO);
activeDocument.selection.deselect();
////////////////////////////////// Finish doing stuff //////////////////////////////////
// Delete XMP metadata to reduce final file size of output files
removeXMP();
// Save path, name + zero pad suffix
var saveFile = File(outputFolder + '/' + Name + "_" + zeroPad(numberNo, numberLength) + '.png');
// Increment the file number
numberNo++;
// Call the save function
savePNG(saveFile);
// Close all open files without saving
while (app.documents.length) {
app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);
}
// Increment the file saving counter
fileCounter++;
///// Functions /////
function savePNG(saveFile) {
var pngOptions = new PNGSaveOptions();
pngOptions.compression = 0; // 0-9
pngOptions.interlaced = false;
// Save as
app.activeDocument.saveAs(saveFile, pngOptions, true, Extension.LOWERCASE);
}
function docNameToLayerName() {
var layerName = app.activeDocument.name.replace(/\.[^\.]+$/, '');
app.activeDocument.activeLayer.name = layerName;
}
function removeXMP() {
if (!documents.length) return;
if (ExternalObject.AdobeXMPScript == undefined) ExternalObject.AdobeXMPScript = new ExternalObject("lib:AdobeXMPScript");
var xmp = new XMPMeta(activeDocument.xmpMetadata.rawData);
XMPUtils.removeProperties(xmp, "", "", XMPConst.REMOVE_ALL_PROPERTIES);
app.activeDocument.xmpMetadata.rawData = xmp.serialize();
}
}
function zeroPad(num, digit) {
var tmp = num.toString();
while (tmp.length < digit) {
tmp = "0" + tmp;
}
return tmp;
}
// Restore saved dialogs
app.displayDialogs = restoreDialogMode;
// End of script notification
app.beep();
alert('Script completed!' + '\n' + fileCounter + ' combined files saved to:' + '\n' + outputFolder.fsName);
// Open the output folder in the Finder or Explorer
// outputFolder.execute();
}());
} catch (e) {
// Restore saved dialogs
app.displayDialogs = restoreDialogMode;
alert("If you see this message, something went wrong!" + "\r" + e + ' ' + e.line);
}
}
else {
alert('Stack 3 Document Sets to Vertically Stacked Layers:' + '\n' + 'Please close all open documents before running this script!');
}
NOTE: I am not currently checking for or cleaning illegal characters in the file base name.
Copy link to clipboard
Copied
@Stephen_A_Marsh
It's awesome that it's even possible to an automatic naming, something happened with the new script though. I don't know if I did something wrong but in this new script is saves the files all backwards, that meaning the last 3 images sorted in an alpha numeric number become the first new stacked image on the new files (their position on the canvas are still correct though). On the previous script the files were being made correctly. The script even renders the images backwards, it starts with the last images and the previous one started with the first images, so I don't know what is happening there.
Except for that everything is working correctly.
Copy link to clipboard
Copied
I didn't intentionally change any code that would have affected file processing sequence or layer stacking order. I had a quick look and couldn't see any differences that would account for this.
You could try changing from:
fileList.sort();
To:
fileList.sort().reverse();
Copy link to clipboard
Copied
Copy link to clipboard
Copied
You can use a scripting language like Python to achieve this. Here's a step-by-step approach:
Install the required libraries by running the following command in your terminal:
pip install numpy opencv-python
import os
import cv2
import numpy as np
# Set the input and output directories
input_dir = 'input_images/'
output_dir = 'output_images/'
# Create the output directory if it doesn't exist
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Get the list of image filenames in the input directory
image_files = sorted([f for f in os.listdir(input_dir) if f.endswith('.jpg')])
# Iterate over the image files and merge them in stacks of three
for i in range(0, len(image_files), 3):
# Load the three images
image1 = cv2.imread(os.path.join(input_dir, image_files[i]))
image2 = cv2.imread(os.path.join(input_dir, image_files[i + 1]))
image3 = cv2.imread(os.path.join(input_dir, image_files[i + 2]))
# Stack the images vertically
merged_image = np.vstack((image1, image2, image3))
# Save the merged image with a new name
output_filename = f"merged_{i // 3}.jpg"
cv2.imwrite(os.path.join(output_dir, output_filename), merged_image)
print("Image merging completed.")
python image_merge.py
The script provided automatically merges images in stacks of three, saving the merged images with a "merged_" prefix and a stack index in the output directory. Input images should be named to ensure correct alphanumeric sorting. The script supports JPEG format by default but can be modified for other formats.
This will help you.