advanced HDR
auto-stacking
Custom Scripting with Adobe -
Advanced HDR Stacking in Bridge
download the HDR AutoStacker script hereOne 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 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").
"Only one file selected... AutoStack this entire folder?"
,true,"AutoStacker Alert"
) == true)
{
"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.
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.
"StackUngroup"
);
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.
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.
"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
{
" "
+allThumbs.length+" files given, "
+thumbList.length+" valid files\n"
);
}
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.
"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).
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.
"Stack limit reached... "
);
makeStack();
j=0;
}
"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.
" pass.\n "
);
" 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.
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.
" 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.
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 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.
"Building a stack with "
+app.document.selections.length+" items\n"
);
"StackGroup"
);
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.
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.
"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.
The first step is to collect information about these files by retrieving the EXIF data, for which Bridge supports the
metadata.read() 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.
"http://ns.adobe.com/exif/1.0/"
,"DateTimeOriginal"
);
var Date2 = thumb2.metadata.read("http://ns.adobe.com/exif/1.0/"
,"DateTimeOriginal"
);
":"
);
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.
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.
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.
"Unknown"
}
": "
+timeDiff+" second"
+plurality(timeDiff)+" apart..."
);
In the unlikely event that the days were different, report the anomaly to the console and of course, return false on the comparison.
": Not from the same day..."
);
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.
""
}
"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.
"command"
, "AutoStack Advanced HDR ("
+bracketWindow+"s "
+stackLimit+"x)"
,"at the end of submenu/Stack"
,"autoStacker"
);
"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.
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.
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.

