Custom Scripting with Adobe -

Advanced HDR Stacking in Bridge

download the HDR AutoStacker script here

One of the more egregious omissions (in my experience) from Adobe Bridge is the lack of options for the built-in "Group into stacks" method. As someone who processes literally hundreds of HDR photos every month, the auto-stacking function would be a huge time-saver for me if only it could be customized. When capturing multiple variations of a shot in succession I may end up with many similar exposures over a ten or fifteen second span, and Bridge's built-in parser cannot keep track of which exposures are meant to stack together.

With no help from the internet and after years of manually sorting and stacking, I decided try and solve it myself. I consider myself to be proficient at the Javascript language, so I dove into the Adobe SDK and in the past year have developed many scripts to improve my workflow. In this article I will be talking about creating an "Autostacker" script for HDR (or panorama) images, with user-specified constraints. This guide assumes some basic knowledge of Javascript, and provides a more detailed explanation than the script's inline comments. The completed script I've made available for personal use and you can download it here.

The purpose of this guide is to explain my programming procedure so that you may modify the script to suit your fancy, or just get some example code to assist you in developing your own scripts. A professional Javascript programmer could certainly find improvements to my work, but for those like me who dabble here and there, this guide and script may be of use.

The script opens with our four user-changable variables, which define how the script will make stacks. They are created as global variables so that we can keep them here at the top of the document, for easy changing.

var bracketWindow = 2
// the maximum time (in seconds) between the first and last exposures var stackLimit = 3
// the maximum number of exposures allowed in one stack var fileTypes = "RAW,NEF,CR2"
// the file extensions accepted for stacking var addMenu = "true"
// whether to add the AutoStacker to Bridge's "Stacks" menu

The first two options here are the primary function of the script, and the most useful. In my work, for example, the base photo for the HDR is always shot around 1/125th, so even at +2 stops the exposures are within 2 seconds of each other. My camera is always bracketed at one-over one-under, so I never want more than three images in a stack.

#target bridge

This is a simple instruction to the Adobe scripting environment that allows the script to be run independently, i.e. double-clicking on the script file from the operating system environment. Not how I prefer to execute my scripts, but I include it as an easy way to keep track of which scripts are for which programs.

Now comes the primary muscle of the script, which we declare as a function called autoStack.

function autoStack() {

When Bridge initializes user-supplied scripts, it enables the ExtendScript Toolkit, a program that automatically opens when Bridge does and includes a console for logging script activity. Throughout the AutoStacker script I've included output commands to this console, so I know what's going on while the program runs (or why it crashes). They are mostly self-explanatory so this guide won't list them all, but here's the first console output, to advise that the script has been initiated and list its settings.

$.write("\n____ AUTOSTACKER 1.0 by Anthony van Winkle ____\n "+bracketWindow+" sec. exposure window, "+stackLimit+" item stack limit\n")

The first step to the program is to determine the scope of the AutoStacker, which means the range of files we are attempting to make into stacks. This range is defined by what files are selected, so the program expectation is that either (a) the user will select a group of files to make stacks from or (b) the user wants the entire directory made into stacks. Therefore, if no files are selected, select them all, or if some files are selected, leave them selected.

Having only one file selected doesn't fit either of these options, nor is there any presumable course of action, so it's best to ask the user what's going on. This can be done with the Window.confirm() command, which creates an alert box in Bridge that requires the user to say yes or no and returns a specified value based on their answer (in our case, yes returns "true").

// If the only one file is selected, let the user know with a popup: if (app.document.selections.length == 1) { // Prompt the user whether to run AutoStack on the entire folder if (Window.confirm("Only one file selected... AutoStack this entire folder?",true,"AutoStacker Alert") == true) { // If user agrees, deselect the one and continue with auto-selection app.document.deselectAll(); } else { // If user declines, exit the program return "Autostack failed; not enough files" } }

According to the behaviour rules, if no files are selected at the time of execution, the script is to attempt on all files in the current folder so all should be selected.

// If the user has not provided a multiple selection, assume all thumbnails in the folder if (app.document.selections.length == 0) { app.document.selectAll(); }

In order to process all the files, the approach logic is to create a list (allThumbs) containing all the selected files. This program is configured to allow only files of certain types to be stacked (typically RAW files), so we'll make a second list (thumbList) that will contain only the selected files that have an approved file extension (for the time being, it will contain nothing). The actual stacking process is also based on the current selection in Bridge, so after the lists are initialized we need to deselect everything.

// Create a list of all possible files var allThumbs = app.document.selections; // Create a list to store files of approved RAW formats var thumbList = []; // Ungroup any stacks that might exist in the selection app.document.chooseMenuItem("StackUngroup"); // Deselect all, for a clean slate app.document.deselectAll();

Our first action is to check all the selected files for ones with approved extensions, which will be done with the javascript iterator loop. We'll start at the first file (i=0) and run through however many files are in the list we made (allThumbs.length), going one file at a time (i++). This is done with a quick-and-dirty approach where we use the slice() function to take the last three characters of the filename, make them all uppercase, and see if that string appears anywhere in our list of accepted file types (the variable fileTypes, declared at the top). The list in fileTypes should be all uppercase, but just in case it's not, we'll force it to be for the comparison.

// Cycle through all possible files, filtering out everything but RAW file extensions for (i=0;i<allThumbs.length;i++) { // Check the extension of the file var fileType = allThumbs[i].path.slice(-3).toUpperCase(); // Make sure our comparison list is all uppercase fileTypes = fileTypes.toUpperCase(); // If a file has an accepted extension, add it to the thumbnail list if (fileTypes.indexOf(fileType) >=0) { thumbList.push(allThumbs[i]); } }

It's important to have some built-in error checking, so now's a good time to make sure that there actually were some accepted file types in the selection. If not, the program has no purpose. The Window.alert() function will pop up an alert in Bridge, similar to the Window.confirm() but without any choices. After that, close out to the console with the failure message.

// If there are no eligible files, let the user know with an error if (thumbList.length == 0) { Window.alert("AutoStacker is configured to process the following extensions:\n"+fileTypes+"\n\nThis list may be changed in AutoStack.jsx","AutoStacker Alert"); return "Autostack failed; no files of approved types found"; }

Hopefully that's not the case, and we can update the ExtendScript console with the status of our lists: namely, how many files were submitted initially and how many of those files were acceptable file types. Additionally, we'll make two variables to keep track of how many stacks we create over the course of the operation, and how many files we skip (no stack made).

else { // Update the console with the numbers of files in consideration $.write(" "+allThumbs.length+" files given, "+thumbList.length+" valid files\n"); } // Start counters for how many we stack and how many we skip, just for reference totalStacked = 0; totalSkipped = 0;

The main part of the operation is a little complex, but breaks down easily. There's a loop that iterates through each image in the list of approved files (thumbList), and for each image there's a loop as it tries to make a stack with the images that follow it. A curious discovery (after many hours of frustration) is that Bridge's built-in function to make stacks is not entirely encapsulated, and it also uses an iterative loop. The universal standard javascript loop iterator variable is i, which means that if we use i in the loop here and call Bridge to make a stack, Bridge's function will change the value of our variable and mess things up. So, for this loop we use k as the variable, and send another update to the console about our progress.

// This is the meat of the operation-- Cycle through the list of RAW thumbnails for (k=0;k<thumbList.length;k++) { // Inform the console what is being worked on $.write("Attempting to stack image "+thumbList[k].path.slice(-12)+"...\n ")

The next step is to start looking at the images near each other. In documentation, I refer to the starting image (at the top of the stack) as the "current" image and whatever one is under comparison as the "subsequent" image, so to start the stacking loop we'll select the "current" image from our above loop, and then use the variable j to count ahead (which I refer to as the subsequent counter). For example, j=1 means the image immediately after the current one, j=2 means the one after that, et cetera.We start with j=1, because if two files next to each other don't stack, none of the ones after them will (or at least they shouldn't, if the selection is in order).

// Select the current thumbnail to start a stack from[k]); // Set the subsequent thumbnail counter, which counts how // far away we've moved from the starting thumbnail j = 1;

Now we'll run another loop, but a "while" loop this time because we don't know how long it's going to run. Depending on a number of criteria, this loop will either (a) add the subsequent image to the selection or (b) try and make a stack of the selection. First it checks against our maximum stack limit to see if it's allowed to add another photo to the stack (the global variable stackLimit, defined at the top of the script). Then it checks to see if there's even another photo in the list. If either of these come back negative, the loop tries to make a stack with what it's got. Afterwords, it sets the j counter to zero, which means we're done dealing with the current image.

// While the counter is active, compare the first thumbnail to a subsequent one... while (j>0) { // If we're looking outside the defined stack-size limit, just make the stack and end the routine if (j >=stackLimit) { $.write("Stack limit reached... "); makeStack(); j=0; } // If we've reached the end of the thumbnail list, make the last stack and end the routine else if (k+j >= thumbList.length) { $.write("End of thumbnails reached... "); makeStack(); j=0; }

If the loop makes it this far, then there is a subsequent image we could add and it won't violate the stack limit defined by the user. To determine whether or not this subsequent image qualifies for the stack, we use the routine compareTimes() to find the difference between it and the current image (this routine is explained in detail later on). If compareTimes() determines that the subsequent image is close to the starting one, it returns true. If that happens, we want to add this subsequent image to our selection (the stack-to-be) and increment our counter so the loop can look ahead to the next subsequent image.

// If the subsequent thumbnail passes the time comparison within the given window, consider it a match else if (compareTimes(thumbList[k],thumbList[k+j],bracketWindow) == true) { $.write(" pass.\n "); // Add it to the selection and increment the subsequent counter[k+j]); j++; } If none of the above conditions have finished the loop, then the only logical conclusion is that the image failed the time test or is, for some other reason, not a match. It's still necessary to try and make a stack before we start on a new one.

// If the time test fails, the subsequent is not a match; make the stack without it else {$.write(" fail!\n ");makeStack();j=0;}

If this loop has ended (when j==0), it means that we tried to make a stack and are now done. Before the loop starts over, we have to deselect all or else we'll keep stacking everything we already stacked.

// After a stack has been made (or not), deselect all before moving on to the next stack app.document.deselectAll();

The subsequent loop will run many times for each image, but if the k<thumbList.length loop has ended, that means we've gone through all of the images in the thumbList and are, essentially, finished. The only thing left to do is write to the console with the status report, namely how many stacks were successfully made and how many images were left unstacked.

// All done! Report how many stacks were made and how many thumbnails were skipped $.write(totalStacked+" stack"+plurality(totalStacked)+" made (from "+(thumbList.length-totalSkipped)+" files); "+totalSkipped+" file"+plurality(totalSkipped)+" skipped\n"); return "all operations complete"; }

There are numerous places in the autoStack() loop where a stack might be attempted, so rather than writing the stack-making code every time, it's more efficient to write a separate function that we can call.

// Function to convert a selection into a stack and reset the counter function makeStack() {

This function runs based off of what is selected in Bridge, and has one of two options: either multiple items are selected that it can stack, or there aren't and it can't. We start with the former.

// If there are multiple selected, stack as a group if (app.document.selections.length > 1) {

If multiple items are selected, we'll first write to the ExtendScript console that a stack is being attempted, and then we'll invoke Bridge's built-in function that makes a stack. Since we're counting how many stacks the program makes, we'll also add one to our stack counter.

// Inform the console that we are making a new stack $.write("Building a stack with "+app.document.selections.length+" items\n"); // Call Bridge's built-in function to make a stack from the selection app.document.chooseMenuItem("StackGroup"); // Increment the counter of how many stacks we've made totalStacked++;

If you go back to our main loop, remember we are using k as our variable to keep track of which item in thumbList is the current image. If we've made a stack with multiple images, it would be redundant to advance the loop to the next image, because that image is already grouped in the stack we just made. The solution here is to force our main loop to skip the images we just stacked, which we do by manually advancing k by the number of images in the new stack. Remember that k is always automatically incremented after each loop, so we'll actually add one less than the number of images we stacked.

// Increment the loop counter to skip over thumbnails included in this stack, // so that we don't try to start new stacks from thumbnails inside other stacks k += j-1; }

That all works if multiple items are selected when makeStack() is called. If not, then it means the autostacker didn't find any matches to the current photo, so we'll add one to our "skipped images" counter and make a note in the console.

// If we get here with only one file selected, it is considered "skipped" else { // Increment the counter of how many files we've skipped totalSkipped++; // Inform the console that we skipped the file $.write("No stack created\n"); } }

Next, we define the compareTimes() function, which accepts two files and a window as arguments and returns true or false depending on whether the two files were created within the window.

// Function to compare the date and times of two thumbnails to see if they fall within a given window of each other (in seconds) function compareTimes(thumb1,thumb2,window) {

The first step is to collect information about these files by retrieving the EXIF data, for which Bridge supports the method. The method returns a string with a date and a time, so we can use the standard Javascript slice() and split() functions to separate out the numbers we want. The day will be a string of digits representing the year/month/date, the time will be a list containing hours, minutes, and seconds.

// Pull the EXIF data from the thumbnail and read the timestamp var Date1 ="","DateTimeOriginal"); var Date2 ="","DateTimeOriginal"); // Parse the day of the shoot from the EXIF data // (just in case two files happen to be in the same folder and were shot at the exact same time on different days) var Day1 = Date1.slice(0,10); var Day2 = Date2.slice(0,10); // Parse the timestamp of the photo into an array containing hours, minutes, seconds var Time1 = Date1.slice(12,20).split(":"); var Time2 = Date2.slice(12,20).split(":");

To compare the times exactly, we have to multiply out the hours and minutes into seconds and add them all together.

// Multiply out the timestamp to calculate the time of exposure in seconds Time1 = parseInt(Time1[0]*3600)+parseInt(Time1[1]*60)+parseInt(Time1[2]); Time2 = parseInt(Time2[0]*3600)+parseInt(Time2[1]*60)+parseInt(Time2[2]);

It's unlikely, but possible, that two photos could be taken within seconds of each other but on different days, so our first test will be to make sure the two images being compared are from the same day.

// If the shots were taken the same day... if (Day1 == Day2) {

The math operation here is pretty straightforward: subtract one timestamp from the other and get the absolute value of the difference. This is our fundamental operation to see if these are bracketed exposures for merging to HDR: if the difference is less than or equal to the exposure window it returns true, otherwise it returns false. This is another good place to update the console with our progress, as errors can occur here.

// Compare the two times to find the difference between exposures (in seconds) var timeDiff = Math.abs(Time2-Time1); // If there was a problem with any of the math, set the time difference to a known error value if (isNaN(timeDiff)) {timeDiff = "Unknown"} // Update the console with the time comparison $.write(thumb2.path.slice(-12)+": "+timeDiff+" second"+plurality(timeDiff)+" apart..."); // If the time difference is within our window of tolerance, the comparison is valid if (timeDiff <= window) { return true } // If not (or if it's an error), the comparison is invalid else { return false} }

In the unlikely event that the days were different, report the anomaly to the console and of course, return false on the comparison.

// If the exposures are from different days... else { // Update the console with the day comparison $.write(thumb2.path.slice(-12)+": Not from the same day..."); // Different days are obviously not a proper bracket, so the comparison is invalid return false } }

You may have noticed a function called plurality() appearing throughout the script, here it's defined. I'm a stickler for good form, so I want the console reports to be accurate and grammatically correct. The plurality() function can be inserted into console outputs to correctly add the plural "s" to nouns if the quantity is greater than one.

// Function to use proper plurality in reports based on a passed quantity function plurality(qty) { // If the quantity is one, don't return an "s" if (qty == 1) {return ""} // All other quantities should have an "s" else {return "s"} }

Everything thus far has been encapsulated within function declarations, so the script hasn't actually DONE anything yet. Here's where we determine what it does, based on the addMenu variable set way at the top. If the variable is true (the default value), then this script is going to add an item to the Bridge "Stacks" menu that invokes the autoStack() routine when selected. For convenience, the menu item will include the window and stack limit in its text.

// Create an entry in the "Stacks" Menu, if instructed by user if (addMenu == true) { // Add a menu item to the end of the "Stacks" menu in Bridge var stacksMenu = new MenuElement( "command", "AutoStack Advanced HDR ("+bracketWindow+"s "+stackLimit+"x)","at the end of submenu/Stack","autoStacker"); // Selecting this menu item runs the AutoStack function stacksMenu.onSelect = function() {autoStack(); } // Inform the console that the menu item has been added $.write("AutoStacker.jsx loaded into Stacks Menu sucessfully!\n") }

If the user has changed addMenu to false, that means they want to manually invoke the autoStack() routine by executing the script directly. In that case, that's exactly what we'll do.

// If a menu command was not instructed, assume the program has been called at runtime if (addMenu == false) { autoStack(); }

Finally, in the preferences menu of Bridge, under the Scripts submenu, the user can view the scripts they have installed. With proper formatting, this view can include a verbose name and description of the script. Adobe typically includes this information at the head of their scripts, but I prefer to keep my head clean and stick this supplemental information at the bottom.

/* @@@BUILDINFO@@@ AutoStacker.jsx */ /* @@@START_XML@@@ <?xml version="1.0" encoding="UTF-8"?> <ScriptInfo xmlns:dc="//" xml:lang="en_US"> <dc:title>AutoStacker Advanced HDR Grouping</dc:title> <dc:description>Advanced (customizable) script for automatically grouping bracketed exposures into stacks.</dc:description> </ScriptInfo> @@@END_XML@@@ */

And that's it!

about the author
Anthony van Winkle is the creator and director of Night Zero, the photographic novel of the zombie post-apocalypse. He processes more than four thousand HDR photographs every year, all shot on location in Seattle. Details and stories from behind-the-scenes can be found on the Night Zero production blog, updated Fridays.