With Support from Apple
(Page 3 of 3 pages for this article < 1 2 3)
Monday, March 16, 2009
FXScript: FCP’s Most Under-appreciated Feature
Adam Wilt | 03/16
I whip up a dead-pixel masker for a feature, and you get the filter for free, along with a quick tour of FXScript.
If you’re so smart, geek-boy, show us your code!
OK, I will. Let’s have a walk-through, so those new to FXScript can see what’s involved, and you other geek-boys can leave snide comments about how inefficient my solution to the problem is—or correct me gently where my assumptions and interpretations of the scanty FXScript docs has led me astray.
scriptid “Pixel Mask AJW” //DO NOT LOCALIZE
filter “Pixel Mask [ajw]”;
group “AJW’s Filters”;
The scriptid is a string that uniquely identified this script from all others. The “do not localize” comment, copied verbatim from every other FXScript I’ve seen, is a reminder not to translate this string into other languages (yes, I could have a version of this script with a French UI, or a Kurdish one, or Elvish or Klingon [note to self: make sure that the ProApps framework supports Klingonese characters before trying this], though the Plugins folder doesn’t have separate folder for different language versions).
The “filter” line tells FCP that (a) it’s a filter, not a generator or transition, and (b) what the displayed name of the filter is. It’s common practice, though not universal, to put some sort of identifying tag in the name (like the “[ajw]” suffix) to distinguish your filters from Apple’s own filters and other third-party filters: Joe Maller calls all his filters “Joe’s” whateveritis, Digital Heaven prefixes all their names with “DH_”, and so on.
“Group” tells FCP where to display the filter; in the user interface it appears as the submenu (in the Effects menu) or the bin (in the Browser) where the filter appears. Again, it’s common (and possibly universal) practice to put all your filters in your own group, so they don’t get mixed up with other third-party filters in FCP’s UI.
// hacked together 8 Feb 2009 by Adam J. Wilt, http://www.adamwilt.com/
// 15 Feb 2009 - fixed blit src bugs, opacity bugs; improved UI
// 16 March 2009 - changed default mask src to all 4 edges
// Use to hide stuck pixels
Comments are useful to track version changes and to explain what’s going on, like my detailed and extremely insightful comment, “Use to hide stuck pixels”. Well, d’oh!
input pixelLoc, “Location”, Point, 0, 0
input trimX, “Horizontal Trim”, slider, 0, -5, 5 snap -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5
input trimY, “Vertical Trim”, slider, 0, -5, 5 snap -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5
input pixelHeight, “Height”, slider, 2, 1, 100 ramp 95
input pixelWidth, “Width”, slider, 2, 1, 100 ramp 95
input blurType, “Mask Source”, Popup, 4, “Left Edge”, “Left & Right Edges”, “Top & Bottom Edges”, “All Four Edges”
input useCol, “Use Test Color”, checkbox, 1
input col, “Test Color”, Color, 0, 255, 255, 255
input unused, “¬© 2009 Adam J. Wilt”, label, “”
“Input” statements define the user interface that appears in the Filters tab of FCP’s Viewer:

Compare the controls shown here with the code that generated them.
Some notes:
- The “snap” parameter is supposed to keep sliders from taking on non-integer values, but I’m not sure if it’s actually useful. You can still type fractional values in the text entry field.
- “Ramp 95” is what gives the Height and Width sliders their nonlinear scales. Oddly, the tick marks on the displayed sliders compress in the wrong direction; they should bunch up at the high end where the numbers get closer together, instead of at the low end where numbers are farther apart! That’s just the way it is.
The blurType popup has a default value of 4, which causes the “All Four Edges” value to be displayed. I made that change today before uploading the filter to my website, because that’s the value I’ve been using the most. If you want a different default, you can edit your own copy to pick a different value.
InformationFlag(“YUVaware”)
InformationFlag is a hint to the system. “YUVaware” tells FCP that this filter can work without converting the frame buffer formats to RGB, so super-whites survive this filter and unneeded color-space conversions are avoided.
There are other InformationFlag values, mostly undocumented by Apple, which various people have uncovered; the best listing I’ve found is at http://www.fxscriptreference.org/search.php?search=InformationFlag
code
point srcpoly[4], destpoly[4]
float height, width
boundsOf(Src1, srcpoly);
boundsOf(Dest, destpoly);
The keyword “code” separates all the declarations and definitions above from the the stuff that actually does the work.
I define two point arrays to hold the corner points of source and destination rectangular regions (simple polygons), and fill them with the bounds of the system-defined Src1 (the source input frame, e.g. the frames of the clip this filter is applied to) and Dest (the frame that results from the action of the filter).
Many FXScripts start off this way: learning how big the frames are that they’re working on, and setting variables to match. That’s how the same filter can work on a 720x480 DV-format frame and a 1920x1080 HD frame with no size-specific user input needed.
// scale pixelLoc from the range 0-1 to 0-screensize
DimensionsOf(Src1, width, height)
pixelLoc.x *= width
pixelLoc.y *= height
// offset by half the size of affected area
pixelLoc.x -= (pixelWidth / 2) - 0.5 - trimX
pixelLoc.y -= (pixelHeight / 2) - 0.5 - trimY
pixelLoc.x = integer(pixelLoc.x)
pixelLoc.y = integer(pixelLoc.y)
Remember that comment about how a point variable is defined? We need to convert 0.0-1.0 numbers into an actual pixel location, so we scale ‘em, offset ‘em by half our mask size (so the mask is centered on our target point) and by our trim values, and then make ‘em into integer values so we position our mask on whole pixels.
// copy src to dest
blitrect(src1, srcpoly, Dest, destpoly)
// define dest region for blit or fill
makerect(destpoly, pixelLoc.x, pixelLoc.y, pixelWidth, pixelHeight)
We blit (bit-block transfer, e.g. copy a rectangular block of bits or pixels) the source image into the destination buffer, since we want to have the source image output with only a small change around the dead pixel.
Then we compute the destination rectangle to be filled by our pixel mask based on the previously-computed corner location, and the height and width of the mask.
if useCol
region reg
MakeRegion(destpoly, reg)
fillregion(reg, dest, col)
If we’re using the test color, simply fill in that destination rectangle with the test color, and we’re done.
If we’re using the real mask, things are a bit more complex… grab yourself a sandwich and a cup of tea, and dive in…
else
// each blit layer should contribute opacity of (1 / layer) to the final composite
float layer
layer = 1
// blurtypes:
// 1 = left edge
// 2 = left & right
// 3 = top & bottom
// 4 = left, right, top, bottom
if blurType != 3 // not top & bottom only
makerect(srcpoly, pixelLoc.x-1, pixelLoc.y, 0, pixelHeight)
blitrect(src1, srcpoly, Dest, destpoly) // blitrect = blit with opacity of 1
layer++
if blurType == 2 or blurType == 4 // left & right, or all 4 edges
makerect(srcpoly, pixelLoc.x+pixelWidth, pixelLoc.y, 0, pixelHeight)
blit(src1, srcpoly, Dest, destpoly, 1 / layer)
layer++
end if
end if
if blurType >= 3 // includes top & bottom
makerect(srcpoly, pixelLoc.x, pixelLoc.y-1, pixelWidth, 0)
blit(src1, srcpoly, Dest, destpoly, 1 / layer)
layer++
makerect(srcpoly, pixelLoc.x, pixelLoc.y+pixelHeight, pixelWidth, 0)
blit(src1, srcpoly, Dest, destpoly, 1 / layer)
end if
end if
There, got that?
This block of code works by blitting the selected border edge pixels over the mask area. The first blit is done with an opacity of 1.0. The second blit (if performed) uses an opacity of 0.5: if we want to average the top and bottom border pixels, each should contribute half, so using a half-opacity second blit lets half the first blit combine with the second to average ‘em out. If we do a third layer, its opacity wants to to be 1/3rd (letting 2/3 of the previous blits through, each of which is itself 1/2 the original value, thus all three layers contribute 1/3 the value of the final pixel), and so on: each new layer wants an opacity of 1 / (layer number).
Once you get that aspect, the rest of the block is simply parsing the blurType parameter to determine which direction(s) to blit new pixels from, what opacity to apply to them, and then performing the blit(s).
It’s worth noting that the source blocks for the blits have a width or height values of 0, not 1, for the narrow dimension: apparently the blit functions march across their regions using an algorithm like:
stepSize = width / numberOfSteps
for (x = startPixel, step = 0; step < numberOfSteps; step++, x += stepSize)…
so using a width value (or height value, where appropriate) of zero causes the source rectangle to be one pixel wide or high, not zero. This was the bug I fixed a week after writing the code; I had ones in the code, not zeroes, and I noticed that when testing along the diagonal edge of a Two Color Ray pattern that I was seeing a fade in pixel values across the mask, not consistent values: my source rectangles were two pixels wide (or high). The blit functions do an excellent job of interpolating pixels when stretching or shrinking a source rectangle to fill a destination rectangle!
Some side notes:
FXScript doesn’t seem to be case-sensitive.
Semicolons are only necessary as statement separators if you have multiple statements on the same line. As a C/C++ programmer for way too many years, though, they just pop out in my code whether I want them there or not.
New versions of FCP sometimes add new functions or InformationFlag parameters, and sometimes change the way existing functions work (or so folks like Joe Maller and Graeme Nattress tell me, though I haven’t had any of my filters break across versions). Apple doesn’t do a very good job at keeping people updated about the changes, either: the docs date from FCP 4 in 2003, fercryinoutloud. Hey, it could be worse; it could be coding for the web! FXScript doesn’t change that fast or that drastically, fortunately.
Dude, my brain like totally hurts now. Thanks for nothing, geek-boy!
Yeah, OK, maybe roll-your-own FXScript filters aren’t for everyone. At least you’ve gotten a glimpse into what coding is like, and consider that the long-suffering programmers on the Apple ProApps teams have to deal with this sort of thing—and worse—every day. Think of that the next time you’re dealing with some whiney client who can’t seem to decide what shade of fuschia his logo should be rendered in, and just give thanks that you aren’t having to grind code for a living!
If, however, you’re of the mindset that you want to fiddle with images in a programmatic manner, there’s probably no easier way to do so than through FXScript. It’s free with every copy of FCP. The official documentation (such as it is) is on Apple’s website, Joe Maller’s original FXScript Reference pages have many of the details Apple’s docs are missing and narratives of how he built his filters, while Joe Maller’s newer FXScript Reference fills in some of the gaps that have developed since 2002.
Have fun.
I came all this way, and I don’t even get a frackin’ T-shirt?
What do I look like, CafePress or something? Sorry, Binky, no T-shirts ‘round here!
What I do have for you is a bundle of FXScript filters. Quit your griping, they’re free, aren’t they?
http://www.adamwilt.com/downloads/AJW’s Filters.zip.
These include:
- Channel Balance - adjust the gain and level of RGB or YCrCb (YUV) channels independently.
- Channel Blur - selectively blur RGB or YCrCb channels independently. If you select YCrCb, and blur Cr and Cb about 2-4 units, it makes a passable-fair chroma smoother for 4:2:0 progressive-scan footage.
- Channel Offset - tweak the positions of RGBA or YCrCbA channels independently. In YCrCb mode, it’s useful for shifting chroma to correct for multiple generations of analog, color-under recording.
- Field Balance - adjust luma and chroma gains on Fields 1 and 2 of an interlaced image independently. Very useful for correcting field imbalances caused by mismatched head amps in dual-head, helical-scan analog VTRs.
- H. Chroma Blur - an alternative to FCP’s own 4:1:1 and 4:2:2 chroma smoothers (and predating them, grin), this lets you adjust the horizontal blurring and offsetting of the chroma signal to correct for color-subsampled recording formats like DV.
- Pixel Mask - that darned filter we’ve been discussing for, like, three pages already?
- Y/C Delay - this is a streamlined version of Channel Offset: it just repositions chroma with respect to luma so that colors that have shifted from too many generations of color-under recording can get put back where they belong.
Download them and unzip them; you’ll get a folder called “AJW’s Filters”. Drop it into /Library/Application Support/Final Cut Pro System Support/Plugins. Start (or restart) FCP. Hey presto: new filters! Use them, take them apart, rebuild them, modify them for your own nefarious needs based on your own brilliant brainstorms. Throw some or all of them away if they bore you. That’s freedom, isn’t it?
Uh, what were those references again?
Apple’s official document: Using FXScript, a 1.2 Mbyte PDF.
Joe Maller’s tutorials and notes: FXScript Reference for Final Cut Pro.
Joe Maller’s Web 2.0-esque FXScript Reference for post-2002 updates. (Look at the sidebar on the right side of the homepage for various vendors of FXScript filters.)
(Page 3 of 3 pages for this article < 1 2 3)
Thank you for the informative examples.
I have looked all over for information on what should be a very simple task, and cannot seem to find any. How do you combine two fxScript effects into a single script?
The problem seems to be that the first script starts with src1 and spits results into dest, but then the second one is trying to do the same thing. I have not found a way of copying src1, src2, or dest into another variable. I thought the “image” variable type should work, but how?
Posted by .(JavaScript must be enabled to view this email address) on 08/31 at 02:37 AM
as you’ve guessed you can just create and use an image buffer (with the same dimensions of your src/dest) and use it to store the result of the first script (instead of dest) and to feed the second script (instead of src)
eg you could set up a buffer “buf” as below
dimensionsof(dest, framesize.h, framesize.v);
image buf[framesize.h][framesize.v];
then use it directly in place of “dest” or “src1” etc as needed, or simply use it to hold such info using a simple x=y type instruction
eg buf = dest;
hope it helps
Andy
Posted by Andy on 08/31 at 04:40 AM
oops ... forgot to include the definition of the “framesize” variable too
dimensionsof(dest, framesize.h, framesize.v);
image buf[framesize.h][framesize.v];
should read:
point framesize;
dimensionsof(dest, framesize.h, framesize.v);
image buf[framesize.h][framesize.v];
Posted by Andy on 08/31 at 04:52 AM
Thank you, Andy, for such a prompt response! I will try it out tomorrow.
Posted by .(JavaScript must be enabled to view this email address) on 08/31 at 05:56 AM
It works! But it took me some time with other issues, like duplicate variable names from combining the effects, before I actually saw it work. I have now managed to combine three effects on one video clip, duplicate the clip into another variable, and add two more effects, with the end result being a single reflected image effect.
I still haven’t figured out how to assign a value to a point variable in my code (such as the center point). As everyone says, the documentation is rather skimpy, and it seems to tell us only how to define a point variable, but not how to give it a value.
Anyhow, I’ll keep learning. Thanks for the help!
Posted by .(JavaScript must be enabled to view this email address) on 09/01 at 07:28 PM
here you go, example code for defining and assigning a value to a point variable:
point p;
p = {0, 0};
Posted by Andy on 09/01 at 07:41 PM
Again, thank you! I had tried p = [0, 0]; and p = (0, 0); but neither of them worked. Knowing the syntax makes all the difference! And the error message I got was that there was a missing parenthesis at the comma position! (But of course, for an x,y coordinate point, p = (0); just wouldn’t make much sense.) Thank you!
BTW, are there any forums for fxScript that would be more appropriate for asking questions like these? maybe a place where I could also post my code? (Someone might appreciate it, and if they don’t, they might improve it for me.
Posted by .(JavaScript must be enabled to view this email address) on 09/01 at 09:54 PM
> are there any forums for fxScript
None that I know of, but you can always use Apple’s own Final Cut Pro discussions forum to get your questions/code out there.
http://discussions.apple.com/forum.jspa?forumID=939
If you’re serious about this stuff then you should join Apple’s pro-app-dev mailing list
http://lists.apple.com/mailman/listinfo/pro-apps-dev
Posted by Andy on 09/01 at 10:49 PM
I’ve been learning a lot about fxScript. One of the more frustrating pieces of knowledge is just how little documentation on it there is. Many functions are only partly documented, and a few, seemingly not at all.
I am working with CJK characters, and need a way of applying an fxScript that will automatically exchange every occurrence of an ancient form of the comma with a modern equivalent. I would also like to be able to parse the text for word splits. (I’m making a text wrap function which will automatically insert return characters to fit a user-adjustable line width.)
Do you know of any way to catch CJK characters (double byte) and to insert them back into the text?
Posted by .(JavaScript must be enabled to view this email address) on 10/15 at 03:21 AM
The “Using FXScript” PDF implies that most of the String and Text functions are double-byte compatible (pages 47-48). If they actually work, you may be able to use FindString to look for both the archaic comma form and for whitespace (word break) characters; CharsOf to break the text into smaller substrings; and MeasureString to check widths for line wraps. Then it’s just the tedious process of marching through your source string(s), breaking them into words, replacing the commas, and reassembling them them with newlines inserted at the appropriate places.
If that doesn’t work out, you may have better luck pre-processing your text with AppleScript (or even a small Cocoa program), and pasting the result back into FCP’s text generators, though you’ll have to work out the correspondence between text metrics in AppleScript and the text metrics in FCP.
Posted by Adam Wilt on 10/15 at 09:49 AM
|