Log in

View Full Version : Compensating for uneven projector light


stephen22
5th December 2015, 16:41
I've captured 8mm film by pointing my old video camera at the projector screen. The projector lamp produces uneven lighting and I am trying to compensate for this.
I've made a jpeg of a frame of the projector image with no film, inverted it, and used it in an overlay script thus:

fname="2015-06-29 22.11.48 levanto"
blank=imagesource("C:\Users\me\Videos\Family Archive\Rolle and Levanto\inverted blank frame.jpg", end=3000)
blank=converttoYV12(blank)
#blank= tweak(blank,bright=-10)
main=avisource("C:\Users\me\Videos\Family Archive\"+fname+".avi")
output=overlay(main, blank,opacity=0.6,mode="add")
output= tweak(output,bright=-30)
return output

The result is not bad, but the values for tweaking the brightness of the blank frame and final output, and the opacity value of the overlay, are quite arbitrary and empirical (and I'm not absolutely sure exactly how they interact). I feel sure there must be a formula that gets the compensation dead right, without losing information at the bright end. Has anybody managed this?

raffriff42
5th December 2015, 17:12
Try the HotSpot filter for VirtualDub:
http://rationalqm.us/hotspot/hotspot.html

But,as brilliant as avisynth is, this looks like a job for resolve lite

https://www.blackmagicdesign.com/products/davinciresolve

it's free... and it's worth having as not so long ago this program was $75k+.
[...]
resolve will do all the colour/tone fixes you'll ever need, though noise reduction is still avisynth's domain.

wonkey_monkey
5th December 2015, 17:36
Would gamma-awareness (or lack thereof, in overlay and tweak) be a factor?

johnmeyer
5th December 2015, 19:07
The VirtualDub Hot Spot filter works well (I've used it many times), but you need to be able to find a suitable frame for creating the mask, preferably a section of leader, or a place where the film contains no image, and has a uniform exposure throughout. If you can't get such a thing, you might be able to create a mask using some of the functions in StainlessS' RT_Stats collection, although I'm not sure exactly how to do that.

Also, you can sometimes minimize this hot spot by changing distances from the projector to the screen, and from the camera to the screen. In addition, if your projector has a zoom lens (used to allow the same projector to be used in big and small rooms) you may be able to minimize the hot spot by using a different zoom amount.

Finally, as you probably know, this is a really lousy way to transfer film. Even if you solve your hot spot problem, you will always get flicker. This happens because the projector shutter closes while each frame of film advances and, with some projectors, closes once or twice even while the film is stationary in the projector gate in order to increase the flicker rate, thus reducing flicker-induced headaches. The problem is that because the video camera and projector are not synchronized, you end up with some frames where the shutter is open for the entire time the video camera is taking a picture of that frame. The result is a nice bright frame. Then, on the next video frame, the projector may just be getting ready to advance to the next frame of film so the video camera records a little of one frame, then averages into that capture the entire dark portion while the projector shutter is closed, and then exposes part of the next frame. The result is a really dark frame which contains a merge of two adjacent frames. Not good stuff.

There are methods, both hardware and software-based, to modify the projector and end up with what is known as a "frame accurate" capture. This is infinitely better than what you are getting. Such a transfer is also known as "frame-by-frame." Google these terms along with "film transfer" and you'll find dozens of places near you (including Costco) that do better transfers. With a frame-accurate transfer, each frame of video contains exactly one frame of film, with no exposure variations. With a frame-accurate transfer you can use AVISynth film restoration scripts, like those posted in this forum by VideoFred, which can fantastically improve the look of your old film.

If you do want to proceed with your method, you also need to play with the speed control on your projector, if it has one. You will find that you can minimize the flicker by changing the speed until the flicker is reduced. On your video camera, use a shutter speed of somewhere between 1/30 and 1/60 second. Do not use a fast shutter speed, or you will end up with some frames which are completely black; the faster shutter speeds increase the flicker problem. Do not use a shutter speed slower than your projector's running speed or you ensure that ALL of your video frames will contain blends of adjacent frames. So, the higher the shutter speed, the more flicker you get, and the lower the video camera's shutter speed, the more blends you get.

StainlessS
5th December 2015, 19:38
Sorry John, dont think anything in RT_ can assist, RT_Stats is mostly runtime and best for sampling frames, you need standard filter returning clip to
create any kind of mask.

Might be interesting to see what ClipBlend(Delay=0) produces on an entire clip (averages entire clip, just use the resultant luma).

There is also Clipblend16 which returns Stack16 format.
The standard ClipBlend should be accurate to 1/2 a bit over entire clip for 8 bit result.

EDIT: Something like


Clipblend(Delay=0)
Trim(FrameCount-1,-1)


Would return the final frame
Here: http://forum.doom9.org/showthread.php?t=168048&highlight=clipblend

raffriff42
5th December 2015, 19:48
A simple Overlay won't work, as brightness, contrast and gamma are all (possibly) affected. I would suggest the following method.

EDIT SmoothLevels is optional, but it's better than regular Levels. You can use regular Levels when testing, as the parameters are compatible. I think AvsPmod can adjust the parameters in real time, which should help the trial-and-error process.

EDIT (this is wrong - skip to post #18 (http://forum.doom9.org/showthread.php?p=1749450#post1749450))

# http://forum.doom9.org/showthread.php?t=154971
#LoadPlugin("SmoothAdjust-2,00\avs-2.5.x\x86\SmoothAdjust.dll")

## match 'edge', 'cent' as closely as possible (hilites, lowlites, midrange)
edge=Last.SmoothLevels(16, 1.0, 230, 16, 235) ## adjust as needed
cent=Last.SmoothLevels(16, 0.95, 225, 16, 235) ## ditto

## refine mask edges if needed (maybe)
#blank=blank.Levels(16, 1.0, 235, 16, 235)

output=Overlay(edge, cent, mask=blank.ConvertToY8, opacity=1.0)
return output


Before and after (an artificial example)
https://www.dropbox.com/s/0slfokk1srh7nq3/projector-shading%20%28lighthouse%29%2000.jpg?raw=1 https://www.dropbox.com/s/bd4g49pnox9o7en/projector-shading%20%28lighthouse%29%2001.jpg?raw=1

johnmeyer
5th December 2015, 20:42
Sorry John, dont think anything in RT_ can assist, RT_Stats is mostly runtime and best for sampling frames, you need standard filter returning clip to
create any kind of mask.I should have been more specific about what I was suggesting. RT_Stats has some functions, as I recall, which can average multiple frames. I was thinking that if you average ALL the frames in a long enough clip, and if there was a consistent overexposure towards the center of each frame (the "hot spot"), the single frame that represented the average of all frames could be used to construct the mask that would then be used by Hot Spot, or by some AVISynth masking function, to increase or decrease exposure, according to the mask.

StainlessS
5th December 2015, 21:32
John, ClipBlend does as you say, as in edits to previous post.
(RGBAmplifier can also do the same[rgb only], not sure if DavidHorman's version of same will also do it, but I think so [also supports YUV])

wonkey_monkey
5th December 2015, 23:06
Stephen22 already has the most perfect "average" frame of the projector's output, doesn't he?

I've made a jpeg of a frame of the projector image with no film

It's just a matter of finding the right way to apply it.

Stephen22, can you post a short clip and your "null" frame?

stephen22
6th December 2015, 14:56
Thanks for your interest guys - what a lot to take in!

The Hotspot filter sounds interesting, and Smoothadjust - but both require arbitrary and empirical settings, same as the overlay method I'm using. Also there is a yellow colourcast from the projector lamp - a greyscale mask wouldn't help this.

My method actually gives quite reasonable results, but I feel that surely if you have a map of the unevenness it should be theoretically possible to just subtract it accurately from the pictures and thus eliminate it. (You can certainly do this with sound.) The problem seems to be not knowing exactly how "brightness" and "opacity" affect the actual numbers.

I agree that videoing the projector screen is not the best way of transferring 8mm, but I have had some very disappointing results from several commercial firms, particularly with dark indoor clips which tend to be hideously overexposed and lose information from peoples' faces (i.e. the most interesting features!). You can get superb results from a Rank machine costing $x00,000 (terrific dynamic range, wetgate and all the rest of it) but it's $80+ a reel and has to go to USA as we don't seem to have one here in the UK.

I use an old analogue Sony with all the special effects turned off so the exposure is as long as possible, and manual aperture control. I get a slight variation in brightness at about 1 Hz which is completely corrected by Donald Graft's deflicker filter. There are (obviously) overlapping frames which are easily identified with Donald Graft's brilliant telecide filter (threshold set to 15) and removed by Multidecimate. Certainly for indoor clips my results are better than the standard telecine offerings out there.

This file has been telecided, not yet multidecimated.

https://drive.google.com/file/d/0BxpFyO_R0t5JMFhYakI3SkhMR3M/view?usp=sharing

These are the blank and inverted blank frames

johnmeyer
6th December 2015, 18:42
I agree that videoing the projector screen is not the best way of transferring 8mm, but I have had some very disappointing results from several commercial firms, particularly with dark indoor clips which tend to be hideously overexposed and lose information from peoples' faces (i.e. the most interesting features!). You can get superb results from a Rank machine costing $x00,000 (terrific dynamic range, wetgate and all the rest of it) but it's $80+ a reel and has to go to USA as we don't seem to have one here in the UK.

I use an old analogue Sony with all the special effects turned off so the exposure is as long as possible, and manual aperture control.Several quick notes about exposure.

When you transfer film, expose for the highlights. There are several tricks that will help you do this. First, get a camera with zebras. I can't imagine trying to do film transfer without this.

Second, if your camera has a "spotlight" function, use it. This is found in almost all Sony camcorders, but many other manufacturers have a similar function. It is designed to compensate for a stage spotlight where the overall scene is relatively dark, but the actors are in a spotlight. The function will darken the overall scene until the actors standing in the bright light are exposed correctly. Yes, the shadows will be too dark, but you can use a histogram function in your NLE or AVISynth to gain the shadows while leaving the highlights alone. The way video works, you can almost recover some details from shadows, but once the details are gone from the highlights ("blown out") they are gone forever.

Sometimes the spotlight function is not enough. Rather than revert to true manual exposure control, which is impossible to adjust quickly enough from scene-to-scene, use the EV adjustment. On most camcorders there is a +-EV setting that will add or subtract a certain amount from the automatic exposure. Using this, the camera will still react to changes in exposure, but will subtract (make darker) from whatever exposure you get. Throughout the whole process, you simply watch the zebras and then adjust the EV up and down to the point where they just start to appear.

The ideal way to do this is to get a camera like VideoFred has. On really high end cameras you can make all sorts of adjustments to the internal histogram in the camera and have many of these adjustments done automatically.

You might want to read this post, and the one on which it was based, in order to get some really good ideas of how to, more or less, convert your transfer into one that is frame accurate:

The power of Avisynth: salvaging "botched" transfers of old 8mm films to DVD. (http://forum.doom9.org/showthread.php?t=161493)

Finally, I know nothing about what sort of transfer businesses are available to you locally, but here in the states, many of the Cintel and Spirit (which is what most places are now using) transfer places sometimes have amazing sales where their transfers are not much more money than any place else. Do a search, and make sure to include "Spirit," and perhaps you'll find a place that does it more cheaply. If you're not familiar with the Spirit technology, here is a description:

Spirit DataCine (https://en.wikipedia.org/wiki/Spirit_DataCine)

wonkey_monkey
6th December 2015, 23:29
First, get a camera with zebras.

Just in case some need that clarified, John is not suggesting that you go shopping with stripey African equines (at least, I don't think he is). "Zebra stripes" are a function of some cameras which will show, on the viewfinder, anything that would be recorded as pure white (which indicates a blown-out highlight) as animated scrolling stripes. This then helps you dial down the exposure or shutter settings until the stripes are no longer showing, at which point you know you're not losing any detail in the highlights.

I just so happen to already have a demo of this handy:

http://horman.net/avisynth/zebra.gif

Motenai Yoda
7th December 2015, 00:30
But Clara is not...

Anyway there is way to get Resolve Lite free without registration ?

stephen22
9th December 2015, 16:16
Thanks so much to John and others for helpful comments.

I did do a quick search and one local firm uses a Golden Eye scanner and quoted me £280 (that's $420 to you!) per 200ft reel + £75 "upload charge", another with a cintel machine wants £900 for 4 reels. I'm sure the results are brilliant, but I have around 4000ft of film. I may go back to Cinepost in Marietta Georgia from whom I have had impressive results on their custom wetgate machine in the past ($17.5 for 50ft). (It's not so much the cost, as sending my precious archives away from civilisation into the uncharted wastes...)

I'm reasonably pleased with my method which with a few filters gives me perfect frame by frame transfer, and with Deshaker and VideoFred's amazing colour-correction cleaning and sharpening scripts gives pretty good results. It doesn't cost me anything, and critically I have control over exposure (if a scene change is sudden, I can simply go back over it). Exposing for highlights is a good tip. The zebra gismo looks pretty cool, but perhaps not worth shelling out for a new camera. The main problems seem to be slight blurring at the edges which I think may be on the original film, and uneven lighting which seems to respond quite well to overlaying.

I just wondered if anyone had good enough math to calculate how to marry my mask to the pictures - how does "opacity" work and (for instance, as David suggested,) are the numbers added/subtracted by the "brightness" filter gamma coded? or should I gamma-correct the pictures before adjusting the brightness. That sort of thing.

Motenai Yoda
9th December 2015, 21:12
Can you shot a white frame?
also the camera is setted on manual with fixed settings?

stephen22
10th December 2015, 14:32
Yeah I uploaded it above (post#10). It's an old analogue camera with exposure set to manual.

stephen22
10th December 2015, 17:36
A simple Overlay won't work, as brightness, contrast and gamma are all (possibly) affected. I would suggest the following method.

EDIT SmoothLevels is optional, but it's better than regular Levels. You can use regular Levels when testing, as the parameters are compatible. I think AvsPmod can adjust the parameters in real time, which should help the trial-and-error process.

# http://forum.doom9.org/showthread.php?t=154971
#LoadPlugin("SmoothAdjust-2,00\avs-2.5.x\x86\SmoothAdjust.dll")

## match 'edge', 'cent' as closely as possible (hilites, lowlites, midrange)
edge=Last.SmoothLevels(16, 1.0, 230, 16, 235) ## adjust as needed
cent=Last.SmoothLevels(16, 0.95, 225, 16, 235) ## ditto

## refine mask edges if needed (maybe)
#blank=blank.Levels(16, 1.0, 235, 16, 235)

output=Overlay(edge, cent, mask=blank.ConvertToY8, opacity=1.0)
return output





Tried really hard to get my head round this - not sure what "edge" and "cent" represent - haven't both lost information at the bright end?

The script doesn't modify "blank" in any way - it comes from the same source as the original and is therefore gamma-equivalent? Bit out of my depth here.

raffriff42
10th December 2015, 23:36
Yeah, that idea wasn't so good. Here's a better one: basically, it's to overlay the inverted empty frame in "multiply" mode at around 50% opacity. This gives good cancellation of the uneven lighting (and as a bonus, partial color correction; the blueness of the inverted empty frame cancels out the yellowness of the footage); however it darkens the image overall, which must be compensated for with makeup gain.

The script below does this, plus additional color balance control, in a single MaskTools function call per RGB channel (therefore all calculations are in floating point). The script looks even more complex than my first one, but is actually easier to use - less tweaking is required to get a decent result.

(Rewritten 11-Dec: skip empty frame Levels step)

https://www.dropbox.com/s/kchtl70wpopvwz0/projector-shading%20S_0.jpg?raw=1
(Raw source. Histogram/waveform monitor added for no reason.)

https://www.dropbox.com/s/k0lz9vzbbsf6mvy/projector-shading%20S_234-4.jpg?raw=1
(My final result. A little yellow on the edges, but hey.)

Requires: MaskTools2 (https://github.com/tp7/masktools/releases), HistogramRGBLevels (http://avisynth.nl/index.php/Histograms_in_RGB_%26_CMY)
## MaskTools2 required
# https://github.com/tp7/masktools/releases
LoadPlugin("tp7masktools\masktools2.dll")

## HistogramRGBLevels:
# http://avisynth.nl/index.php/Histograms_in_RGB_%26_CMY

## raw source
C=AviSource("E:\Data\Downloads\projector-shading Julie & Dad in sea.avi")
\ .Spline64Resize(768, 576) [* aspect ratio correction *]
#return C.Histogram ## preview source

## empty frame
E=ImageSource("E:\Data\Downloads\projector-shading blank frame.jpg")
\ .BilinearResize(C.Width/2, C.Height/2)
\ .ConvertToYV12
\ .Blur(1.0)
\ .BilinearResize(C.Width, C.Height)
\ .Invert [* if needed *]
\ .AssumeFPS(C).Loop(-1)
#return E.Histogram ## preview empty frame

## adjust opacity (starting at 0.5) for best compensation
## adjust gain (starting at 2.0) for proper white level
## adjust gain_r, gain_g, gain_b for color balance (white)
## adjust bias_r, bias_g, bias_b for color balance (black)
## adjust gamma_r, gamma_g, gamma_b for color balance (mid)

R = rgb_multiply(C.ConvertToRGB24, E.ConvertToRGB24,
\ opacity=0.4, gain=1.5,
\ bias_r=0, gamma_r=1.0, gain_r=0.9,
\ bias_g=0, gamma_g=1.0, gain_g=1.05,
\ bias_b=0, gamma_b=1.0, gain_b=1.10)
return R.HistogramRGBLevels

## FINAL (no histograms)
return R.ConvertToYV12

## MaskTools2 required
## https://github.com/tp7/masktools/releases
#######################################
### See "Compensating for uneven projector light"
### http://forum.doom9.org/showthread.php?p=1749450#post1749450
##
## equivalent to Overlay(mode="multiply") + color correction
##
## @ opacity : strength of effect; 0=none, 1=full; default 0.5
## @ gain : white level; overall makeup gain; default 2.0
## @ bias_r, g, b : bias (black level); default 0
## @ gamma_r, g, b : middle tones; <1=darker, >1=lighter; default 1.0
## @ gain_r, g, b : white color balance; default 1.0
##
function rgb_multiply(clip C, clip D, float "opacity", float "gain",
\ int "bias_r", float "gamma_r", float "gain_r",
\ int "bias_g", float "gamma_g", float "gain_g",
\ int "bias_b", float "gamma_b", float "gain_b")
{
Assert(C.IsRGB && D.IsRGB, "rgb_multiply: sources must be RGB")

opacity = Min(Max(0.0, Default(opacity, 0.5)), 1.0)
gain = Min(Max(0.1, Default(gain, 2.0)), 100.0)
bias_r = Default(bias_r, 0)
bias_g = Default(bias_g, 0)
bias_b = Default(bias_b, 0)
gamma_r = Min(Max(0.01, Default(gamma_r, 1.0)), 100.0)
gamma_g = Min(Max(0.01, Default(gamma_g, 1.0)), 100.0)
gamma_b = Min(Max(0.01, Default(gamma_b, 1.0)), 100.0)
gain_r = Min(Max(0.0, Default(gain_r, 1.0)), 1000.0)
gain_g = Min(Max(0.0, Default(gain_g, 1.0)), 1000.0)
gain_b = Min(Max(0.0, Default(gain_b, 1.0)), 1000.0)

rexpr = ("((((1-$o)*x/256) + ($o * x/256 * y/256) ) * $a)^(1/$g) * 256 + $b")
\ .StrReplace("$o", String(opacity))
\ .StrReplace("$a", String(gain * gain_r))
\ .StrReplace("$g", String(gamma_r))
\ .StrReplace("$b", String(bias_r))
\ .mt_polish

gexpr = ("((((1-$o)*x/256) + ($o * x/256 * y/256) ) * $a)^(1/$g) * 256 + $b")
\ .StrReplace("$o", String(opacity))
\ .StrReplace("$a", String(gain * gain_g))
\ .StrReplace("$g", String(gamma_g))
\ .StrReplace("$b", String(bias_g))
\ .mt_polish

bexpr = ("((((1-$o)*x/256) + ($o * x/256 * y/256) ) * $a)^(1/$g) * 256 + $b")
\ .StrReplace("$o", String(opacity))
\ .StrReplace("$a", String(gain * gain_b))
\ .StrReplace("$g", String(gamma_b))
\ .StrReplace("$b", String(bias_b))
\ .mt_polish

return MergeRGB(
\ mt_lutxy(C.ShowRed("YV12"),
\ D.ShowRed("YV12"),
\ yexpr=rexpr,
\ U=-128, V=-128),
\ mt_lutxy(C.ShowGreen("YV12"),
\ D.ShowGreen("YV12"),
\ yexpr=gexpr,
\ U=-128, V=-128),
\ mt_lutxy(C.ShowBlue("YV12"),
\ D.ShowBlue("YV12"),
\ yexpr=bexpr,
\ U=-128, V=-128)
\ )
}

# http://avisynth.nl/index.php/HDColorBars
function StrReplace(string base, string findstr, string repstr)
{
pos = FindStr(base, findstr)
return (StrLen(findstr)==0) || (pos==0)
\ ? base
\ : StrReplace(
\ LeftStr(base, pos-1) + repstr +
\ MidStr(base, pos+StrLen(findstr)),
\ findstr, repstr)
}

Motenai Yoda
12th December 2015, 14:06
Yeah I uploaded it above (post#10). It's an old analogue camera with exposure set to manual.

nope, I see black and inverted only, and inverted isn't the same as white.

maybe working in rgb subtracting black one and multiply for 255/white

raffriff42
12th December 2015, 18:16
nope, I see black and inverted only, and inverted isn't the same as white.stephen22 said "blank" (meaning "empty," with no film in the gate), not "black." The "black" one is the inverted one.
The blank/empty frame is as "white" as he can get it - that's the problem.

maybe working in rgb subtracting black one and multiply for 255/whiteYes, maybe... (that is what I have done; see post just above yours)

stephen22
12th December 2015, 21:53
Thanks that's very impressive. But quite complex for what should be a simple process, lots of new filters to learn about. Does lots of other stuff too.

Motenai Yoda
13th December 2015, 00:38
stephen22 said "blank" (meaning "empty," with no film in the gate), not "black." The "black" one is the inverted one.
The blank/empty frame is as "white" as he can get it - that's the problem.

Yes, maybe... (that is what I have done; see post just above yours)

Whatever inverted blank/white isn't the same as black too.
If you have clouding or tint in the blank one, often you'll don't get them inverted in the black one.


also ("256 * (( ((1-$o)*x/256) + ($o * x/256 * y/256))^(1/$g) * $a) + $b") didn't do what I means, it's more like

"x 1 "+String(opacity)+" - * x y - z y - / 1 "+String(gamma_c)+" / ^ 255 * "+String(opacity)+" * + "+String(bias_c)+" +"

raffriff42
13th December 2015, 01:51
Please post a functional script. :script:

raffriff42
13th December 2015, 18:36
Thanks that's very impressive. But quite complex for what should be a simple process, lots of new filters to learn about. Does lots of other stuff too.Thank you. Please Let me know if anything is not clear.

In case you need them, I'll give some quick install instructions:

1. Install the required MaskTools2 (https://github.com/tp7/masktools/releases):

Download it here (http://github.com/tp7/masktools/releases/download/b1/masktools2-x86.zip) (this is the one to get if you are running Avisynth 2.6, 32-bit (http://sourceforge.net/projects/avisynth2/), which I recommend)
Place masktools2.dll in the Avisynth Plugins folder, or
Place masktools2.dll in any folder you like, but use LoadPlugin (http://avisynth.nl/index.php/Plugins) like I do


2. Install the required HistogramRGBLevels (http://avisynth.nl/index.php/Histograms_in_RGB_%26_CMY):

Click the link above, right click the "download" link and "Save link as..."
Move "Histograms_in_RGB_%26_CMY.avsi" to your the Avisynth Plugins folder, or
Add Import (http://avisynth.nl/index.php/Import)("<path>\Histograms_in_RGB_%26_CMY.avsi") at the top of the script


Usage instructions, step-by-step: I started with all arguments at default:R = rgb_multiply(C.ConvertToRGB24, E.ConvertToRGB24,
\ opacity=0.5, gain=2.0,
\ bias_r=0, gamma_r=1.0, gain_r=1.0,
\ bias_g=0, gamma_g=1.0, gain_g=1.0,
\ bias_b=2, gamma_b=1.0, gain_b=1.0)
...and here is what I saw:
https://www.dropbox.com/s/3o9la8c6z5q2y59/projector-shading%20CC-02.jpg?raw=1


...first, it's way too bright; we need the whitest values to be somewhere near the faint bright bar
on the right of the RGB scope (that's 235). Let's try gain=1.6:
https://www.dropbox.com/s/bt7zjulw9b7hrjp/projector-shading%20CC-03.jpg?raw=1


...now we can see the shading is under-compensated (edges are still dark), so we know opacity needs adjusting;
Let's try opacity=0.3 - trimming gain to 1.2, to reestablish rough white levels:
https://www.dropbox.com/s/ogzo4tvc7cd8fz0/projector-shading%20CC-04.jpg?raw=1


...hmm, that's worse; let's go the other way - all the way to opacity=1.0:
https://www.dropbox.com/s/oik0kkz9qzj69lw/projector-shading%20CC-05.jpg?raw=1


...that's what I call over-compensation; let's dial it back to somewhere in the middle: opacity=0.6:
https://www.dropbox.com/s/3mvzo0wl8cg61ky/projector-shading%20CC-07.jpg?raw=1

...good enough for me! The edges are a little dark still, but I don't want to over-correct as that may cause
other problems down the road.

Then I did color correction. I'll skip the boring details here. It's a matter of taking the "tint" out of the highlights
and lowlights. There's no right way to do it; you just mess around until it looks alright, using the same over-under
method I showed above for opacity. Note I did not bother with gamma at all.
https://www.dropbox.com/s/kmg0ak3ryuffdho/projector-shading%20CC-99.jpg?raw=1

johnmeyer
13th December 2015, 21:32
Great job, raffriff42.

stephen22
14th December 2015, 13:09
Yeah thanks very much.

Motenai Yoda
15th December 2015, 19:48
Please post a functional script. :script:
but it's only a xyz polish expr...

ie

## MaskTools2 required
# https://github.com/tp7/masktools/releases
LoadPlugin("tp7masktools\masktools2.dll")

## DitherTools required
# http://ldesoras.free.fr/prod.html#src_ditheravsi

## raw source
C=AviSource("E:\Data\Downloads\projector-shading Julie & Dad in sea.avi")
\.ConvertToRGB()
\.separateRGBchannels()

## blank_frame
E=ImageSource("E:\Data\Downloads\projector-shading blank frame.jpg",end = c.framecount(), fps = c.framerate(), string pixel_type = "RGB32")
\.separateRGBchannels()

## black_frame
F=ImageSource("E:\Data\Downloads\projector-shading black frame.jpg",end = c.framecount(), fps = c.framerate(), string pixel_type = "RGB32")
\.separateRGBchannels()

simple_colorscaling(C,E,F)
\.separateRGBchannels()


function separateRGBchannels(clip c){
interleave(c.showred(),c.showgreen(),c.showblue())
converttoy8()
}

function mergeRGBchannels(clip c){
MergeRGB(c.selectevery(3,0), c.selectevery(3,1), c.selectevery(3,2))
}

function simple_colorscaling(clip c, clip e, clip f, float "opacity", float "gamma", float "bias"){
opacity = Min(Max(0., Default(opacity, 1.)), 1.)
gamma = Min(Max(0.1, Default(gamma, 1.)), 10.)
bias = Min(Max(-50., Default(bias, 0.)), 50.)

#as all those clip are 8bit, difference can be done with

c_diff = mt_lutxy(c, f, "x y -", y=3, u=1, v=1)
e_diff = mt_lutxy(e, f, "x y -", y=3, u=1, v=1)

#using dither_lutxy8 (can be done with standard mt_lutxy too)

p1 = Dither_lutxy8(c_diff, e_diff, expr="x y / 1 "+String(gamma)+" / ^ 255 * "+String(opacity)+" * "+String(bias_c)+" + 256 *", y=3, u=1, v=1)

p2 = Dither_lut8(c, expr = " x 1 "+String(opacity)+" - * 256 *", y=3, u=1, v=1)

Dither_add16(p2, p1, y=3, u=1, v=1)
DitherPost()
}

raffriff42
15th December 2015, 23:40
Your script does not run; it has several syntax errors. I'll try to get it working, but I must guess your intention:## MaskTools2 required
# https://github.com/tp7/masktools/releases
LoadPlugin("tp7masktools\masktools2.dll")

## DitherTools required
# http://ldesoras.free.fr/prod.html#src_ditheravsi
LoadPlugin("Dither\v1.27.1\win32\dither.dll")
LoadPlugin("Dither\v1.27.1\win32\avstp.dll")
Import("Dither\v1.27.1\dither.avsi")

## raw source
C=AviSource("E:\Data\Downloads\projector-shading Julie & Dad in sea.avi")
\.ConvertToRGB()
\.separateRGBchannels()

## blank_frame
#[E] Error: Avisynth open failure:
# Script error: expected a , or )
E=ImageSource("E:\Data\Downloads\projector-shading blank frame.jpg",end = c.framecount(), fps = c.framerate(), [*string*] pixel_type = "RGB32")
\.separateRGBchannels()

## black_frame
## ("black frame.jpg" - no such file - using "inverted blank frame.jpg")
#[E] Error: Avisynth open failure:
# Script error: expected a , or )
F=ImageSource("E:\Data\Downloads\projector-shading inverted blank frame.jpg",end = c.framecount(), fps = c.framerate(), [*string*] pixel_type = "RGB32")
\.separateRGBchannels()


#[E] Error: Avisynth open failure:
# ShowRed: RGB data only
# Do you mean mergeRGBchannels?
simple_colorscaling(C,E,F)
\/*.separateRGBchannels()*/
\.mergeRGBchannels

function separateRGBchannels(clip c){
interleave(c.showred(),c.showgreen(),c.showblue())
converttoy8()
}

function mergeRGBchannels(clip c){
MergeRGB(c.selectevery(3,0), c.selectevery(3,1), c.selectevery(3,2))
}

function simple_colorscaling(clip c, clip e, clip f, float "opacity", float "gamma", float "bias"){
opacity = Min(Max(0., Default(opacity, 1.)), 1.)
gamma = Min(Max(0.1, Default(gamma, 1.)), 10.)
bias = Min(Max(-50., Default(bias, 0.)), 50.)

#as all those clip are 8bit, difference can be done with

c_diff = mt_lutxy(c, f, "x y -", y=3, u=1, v=1)
e_diff = mt_lutxy(e, f, "x y -", y=3, u=1, v=1)

#using dither_lutxy8 (can be done with standard mt_lutxy too)

#[E] Error: Avisynth open failure:
# I don't know what "bias_c" means
p1 = Dither_lutxy8(c_diff, e_diff, expr="x y / 1 "+String(gamma)+" / ^ 255 * "+String(opacity)+" * "+String(bias[*_c*])+" + 256 *", y=3, u=1, v=1)

p2 = Dither_lut8(c, expr = " x 1 "+String(opacity)+" - * 256 *", y=3, u=1, v=1)

Dither_add16(p2, p1, y=3, u=1, v=1)
DitherPost()
}
EDIT after the latest fix (separateRGBchannels->mergeRGBchannels), I get this:
https://www.dropbox.com/s/7xdbcncg8ti7fh2/projector-shading%20MY1.jpg?raw=1

Is that it? No, it can't be. Maybe I should not have used "inverted blank frame.jpg"
What did you use for the "black frame"?

Please post a functional script (one that runs) and any special source images,
so we can see what you are trying to do.

Motenai Yoda
17th December 2015, 23:31
yep some copy/paste errors...
for "black frame" I used the blank one (but isn't optimal)
also blank frame is darker than the sample, and is 719x575 :???:
sample too is overexposed, maybe a bit of headroom?

## MaskTools2 required
# https://github.com/tp7/masktools/releases
#~ LoadPlugin("tp7masktools\masktools2.dll")

## DitherTools required
# http://ldesoras.free.fr/prod.html#src_ditheravsi

## raw source
C=dss2("C:\Julie & Dad in sea.avi",preroll=100, pixel_type="RGB32")
\/*.ConvertToRGB()*/
\.separateRGBchannels()

## blank_frame
E=ImageSource("C:\blank frame.jpg",end = c.framecount(), fps = c.framerate(), pixel_type = "RGB32").addborders(1,1,0,0).rgbadjust(r=1.1,g=1.0,b=1.2)
\.separateRGBchannels()
## black_frame
F=ImageSource("C:\blank frame.jpg",end = c.framecount(), fps = c.framerate(), pixel_type = "RGB32").addborders(1,1,0,0).rgbadjust(r=0.06,g=.05,b=0.07)
\.separateRGBchannels()


simple_colorscaling(C,E,F)
\.mergeRGBchannels()


function separateRGBchannels(clip c){
interleave(c.showred(),c.showgreen(),c.showblue())
converttoy8()
}

function mergeRGBchannels(clip c){
MergeRGB(c.selectevery(3,0), c.selectevery(3,1), c.selectevery(3,2))
}

function simple_colorscaling(clip c, clip e, clip f, float "opacity", float "gamma", float "bias"){
opacity = Min(Max(0., Default(opacity, 1.)), 1.)
gamma = Min(Max(0.1, Default(gamma, 1.)), 10.)
bias = Min(Max(-50., Default(bias, 0.)), 50.)

#as all those clip are 8bit, difference can be done with

c_diff = mt_lutxy(c, f, "x y -", y=3, u=1, v=1)
e_diff = mt_lutxy(e, f, "x y -", y=3, u=1, v=1)

#using dither_lutxy8 (can be done with standard mt_lutxy too)

p1 = Dither_lutxy8(c_diff, e_diff, expr="x y / 1 "+String(gamma)+" / ^ 255 * "+String(opacity)+" * "+String(bias)+" + 256 *", y=3, u=1, v=1)

p2 = Dither_lut8(c, expr = " x 1 "+String(opacity)+" - * 256 *", y=3, u=1, v=1)

Dither_add16(p2, p1, y=3, u=1, v=1)
DitherPost()
}

http://thumbnails105.imagebam.com/45317/ad8bc4453161275.jpg (http://www.imagebam.com/image/ad8bc4453161275)
(the borders are a bit fucked coz division by 0)

raffriff42
18th December 2015, 22:09
I can't see how it works (exactly), but it does work. :)

raffriff42
19th December 2015, 02:08
Note I did not bother with gamma at all.Oops, I should have tried it at least, because the code was wrong (gain before gamma). Fixed @ post #18 (http://forum.doom9.org/showthread.php?p=1749450#post1749450).

Motenai Yoda
19th December 2015, 14:25
I can't see how it works (exactly), but it does work. :)
assuming blank frame (y) as upper bound and black frame (z) as lower bound, just levelling the video (x) with
((x-z)/(y-z))*255
that code is a bit dirty..
if it needs to be gamma adware, convert all to linear first and restore after.

raffriff42
19th December 2015, 20:36
assuming blank frame (y) as upper bound and black frame (z) as lower bound, just levelling the video (x) with
((x-z)/(y-z))*255...I made a test that seems to me a fair comparison between x/y and x*(-y) (divide and multiply for short).
Divide looks a bit better, with somewhat less tweaking required to get a decent result.

I do wonder about one thing, though: I don't see why you used (x-z) and (y-z) when z is almost
black (zero). It's very nearly a unity function. Leaving it out makes little difference - which is what I have done.

https://www.dropbox.com/s/1byywaxxn5diho1/projector-shading%20divide-test%2000.jpg?raw=1
(source)

https://www.dropbox.com/s/18xw8444nlcfwid/projector-shading%20divide-test%2002.jpg?raw=1
(multiply)

https://www.dropbox.com/s/480j4xffqbs09hu/projector-shading%20divide-test%2001c.jpg?raw=1
(divide)

Divide and Multiply are tweaked as well as I could make them, within the time (and patience) available to me.
Multiply has an faintly magenta spot in the center that is hard to get rid of, without under-correcting the edges.

Note: any color balance problems are due to my lack of time and skill. I could and should have matched them
more precisely. The point is that divide is a little flatter looking edge-to-center.

EDIT testing with other sources, it seems Divide gets crazy where overlay is very dark. It's not only
a divide-by-zero problem, it's a problem dividing by anything less than about 32. A simple fix would be a
"Levels(0,1,255,32,255,coring=false)" filter for the overlay clip, shown below in blue. Of course an empty frame
should never approach black; however, there may be other situations where multiply looks better.

EDIT @stephen22: on the source image above, notice the reds are completely clipped. Notice how after
adjustment, the red channel has a flat-topped appearance. This prevents proper recovery and may be the cause
of the magenta tint I am seeing. Note, the empty frame is not clipped. This means your camera settings changed
between taking the film and the empty frame, which is not good for this kind of recovery procedure. Fortunately,
the end result looks OK - at least in this scene. I'm curious what a white wall will look like.

Notes on the test code: I left out the (x-z), (y-z) for the moment.
My function now has global bias and gamma, and individual opacity controls for each channel.
I did not need to use the individual opacity controls with divide, which made my life easier.
I am using the RGB "waveform" scope instead of the "levels" scope, as it helps me pick out particular elements on screen.
I am expressing gain as (X/256.0), as this makes it easier to say "I want Red to peak ten units higher" (new gain = X+10)
Changing bias normally requires an equal and opposite change in X.
I am cropping the borders to hide the divide-by-zero artifacts (if that's what they are)
I am using Interleave to compare the three clips: for frame-by-frame evaluation only!
## "Compensating for uneven projector light" test...
# http://forum.doom9.org/showthread.php?t=172942

LoadPlugin("MaskTools2\tp7masktools\masktools2.dll")

## HistogramRGBLevels:
# http://forum.doom9.org/showthread.php?p=1570968#post1570968

## raw source
C=AviSource("D:\VideoProjects\work\projector-shading Julie & Dad in sea.avi")
\ .ConvertToRGB32
\ .Spline64Resize(768+8+12, 576+8+8) [* aspect ratio correction *]
#return C.HistogramRGBParade

## empty frame
E=ImageSource("D:\VideoProjects\work\projector-shading blank frame.jpg")
\ .BilinearResize(C.Width, C.Height)
\ .AssumeFPS(C).Loop(-1) [* make same framerate, extend running time *]
#return E.HistogramRGBParade

## (default arguments)
#X = filmfixrgb(C, E,
#\ opacity=0.50, bias=0, gamma=1.0, gain=(256/256.0),
#\ o_r=1.0, bias_r=000, gamma_r=1.0, gain_r=(256/256.0),
#\ o_g=1.0, bias_g=000, gamma_g=1.0, gain_g=(256/256.0),
#\ o_b=1.0, bias_b=000, gamma_b=1.0, gain_b=(256/256.0),
#\ coring=0>0, divide=true)

## DIVIDE TEST
D = filmfixrgb(C, E.Levels(0,1,255,32,255,coring=false),
\ opacity=0.50, bias=0, gamma=1.0, gain=(220/256.0),
\ o_r=1.0, bias_r=-10, gamma_r=1.0, gain_r=(255/256.0),
\ o_g=1.0, bias_g=-15, gamma_g=1.1, gain_g=(315/256.0),
\ o_b=1.0, bias_b=-15, gamma_b=0.8, gain_b=(370/256.0),
\ coring=0>0, divide=true)

## MULTIPLY by inverse TEST
M = filmfixrgb(C, E.Invert,
\ opacity=0.67, bias=-10, gamma=1.0, gain=(450/256.0),
\ o_r=0.5, bias_r=-05, gamma_r=1.0, gain_r=(195/256.0),
\ o_g=0.9, bias_g=000, gamma_g=1.0, gain_g=(310/256.0),
\ o_b=1.0, bias_b=-10, gamma_b=0.8, gain_b=(370/256.0),
\ coring=0>0, divide=false)

## FINAL
#return D.Crop(8, 8, -12, -8)
#\ .HistogramRGBParade

## COMPARE
return Interleave(
\ C.Crop(8, 8, -12, -8).sub("C"),
\ D.Crop(8, 8, -12, -8).sub("D"),
\ M.Crop(8, 8, -12, -8).sub("M")
\ ).HistogramRGBParade


#######################################
function sub(clip C, string msg)
{
return C.Subtitle(msg, align=7, size=C.Height/4, text_color=$fefefe, halo_color=$ff000000)
\ .Subtitle(msg, align=3, size=C.Height/4, text_color=$010101, halo_color=$ff000000)
## (text colors are 1 and 254, used as check on HistogramRGBParade...)
}

## MaskTools2 required
## https://github.com/tp7/masktools/releases
#######################################
### Try to cancel film "hot spot" or other shading problem
### by merging with open film gate or other compensation source.
### Includes color correction, done at the same time as the merge.
##
## See "Compensating for uneven projector light"
## http://forum.doom9.org/showthread.php?p=1750330#post1750330
##
## @ opacity : strength of effect; 0=none, 1=full; default 0.5
## @ bias : black level adjustment; default 0 (no change)
## @ gamma : middle tones; <1=darker, >1=lighter; default 1.0
## (attempts to preserve black & white, 16 & 235 if coring=true)
## @ gain : white level control; default 1.0 (no change)
## (attempts to preserve black, 16 if coring=true)
## @ o_r, g, b : additional opacity multiplier per channel; default 1.0
## @ bias_r, g, b : additional bias per channel; default 0
## @ gamma_r, g, b : additional gamma per channel; default 1.0
## @ gain_r, g, b : additional gain per channel; default 1.0
## @ coring : if true, calculations based 16-235 levels; default false
## @ divide : if true, merge as C/D, else, merge as C*D
##
function filmfixrgb(clip C, clip D,
\ float "opacity", int "bias", float "gamma", float "gain",
\ float "o_r", int "bias_r", float "gamma_r", float "gain_r",
\ float "o_g", int "bias_g", float "gamma_g", float "gain_g",
\ float "o_b", int "bias_b", float "gamma_b", float "gain_b",
\ bool "coring", bool "divide")
{
Assert(C.IsRGB && D.IsRGB,
\ "filmfixrgb: sources must be RGB")

opacity = Min(Max(0.0, Default(opacity, 0.5)), 1.0)
bias = Default(bias, 0)
bias_r = Default(bias_r, 0)
bias_g = Default(bias_g, 0)
bias_b = Default(bias_b, 0)
gamma = Min(Max(0.01, Default(gamma, 1.0)), 20.0)
gamma_r = Min(Max(0.01, Default(gamma_r, 1.0)), 20.0)
gamma_g = Min(Max(0.01, Default(gamma_g, 1.0)), 20.0)
gamma_b = Min(Max(0.01, Default(gamma_b, 1.0)), 20.0)
gain = Min(Max( 0.0, Default(gain, 1.0)), 200.0)
gain_r = Min(Max(-2.0, Default(gain_r, 1.0)), 200.0)
gain_g = Min(Max(-2.0, Default(gain_g, 1.0)), 200.0)
gain_b = Min(Max(-2.0, Default(gain_b, 1.0)), 200.0)
o_r = Min(Max(-2.0, Default(o_r, 1.0)), 2.0)
o_g = Min(Max(-2.0, Default(o_g, 1.0)), 2.0)
o_b = Min(Max(-2.0, Default(o_b, 1.0)), 2.0)
coring = Default(coring, false)
divide = Default(divide, true)

## normalize: "black"=0.0, "white"=1.0
sx = (coring) ? "((x-16)/219)" : "(x/255)"
sy = (coring) ? "((y-16)/219)" : "(y/255)"
sz = (coring) ? "*219+16" : "*255"

base_expr = (mode=="divide") [* with divide-by-zero trap *]
\ ? ("max(0,(((1-$o)*"+sx+")+($o*("+sy+"<(0.3/256)?1:("+sx+"/"+sy+"))))*$a)^(1/$g)"+sz+"+$b")
\ : ("max(0,(((1-$o)*"+sx+")+($o*" +sx+"*"+sy+ "))*$a)^(1/$g)"+sz+"+$b")

rexpr = base_expr
\ .StrReplace("$o", String(opacity * o_r))
\ .StrReplace("$a", String(gain * gain_r ))
\ .StrReplace("$g", String(gamma * gamma_r))
\ .StrReplace("$b", String(bias + bias_r ))

gexpr = base_expr
\ .StrReplace("$o", String(opacity * o_g))
\ .StrReplace("$a", String(gain * gain_g ))
\ .StrReplace("$g", String(gamma * gamma_g))
\ .StrReplace("$b", String(bias + bias_g ))

bexpr = base_expr
\ .StrReplace("$o", String(opacity * o_b))
\ .StrReplace("$a", String(gain * gain_b ))
\ .StrReplace("$g", String(gamma * gamma_b))
\ .StrReplace("$b", String(bias + bias_b ))

return uu_mt_rgblutxy(C, D,
\ mt_polish(rexpr), mt_polish(gexpr), mt_polish(bexpr))
#\ .Subtitle(rexpr)
}

## MaskTools2 required
## https://github.com/tp7/masktools/releases
#######################################
### RGB version of mt_lutxy
##
## @ Rexpr, Gexpr, Bexpr - MaskTools expression
## (see http://avisynth.nl/index.php/MaskTools2#Reverse_polish_notation)
##
function uu_mt_rgblutxy(clip C, clip D,
\ string Rexpr, string Gexpr, string Bexpr)
{
Assert(C.IsRGB && D.IsRGB,
\ "uu_mt_rgblutxy: sources must be RGB")

return (C.IsRGB24)
\ ? MergeRGB(
\ mt_lutxy(C.ShowRed("YV12"), D.ShowRed("YV12"),
\ yexpr=Rexpr,
\ U=1, V=1),
\ mt_lutxy(C.ShowGreen("YV12"), D.ShowGreen("YV12"),
\ yexpr=Gexpr,
\ U=1, V=1),
\ mt_lutxy(C.ShowBlue("YV12"), D.ShowBlue("YV12"),
\ yexpr=Bexpr,
\ U=1, V=1)
\ )
\ : MergeARGB(
\ C.ShowAlpha("YV12"),
\ mt_lutxy(C.ShowRed("YV12"), D.ShowRed("YV12"),
\ yexpr=Rexpr,
\ U=1, V=1),
\ mt_lutxy(C.ShowGreen("YV12"), D.ShowGreen("YV12"),
\ yexpr=Gexpr,
\ U=1, V=1),
\ mt_lutxy(C.ShowBlue("YV12"), D.ShowBlue("YV12"),
\ yexpr=Bexpr,
\ U=1, V=1)
\ )
}

# http://avisynth.nl/index.php/HDColorBars
function StrReplace(string base, string findstr, string repstr)
{
pos = FindStr(base, findstr)
return (StrLen(findstr)==0) || (pos==0)
\ ? base
\ : StrReplace(
\ LeftStr(base, pos-1) + repstr +
\ MidStr(base, pos+StrLen(findstr)),
\ findstr, repstr)
}

stephen22
24th December 2015, 13:15
Glad my little problem generated so much interest. (Most of what you say is above my head.)

Don't know why the blank image lost pixels at the edges - seems to have happened during the uploading process (the original files are 720 X 576.)


Note, the empty frame is not clipped. This means your camera settings changed between taking the film and the empty frame, which is not good for this kind of recovery procedure. Fortunately, the end result looks OK - at least in this scene. I'm curious what a white wall will look like.



Both taken same session, same avi file. I may well have adjusted the exposure slightly during capture.

Christmas greetings!