Log in

View Full Version : MCTD and memory


flyvholm
8th February 2015, 03:22
I'm new to Avisynth, jumping in because I'm looking to do some noise reduction. I'm working with DSLR timelapse image sequences that I'm making a film from, importing the sequences in Adobe After Efffects for post processing (such as pan/zoom). For best results I should apply the noise reduction to the original image sequences where most of the image is static from frame to frame, and due to the further processing I generally can't reduce the resolution before denoising. Images are mostly 12MP, in some cases 21MP.

The only hardware I have available for the task is an ASUS G73JH laptop (i7-720QM, 8GB RAM, ATI Mobility Radeon HD 5870 w. 1 GB RAM).

As you can guess, my initial attempts were not successful. Using Avisynth 2.5.8 because 2.6 made MCTD crash. Starting out very basic:
Import("C:\Path\to\MCTemporalDenoise.v1.4.20.avsi")
ImageSource("I:\Path\to\DSC_%05d.jpg", 0, 99, 23.976)
ConverttoYv12()
MCTemporalDenoise(settings="low")
ConvertToRGB24()
This will open in VirtualDub, but image is black and status line at bottom reveals an out-of-memory error ("I think we have run out of memory folks!"). Using gpu=true crashes with an E_OUTOFMEMORY error. Sadly, this is just the 12MP images.

Questions:
1) Is it possible to do some memory management to prevent errors? SetMemoryMax() seems to make no difference.
2) How do I most effectively lower MCTD's memory consumption without crippling it too much?

poisondeathray
8th February 2015, 03:39
What are the dimensions of the image sequence ?

Can you open the image sequence up without MCTD ?

EDIT: sorry I didn't see the 12MP

StainlessS
8th February 2015, 03:45
Have you tried with "MCTemporalDenoise(settings="low")" commented out ?
Have you tried low setting for SetMemoryMax(), eg SetMemoryMax(256) ?

Some of the functions in Avisynth do not cope with frame bigger than about 4Kx4K eg AverageLuma() (at least up to v2.6 Alpha 5).

EDIT: @PDR, 12 -> 21 MP.

flyvholm
8th February 2015, 06:15
The 12MP images are 4256x2832 pixels, 21MP are 5616x3744. Both resolutions open fine when MCTD is commented out. I did try SetMemoryMax(256), both with or without GPU, and it made no difference in either case.

Monitoring the task manager, VirtualDub "only" takes ~870MB of memory with a 12MP sequence opened. This is with > 50% memory free. When trying to open a 21MP sequence VirtualDub fails with an out-of-memory error when hitting 1GB (memory usage appears to be directly proportional to resolution btw). But from what I read, 32-bit Avisynth + VirtualDub should be able to use up to 2GB. So I wonder why I run short at < 1GB? With 12MP sequence open VirtualDub status bar shows:

Avisynth read error: GetFrameBuffer: Returned a VFB with a 0 data pointer! size=91527424, max=268435456, used=294194112 I think we have run out of memory folks!

Overdrive80
8th February 2015, 07:36
Try SetMemoryMax(1024)

Groucho2004
8th February 2015, 11:47
The 12MP images are 4256x2832 pixels, 21MP are 5616x3744. Both resolutions open fine when MCTD is commented out. I did try SetMemoryMax(256), both with or without GPU, and it made no difference in either case.

Monitoring the task manager, VirtualDub "only" takes ~870MB of memory with a 12MP sequence opened. This is with > 50% memory free. When trying to open a 21MP sequence VirtualDub fails with an out-of-memory error when hitting 1GB (memory usage appears to be directly proportional to resolution btw). But from what I read, 32-bit Avisynth + VirtualDub should be able to use up to 2GB. So I wonder why I run short at < 1GB? With 12MP sequence open VirtualDub status bar shows:

Avisynth read error: GetFrameBuffer: Returned a VFB with a 0 data pointer! size=91527424, max=268435456, used=294194112 I think we have run out of memory folks!
It's not surprising that you get an out of memory error with that resolution and a temporal filter.
Avisynth tries to allocate memory for the number of frames requested by the filter and throws the error if there is not enough available. The memory usage you read in Taskmanager shows the current state.

One way to increase the available memory to Avisynth is to pipe the script to the client application (avs2yuv, etc). If the piping application is "Large Address Aware", up to 4 GB can be allocated on a 64 Bit OS.
I have not used MCTD but maybe there are settings with less memory usage.
Lastly - How about a non-temporal noise filter?

StainlessS
8th February 2015, 18:22
Perhaps try temporal Hqdn3d() or FFT3DFilter() or Spatial Vaguedenoise().

EDIT: I'm not aware of any RGB temporal denoisers to avoid colorspace conversions, anybody suggestions ?

raffriff42
9th February 2015, 02:27
What if you processed the frames in slices to lower peak memory requirements?
Say you split each into 9 smaller frames, processed them, then joined them together again?

Sounds simple, but you need overlap between the slices for a smooth result.
Here's some code for you to try (it seems to work OK in a quick test): Assert(IsClip(Last) && IsYV12(Last),
\ "YV12 source required for noise filter")
Assert(Last.Width>48 && Last.Height>48,
\ "source resolution too low")

pad=8 ## size of mirror border / overlapped area as rq'd
pad2=2*pad

wid=Width
hgt=Height
wid3 = Round(wid/3.0)
hgt3 = Round(hgt/3.0)
new_wid = wid3 - (wid3 % 2)
new_hgt = hgt3 - (hgt3 % 2)

## create the 9 slices
## (slice numbers refer to position in 'numeric keypad' aka Subtitle notation)
C7=Crop(0, 0, new_wid+pad, new_hgt+pad)
\ .MirrorBorderLTRB(pad, pad, 0, 0)
\ .Crop(0, 0, new_wid+pad2, new_hgt+pad2)

C8=Crop(new_wid-pad, 0, new_wid+pad2, new_hgt+pad)
\ .MirrorBorderLTRB(0, pad, 0, 0)
\ .Crop(0, 0, new_wid+pad2, new_hgt+pad2)

C9=Crop(2*new_wid-pad, 0, wid-2*new_wid+pad, new_hgt+pad)
\ .MirrorBorderLTRB(0, pad, pad, 0)
\ .Crop(0, 0, new_wid+pad2, new_hgt+pad2)


C4=Crop(0, new_hgt-pad, new_wid+pad, new_hgt+pad2)
\ .MirrorBorderLTRB(pad, 0, 0, 0)
\ .Crop(0, 0, new_wid+pad2, new_hgt+pad2)

C5=Crop(new_wid-pad, new_hgt-pad, new_wid+pad2, new_hgt+pad2)
\ .MirrorBorderLTRB(0, 0, 0, 0)
\ .Crop(0, 0, new_wid+pad2, new_hgt+pad2)

C6=Crop(2*new_wid-pad, new_hgt-pad, wid-2*new_wid+pad, new_hgt+pad2)
\ .MirrorBorderLTRB(0, 0, pad, 0)
\ .Crop(0, 0, new_wid+pad2, new_hgt+pad2)


C1=Crop(0, 2*new_hgt-pad, new_wid+pad, hgt-2*new_hgt+pad)
\ .MirrorBorderLTRB(pad, 0, 0, pad)
\ .Crop(0, 0, new_wid+pad2, new_hgt+pad2)

C2=Crop(new_wid-pad, 2*new_hgt-pad, new_wid+pad2, hgt-2*new_hgt+pad)
\ .MirrorBorderLTRB(0, 0, 0, pad)
\ .Crop(0, 0, new_wid+pad2, new_hgt+pad2)

C3=Crop(2*new_wid-pad, 2*new_hgt-pad, wid-2*new_wid+pad, hgt-2*new_hgt+pad)
\ .MirrorBorderLTRB(0, 0, pad, pad)
\ .Crop(0, 0, new_wid+pad2, new_hgt+pad2)

## join the 9 slices temporally - add 30 frames of filler at each end:
count=Last.FrameCount
T = C7.Trim(0, 29).Reverse + C7 + C7.Trim(count-30, 0).Reverse
\ + C8.Trim(0, 29).Reverse + C8 + C8.Trim(count-30, 0).Reverse
\ + C9.Trim(0, 29).Reverse + C9 + C9.Trim(count-30, 0).Reverse
\ + C4.Trim(0, 29).Reverse + C4 + C4.Trim(count-30, 0).Reverse
\ + C5.Trim(0, 29).Reverse + C5 + C5.Trim(count-30, 0).Reverse
\ + C6.Trim(0, 29).Reverse + C6 + C6.Trim(count-30, 0).Reverse
\ + C1.Trim(0, 29).Reverse + C1 + C1.Trim(count-30, 0).Reverse
\ + C2.Trim(0, 29).Reverse + C2 + C2.Trim(count-30, 0).Reverse
\ + C3.Trim(0, 29).Reverse + C3 + C3.Trim(count-30, 0).Reverse
#return T

## x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x
## NOTE: if memory is still a problem, split the script at this point:
## create a lossless dub of the above video, then continue
## with the rest of this script using the dub.
#T = AviSource("lossless.avi")

## do noise processing etc
## (I don't have MCTemporalDenoise handy, but here is where it would go)
#T = T.hqdn3d(0, 0, 35, 25, 40)
#T = T.TemporalSoften(3, 28, 36, 28, 2)


## extract the slices from the processed video:
count2=framecount+60
P7=T.Trim(30, -framecount)
P8=T.Trim(30+count2, -framecount)
P9=T.Trim(30+2*count2, -framecount)

P4=T.Trim(30+3*count2, -framecount)
P5=T.Trim(30+4*count2, -framecount)
P6=T.Trim(30+5*count2, -framecount)

P1=T.Trim(30+6*count2, -framecount)
P2=T.Trim(30+7*count2, -framecount)
P3=T.Trim(30+8*count2, -framecount)

## crop off the padding
P7=P7.Crop(pad, pad, new_wid, new_hgt)
P8=P8.Crop(pad, pad, new_wid, new_hgt)
P9=P9.Crop(pad, pad, wid-2*new_wid, new_hgt)

P4=P4.Crop(pad, pad, new_wid, new_hgt)
P5=P5.Crop(pad, pad, new_wid, new_hgt)
P6=P6.Crop(pad, pad, wid-2*new_wid, new_hgt)

P1=P1.Crop(pad, pad, new_wid, hgt-2*new_hgt)
P2=P2.Crop(pad, pad, new_wid, hgt-2*new_hgt)
P3=P3.Crop(pad, pad, wid-2*new_wid, hgt-2*new_hgt)

## reconstruct the frame from slices and exit:
StackVertical(
\ StackHorizontal(P7, P8, P9),
\ StackHorizontal(P4, P5, P6),
\ StackHorizontal(P1, P2, P3)
\ )
return Last


##################################
#
function MirrorBorderLTRB(clip C,
\ int "bdrLt", int "bdrTp", int "bdrRt", int "bdrBt")
{
bdrLt = Max(2, Default(bdrLt, 16))
bdrTp = Max(2, Default(bdrTp, 16))
bdrRt = Max(2, Default(bdrRt, 16))
bdrBt = Max(2, Default(bdrBt, 16))

bdrLt = Max(0, bdrLt - (bdrLt % 2))
bdrTp = Max(0, bdrTp - (bdrTp % 2))
bdrRt = Max(0, bdrRt - (bdrRt % 2))
bdrBt = Max(0, bdrBt - (bdrBt % 2))

CL = C.Crop(0, 0, bdrLt, 0)
CR = C.Crop(C.Width-bdrRt, 0, bdrRt, 0)

C2 = (bdrLt==0)
\ ? (bdrRt==0)
\ ? C
\ : StackHorizontal(C, CR.FlipHorizontal)
\ : (bdrRt==0)
\ ? StackHorizontal(CL.FlipHorizontal, C)
\ : StackHorizontal(CL.FlipHorizontal, C, CR.FlipHorizontal)

CT = C2.Crop(0, 0, 0, bdrTp)
CB = C2.Crop(0, C2.Height-bdrBt, 0, bdrBt)

C3 = (bdrTp==0)
\ ? (bdrBt==0)
\ ? C2
\ : StackVertical(C2, CB.FlipVertical)
\ : (bdrBt==0)
\ ? StackVertical(CT.FlipVertical, C2)
\ : StackVertical(CT.FlipVertical, C2, CB.FlipVertical)

return C3
}

__END__

flyvholm
9th February 2015, 04:49
Thanks for all the input! I tried SetMemoryMax() with values from 256 to 2048. It doesn't appear to make any difference in memory consumption.

I also tried using avs2yuv to pipe to x264 (64-bit). I get the same Avisynth out-of-memory error when hitting ~1GB of memory usage. I've seen VirtualDub take up to about 1.5GB of memory for something else (that crashed too ;)), so I don't know why it seems impossible to allocate more than 1 GB for this task.

I'm working with nighttime timelapse footage. It needs strong noise reduction and mostly works quite favorably with temporal filters, so I do want to take advantage of that. I tried FFT3DFilter() on its own. I suspect that's the big memory hog inside MCTD, and it crashes (access violation) if I use the MCTD "high" settings. I can open the 12MP image sequence with default settings, but once I try to encode VirtualDub just hangs.

Hqdn3d() appears to be a whole different beast. Opens even 21MP image sequences fine and processing is fast! It looks like it will do the job in at least some cases, but I suspect it will fall short in others. We'll see, it's at least something to get me started.

Also, I'm able to open 1080p footage with MCTD, so even though it's not ideal it's an option to use that on the finished video.

@raffriff42: That is awesome. I was thinking of that, but being new to Avisynth it would have taken a lot of effort to get all of that figured out. Unsure of success, I wouldn't even have attempted. But what an excellent illustration of the power of knowing how to use scripting. With the rising popularity of 4K (and timelapse for that matter) I could see this come in handy for many others. I haven't tested yet, but I will do that and return with results (or questions ;)).

StainlessS
9th February 2015, 05:13
Yeh nice work rr42 - ever the show off :)

If Hqdn3d() works ok, then there is another filter (that I have never actually tried) but is also very fast, and because it is very fast I suspect
that it may too work ok (fast == less memory usage - Probably), FluxSmooth().
Might also be worth a try.

EDIT: Fluxsmooth: http://avisynth.nl/index.php/FluxSmooth

Stereodude
9th February 2015, 05:35
I'm new to Avisynth, jumping in because I'm looking to do some noise reduction. I'm working with DSLR timelapse image sequences that I'm making a film from, importing the sequences in Adobe After Efffects for post processing (such as pan/zoom). For best results I should apply the noise reduction to the original image sequences where most of the image is static from frame to frame, and due to the further processing I generally can't reduce the resolution before denoising. Images are mostly 12MP, in some cases 21MP.
When I suggested MCTD to you I didn't realize you were panning and zooming still images. I thought you had HD video footage. AFAIK MCTD isn't well suited for noise reduction of still images like you have. It works by comparing frames temporally for what's changing (grain & noise). It does work well on the output footage from After Effects (at least in your title sample).

The Noise Ninja Photoshop plug-in worked pretty well at removing noise from still photos when I played with it in several years ago. It was highly customizable.

flyvholm
9th February 2015, 06:50
Thanks for continued suggestions - I won't be running out of things to experiment with any time soon. :)

AFAIK MCTD isn't well suited for noise reduction of still images like you have. It works by comparing frames temporally for what's changing (grain & noise).
Actually the still images are highly suitable for a temporal noise filter because most of the image is static from frame to frame. My guess is that you pointed me right to the filter that will give me the highest quality output - if only I can avoid running out of memory. rr42's code above may just be the tool I need to get there.

EDIT: @Stereodude: I think I see... the title video we've been playing with looks like it's panning across a single image, but it's actually an image sequence (you can tell because the noise changes from frame to frame).

Groucho2004
9th February 2015, 10:02
I also tried using avs2yuv to pipe to x264 (64-bit). I get the same Avisynth out-of-memory error when hitting ~1GB of memory usage.
Make sure that you are using a avs2yuv version that has been linked with the "LARGE_ADDRESS_AWARE" flag. Here (https://www.dropbox.com/s/yck2vrp7hm6tfuk/avs2yuv.7z?dl=0) is the one I use.

flyvholm
10th February 2015, 11:27
Make sure that you are using a avs2yuv version that has been linked with the "LARGE_ADDRESS_AWARE" flag. Here (https://www.dropbox.com/s/yck2vrp7hm6tfuk/avs2yuv.7z?dl=0) is the one I use.
That helped - I see avs2yuv grabbing > 3GB of memory now. The process still fails, producing empty output and the following errors:

avs2yuv "input.avs" -raw - | x264 --crf 20 --output "output.mkv" - 4256x2832
raw [error]: raw input requires a resolution.
x264 [error]: could not open input file '-'
error: wrote only 17399278 of 18079488 bytes

AFAICT this syntax works for others. What am I missing? :confused:

Groucho2004
10th February 2015, 11:42
That helped - I see avs2yuv grabbing > 3GB of memory now. The process still fails, producing empty output and the following errors:

avs2yuv "input.avs" -raw - | x264 --crf 20 --output "output.mkv" - 4256x2832
raw [error]: raw input requires a resolution.
x264 [error]: could not open input file '-'
error: wrote only 17399278 of 18079488 bytes

AFAICT this syntax works for others. What am I missing? :confused:
I'm using something like this:
avs2yuv "script.avs" -o - | x264 - --crf 20 --frames 1000 --output "output.mkv" --demuxer y4m


Before you feed the script to the encoder you could run it through AVSMeter (http://forum.doom9.org/showthread.php?t=165528) to check the memory usage and if it runs through without crashing.

Stereodude
10th February 2015, 13:45
Post your script.avs file.

flyvholm
14th February 2015, 03:41
The sample script in the first post (minus the RGB conversion) would fail. However, with the --demuxer option set instead of resolution (as suggested by Groucho2004) the process completed. So that is potentially one way to work around the memory issue.

Admittedly, this topic is of much less interest to me after I was pointed to Neat Video. Getting that as a plugin for After Effects means that I can easily integrate it in my project files (i.e. no need to render intermediate, denoised footage), I get live previews as I tweak settings and I avoid color space conversions. On top of that it seems to work extremely well. I think I'd be hard pressed to produce as good results with Avisynth plugins, so for me Neat Video was the perfect solution and well worth the price.

But. I am really grateful for the help I received here, and in particular I think the script posted by raffriff42 is pretty cool, so I would like to make that work because it could come in handy for others. I did manage to open a 12MP image sequence in VirtualDub, but the slices were garbled. And now somehow MCTD can't find FFT3DFilter anymore. I'm off for the weekend, but will try to work out the kinks when I get back. :-)

raffriff42
14th February 2015, 19:56
I think the script posted by raffriff42 is pretty cool, so I would like to make that work because it could come in handy for others. I did manage to open a 12MP image sequence in VirtualDub, but the slices were garbled.Thanks, I hope it works for you.
Try removing the Reverse calls; they require perfect frame accuracy when decoding in reverse, which some codecs have trouble doing. These calls are probably overkill anyway...
<...>
#T = C7.Trim(0, 29).Reverse + C7 + C7.Trim(count-30, 0).Reverse
#\ + C8.Trim(0, 29).Reverse + C8 + C8.Trim(count-30, 0).Reverse
#\ + C9.Trim(0, 29).Reverse + C9 + C9.Trim(count-30, 0).Reverse
#\ + C4.Trim(0, 29).Reverse + C4 + C4.Trim(count-30, 0).Reverse
#\ + C5.Trim(0, 29).Reverse + C5 + C5.Trim(count-30, 0).Reverse
#\ + C6.Trim(0, 29).Reverse + C6 + C6.Trim(count-30, 0).Reverse
#\ + C1.Trim(0, 29).Reverse + C1 + C1.Trim(count-30, 0).Reverse
#\ + C2.Trim(0, 29).Reverse + C2 + C2.Trim(count-30, 0).Reverse
#\ + C3.Trim(0, 29).Reverse + C3 + C3.Trim(count-30, 0).Reverse
T = C7 + C8 + C9 + C4 + C5 + C6 + C1 + C2 + C3
<...>
#count2=framecount+60
#P7=T.Trim(30, -framecount)
#P8=T.Trim(30+count2, -framecount)
#P9=T.Trim(30+2*count2, -framecount)
#
#P4=T.Trim(30+3*count2, -framecount)
#P5=T.Trim(30+4*count2, -framecount)
#P6=T.Trim(30+5*count2, -framecount)
#
#P1=T.Trim(30+6*count2, -framecount)
#P2=T.Trim(30+7*count2, -framecount)
#P3=T.Trim(30+8*count2, -framecount)
#
P7=T.Trim(0 -framecount)
P8=T.Trim(framecount -framecount)
P9=T.Trim(2*framecount, -framecount)

P4=T.Trim(3*framecount, -framecount)
P5=T.Trim(4*framecount, -framecount)
P6=T.Trim(5*framecount, -framecount)

P1=T.Trim(6*framecount, -framecount)
P2=T.Trim(7*framecount, -framecount)
P3=T.Trim(8*framecount, -framecount)

flyvholm
18th February 2015, 03:03
That did the trick. There were a few additional tweaks to do. MCTD requires mod-4, and the input must initially be padded to be mod-12 to create a 3x3 grid with equal mod-4 slices. Also, the MirrorBorderLTRB() function was always adding padding on all sides, which doesn't appear to be what you intended. With that fixed the output did not need additional cropping. Below is a modified script that gave me "correct" results. One caveat is that we had to remove the filler frames at the start and end to avoid garbled output, but for a temporal filter like MCTD they ought to be included. It wasn't a problem for the footage I tested on, but in other cases it probably would be.

Assert(IsClip(Last) && IsYV12(Last),
\ "YV12 source required for noise filter")
Assert(Last.Width>48 && Last.Height>48,
\ "source resolution too low")

#The clip will be split into 9 slices (3x3 grid). Slices will be joined temporally,
#then fed to a filter for processing. In this case the filter is MCTD and requires
#input to be mod-4. First task is then to add padding to the clip so it can be split
#into 9 equal slices that are mod-4.
mod = 4
FrmPadTp = 3*mod - (Height % (3*mod))
FrmPadRt = 3*mod - (Width % (3*mod))
MirrorBorderLTRB(0, FrmPadTp, FrmPadRt, 0)

pad=8 ## size of mirror border / overlapped area as rq'd
pad2=2*pad

wid=Width
hgt=Height
wid3 = Round(wid/3.0)
hgt3 = Round(hgt/3.0)
new_wid = wid3 - (wid3 % mod)
new_hgt = hgt3 - (hgt3 % mod)

## create the 9 slices
## (slice numbers refer to position in 'numeric keypad' aka Subtitle notation)
C7=Crop(0, 0, new_wid+pad, new_hgt+pad)
\ .MirrorBorderLTRB(pad, pad, 0, 0, mod)

C8=Crop(new_wid-pad, 0, new_wid+pad2, new_hgt+pad)
\ .MirrorBorderLTRB(0, pad, 0, 0, mod)

C9=Crop(2*new_wid-pad, 0, wid-2*new_wid+pad, new_hgt+pad)
\ .MirrorBorderLTRB(0, pad, pad, 0, mod)


C4=Crop(0, new_hgt-pad, new_wid+pad, new_hgt+pad2)
\ .MirrorBorderLTRB(pad, 0, 0, 0, mod)

C5=Crop(new_wid-pad, new_hgt-pad, new_wid+pad2, new_hgt+pad2)
\ .MirrorBorderLTRB(0, 0, 0, 0, mod)

C6=Crop(2*new_wid-pad, new_hgt-pad, wid-2*new_wid+pad, new_hgt+pad2)
\ .MirrorBorderLTRB(0, 0, pad, 0, mod)


C1=Crop(0, 2*new_hgt-pad, new_wid+pad, hgt-2*new_hgt+pad)
\ .MirrorBorderLTRB(pad, 0, 0, pad, mod)

C2=Crop(new_wid-pad, 2*new_hgt-pad, new_wid+pad2, hgt-2*new_hgt+pad)
\ .MirrorBorderLTRB(0, 0, 0, pad, mod)

C3=Crop(2*new_wid-pad, 2*new_hgt-pad, wid-2*new_wid+pad, hgt-2*new_hgt+pad)
\ .MirrorBorderLTRB(0, 0, pad, pad, mod)

## Join the 9 slices temporally. This may cause problems with filters
## that mix data from adjacent frames unless filler frames are added
## at start and end.
T = C7 + C8 + C9 + C4 + C5 + C6 + C1 + C2 + C3
#return T

## x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x
## NOTE: if memory is still a problem, split the script at this point:
## create a lossless dub of the above video, then continue
## with the rest of this script using the dub.
#T = AviSource("lossless.avi")

## do noise processing etc
T = T.MCTemporalDenoise(settings="medium")

## extract the slices from the processed video:
P7=T.Trim(0, -framecount)
P8=T.Trim(framecount, -framecount)
P9=T.Trim(2*framecount, -framecount)

P4=T.Trim(3*framecount, -framecount)
P5=T.Trim(4*framecount, -framecount)
P6=T.Trim(5*framecount, -framecount)

P1=T.Trim(6*framecount, -framecount)
P2=T.Trim(7*framecount, -framecount)
P3=T.Trim(8*framecount, -framecount)

## crop off the padding
P7=P7.Crop(pad, pad, new_wid, new_hgt)
P8=P8.Crop(pad, pad, new_wid, new_hgt)
P9=P9.Crop(pad, pad, wid-2*new_wid, new_hgt)

P4=P4.Crop(pad, pad, new_wid, new_hgt)
P5=P5.Crop(pad, pad, new_wid, new_hgt)
P6=P6.Crop(pad, pad, wid-2*new_wid, new_hgt)

P1=P1.Crop(pad, pad, new_wid, hgt-2*new_hgt)
P2=P2.Crop(pad, pad, new_wid, hgt-2*new_hgt)
P3=P3.Crop(pad, pad, wid-2*new_wid, hgt-2*new_hgt)

## reconstruct the frame from slices
StackVertical(
\ StackHorizontal(P7, P8, P9),
\ StackHorizontal(P4, P5, P6),
\ StackHorizontal(P1, P2, P3)
\ )

#Finally, remove the padding that was added before making slices
Crop(0, FrmPadTp, -FrmPadRt, 0)

return Last


##################################
#
function MirrorBorderLTRB(clip C,
\ int "bdrLt", int "bdrTp", int "bdrRt", int "bdrBt", int "mod")
{

Defined(mod) ? Eval("""

bdrLt = ((bdrLt % mod) > 0) ? bdrLt + mod - (bdrLt % mod) : bdrLt
bdrTp = ((bdrTp % mod) > 0) ? bdrTp + mod - (bdrTp % mod) : bdrTp
bdrRt = ((bdrRt % mod) > 0) ? bdrRt + mod - (bdrRt % mod) : bdrRt
bdrBt = ((bdrBt % mod) > 0) ? bdrBt + mod - (bdrBt % mod) : bdrBt

""") : NOP

bdrLt > 0 ? Eval("""CL = C.Crop(0, 0, bdrLt, 0)""") : NOP
bdrRt > 0 ? Eval("""CR = C.Crop(C.Width-bdrRt, 0, bdrRt, 0)""") : NOP

C2 = (bdrLt==0)
\ ? (bdrRt==0)
\ ? C
\ : StackHorizontal(C, CR.FlipHorizontal)
\ : (bdrRt==0)
\ ? StackHorizontal(CL.FlipHorizontal, C)
\ : StackHorizontal(CL.FlipHorizontal, C, CR.FlipHorizontal)

bdrTp > 0 ? Eval("""CT = C2.Crop(0, 0, 0, bdrTp)""") : NOP
bdrBt > 0 ? Eval("""CB = C2.Crop(0, C2.Height-bdrBt, 0, bdrBt)""") : NOP

C3 = (bdrTp==0)
\ ? (bdrBt==0)
\ ? C2
\ : StackVertical(C2, CB.FlipVertical)
\ : (bdrBt==0)
\ ? StackVertical(CT.FlipVertical, C2)
\ : StackVertical(CT.FlipVertical, C2, CB.FlipVertical)

return C3
}

__END__

Unfortunately, in practice I can't use it myself. MCTD on 12MP+ images is so slow that it just isn't plausible. But maybe some others will be able to use this neat little trick as a workaround for memory problems. :)