Last active
May 31, 2019 17:35
-
-
Save pigeonhill/d02e5d5a88516c67e929397d74af0142 to your computer and use it in GitHub Desktop.
Get Focus Bracket (ML)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Get Focus Brackets | |
This is a Lua 'simple script' to capture 'perfect' focus brackets from the current focus position to a defocus blur defined infinity. | |
I wrote it to cover auto capture, Multi-Image, Deep Field photography needs. | |
This version includes a modified split thin lens model, which better matches the real world. | |
There is no menu: just run the script from the ML Scripts menu. | |
The UI is simple, but functional: just follow the on screen instructions. The script pauses to allow you to decide if you wish to complete, ie in case | |
there are too many brackets. During the pause you can refocus, change aperture and focal length, ie to change the number of brackets. | |
During the pause the SET key can be used, ie for ETTRing. A long half shutter will take the brackets, any other key, including a half shutter press, but other than SET, will terminate the script. | |
The long half shutter (>2s) is indicated by a color change in the UI’s text, to green. | |
The script will display the estimated the number of brackets. The actual number may be different. That is the estimate assumes perfectly touching brackets, something that the Canon lens control can't achieve. | |
Script needs to run in LV, AF mode and M Mode. Script will switch to LV if called outside of LV. | |
The script displays the defocus, diffraction and total blurs at infinity. Remember ML shows you diffraction aware DoFs ;-) | |
If the current position is greater than the hyperfocal, then additional brackets will be taken, eg at H/3, H and 4*H, giving a 3 image depth of focus of H/4 to (4*H) infinity. | |
Set the ML CoC to the required bracket to bracket defocus blur overlap, ie focus insurance, but, sensibly, don't go below, say, 15/Crop. | |
Reducing the ML CoC below the 'normally recognised' CoC, eg 30/Crop , will result in more brackets, but higher focus quality through the scene. | |
Consider CoCs less than 2*sensor_pixel_size the minimum | |
The UI provides colour feedback: red means you are focused short of H (with H based on the ML CoC, ie the overlap blur); yellow means you are focused between H and 2*H; | |
In addition the infinity defocus blur and diffraction blur are shown as a % of the total | |
Green means you are between 2*H and 4*H; and orange beyond 4*H | |
To enable exposure brackets, simply turn on ML's Advanced Bracketing. | |
The algorithm used is OK for non-macro use, ie 'normal' lenses, and works best with WA lenses. | |
Two (dark exposure) bookend images are taken, one at the beginning and one at end of bracketing, to help differentiate the bracket set in post. Can be switched off by changing line 37. | |
The script is run from the ML Scripts menu or be triggered by the (separate) helper script. | |
***WARNING*** This script works reasonably well with WA, retrofocus lenses. It will work with longer lenses, eg telephoto, but you may see some strangeness ;-) | |
Tested on 5D3 with an EF 24-105 F4/L & Sigma 12-24 F4 | |
Add your lens below | |
Please let me know if you have any issues or feedback. | |
Release 1.75 | |
May 2019 | |
Garry George | |
photography.grayheron.net | |
--]] | |
direction = 1 | |
bookends = true -- set to false to switich off bookends | |
x = 0 | |
start_x = 0 | |
H = 0 | |
inf = 650000 | |
m = 0 | |
f = 0 | |
a = 0 | |
last_key = key.last | |
ML_blur = menu.get("DOF Settings","Circle of Confusion",0) -- set this as your overlap blur | |
infinity_blur = 0 | |
diff_blur = 0 | |
total_blur = 0 | |
blurs = "" | |
timer = 0 | |
trigger_time = 0 | |
timer_on = false | |
num = 3 | |
last_f = 0 | |
last_x = 0 | |
last_a = 0 | |
dirty = true | |
finish = false | |
mfd = 0 | |
max_fl = 0 | |
max_mag = 0 | |
min_fl = 0 | |
min_mag = 0 | |
lens_thickness = 0 | |
lens_exists = false | |
u = 0 -- lens to object distance = x - lens_thickness - (1+mag_@_x)*f | |
-- Lens info: just add your lens to the list. Script will detect a non-registered lens and provide name and direction info | |
-- For a prime lens, set min and max (mag and fl) values the same. Best to measure magnifications yourself, ie at min and max f for a zoom | |
-- Needs setting up for your (camera), eg FF vs Crop | |
lens_info = | |
{ | |
{name = "EF24-105mm f/4L IS USM", magmax = 0.2434, magmin = 0.0766, flmin = 24, flmax = 105, mfd = 450, dir = -1}, | |
{name = "12-24mm", magmax = 0.1589, magmin = 0.0876, flmax = 24, flmin = 12, mfd = 280, dir = -1}, | |
} | |
function myround(nu,dp) | |
-- nu = a number, dp = decimal places required to show | |
-- returns number as a string | |
if dp == 0 then | |
nu = tostring(math.floor(nu)) | |
else | |
nu = tostring(math.floor(nu)).."."..string.sub(tostring(math.floor(num*10^dp)),-dp,-1) | |
end | |
return nu | |
end | |
function my_shoot() | |
camera.shoot() | |
camera.wait() | |
end | |
function box() | |
display.rect(40, 150, 550, 135, COLOR.BLACK, COLOR.BLACK) | |
display.rect(43, 153, 546, 129, COLOR.BLACK, COLOR.TRANSPARENT_BLACK) | |
end | |
function lens_zero(xx) | |
local x = xx - lens_thickness | |
local magx = (x - 2*f - math.sqrt(x*(x - 4*f)))/(2*f) -- estimate of mag at xx using thin lens formulation, after accounting for the split lens thickness | |
xx = lens_thickness + (1 + magx)*f | |
return xx | |
end | |
function my_display(text,time,x_pos,y_pos) | |
text = string.rep("\n",y_pos)..string.rep(" ",x_pos)..text | |
display.notify_box(text,time*1000) | |
end | |
function set_focus(xx) | |
local lens_ok = true | |
repeat sleep(0.1) until lv.running | |
local current_x = lens.focus_distance | |
if xx == current_x then return xx end | |
if xx > current_x then | |
while lens.focus_distance < xx and lens_ok do | |
lens_ok = lens.focus(direction,2,true) | |
end | |
sleep(0.1) | |
lens_ok = true | |
while lens.focus_distance > xx and lens_ok do | |
lens_ok = lens.focus(-direction,1,true) | |
end | |
else | |
while lens.focus_distance > xx and lens_ok do | |
lens_ok = lens.focus(-direction,2,true) | |
end | |
sleep(0.1) | |
lens_ok = true | |
while lens.focus_distance < xx and lens_ok do | |
lens_ok = lens.focus(direction,1,true) | |
end | |
end | |
return lens.focus_distance -- final focus distance, ie may not be extactly x ;-) | |
end | |
function calibrate() | |
f = lens.focal_length | |
x = lens.focus_distance | |
a = camera.aperture.value | |
start_x = x | |
for i = 1, #lens_info, 1 | |
do | |
if lens.name == lens_info[i].name then | |
max_mag = lens_info[i].magmax | |
max_fl = lens_info[i].flmax | |
min_mag = lens_info[i].magmin | |
min_fl = lens_info[i].flmin | |
mfd = lens_info[i].mfd | |
direction = lens_info[i].dir | |
lens_exists = true | |
end | |
end | |
if not lens_exists then | |
direction = 1 | |
start_x = lens.focus_distance | |
local lens_ok = lens.focus(1,3,true) -- test lens chirality | |
sleep(0.2) | |
if lens.focus_distance < start_x then | |
direction = -direction | |
elseif lens.focus_distance == start_x and start_x < inf then | |
direction = -direction | |
end | |
lens_ok = set_focus(start_x) | |
display.print("Need to register lens",50,160,FONT.SANS_32) | |
display.print("Lens name:"..lens.name,50,200,FONT.SANS_32) | |
display.print("Direction: "..tostring(direction),50,240,FONT.SANS_32) | |
display.print("Press halfshutter to clear",50,280,FONT.SANS_32) | |
key.wait(KEY.HALFSHUTTER) | |
display.clear() | |
end | |
dirty = true | |
end | |
function bookend() | |
local test = menu.get("Shoot","Advanced Bracket",0) | |
if test == 1 then | |
menu.set("Shoot","Advanced Bracket",0) | |
repeat sleep(0.1) until menu.get("Shoot","Advanced Bracket",0) == 0 | |
end | |
local test2 = menu.get("Expo","Dual ISO",0) | |
if test2 == 1 then | |
menu.set("Expo","Dual ISO",0) | |
repeat sleep(1) until menu.get("Expo","Dual ISO",0) == 0 | |
end | |
local t = camera.shutter.apex | |
camera.shutter.apex = 10 | |
my_shoot() | |
camera.shutter.apex = t | |
if test == 1 then | |
menu.set("Shoot","Advanced Bracket",1) | |
repeat sleep(0.1) until menu.get("Shoot","Advanced Bracket",0) == 1 | |
end | |
if test2 == 1 then | |
menu.set("Expo","Dual ISO",1) | |
repeat sleep(0.1) until menu.get("Expo","Dual ISO",0) == 1 | |
end | |
end | |
function update() | |
local mag = max_mag | |
if max_fl ~= min_fl then -- linear estimate of mag at f | |
mag = min_mag+((max_mag-min_mag)*(f-min_fl))/(max_fl-min_fl) | |
end | |
lens_thickness = mfd - (f/mag)*(1+mag)*(1+mag) -- estimate of the virtual thickness of the lens at f: could be negative! | |
dirty = false | |
H = f + (1000*f*f)/(a*ML_blur) | |
box() | |
display.print("Estimate "..tostring(num).." brackets",50, 200,FONT.SANS_32,COLOR.TRANSPARENT_BLACK,COLOR.TRANSPARENT_BLACK) | |
num = 1 + math.ceil((1 + (H/(x-lens_zero(x))))/2) | |
if num < 3 then num = 3 end | |
if lens_thickness > 0 then | |
display.print("Estimate "..tostring(num).." brackets",50, 200,FONT.SANS_32,COLOR.WHITE,COLOR.TRANSPARENT_BLACK) | |
else | |
display.print("Estimate "..tostring(num).." brackets",50, 200,FONT.SANS_32,COLOR.RED,COLOR.TRANSPARENT_BLACK) | |
end | |
infinity_blur = ML_blur*H/(x-lens_zero(x)) | |
diff_blur = 2.44*a*0.550 -- assumes you are shooting in the visible bands and not IR (use 0.850 here or someother number that matches your conversion/filter) | |
total_blur = math.sqrt(infinity_blur*infinity_blur + diff_blur*diff_blur) | |
display.print("Def/Dif/Tot Blurs: "..blurs.." um" ,50, 160,FONT.SANS_32,COLOR.TRANSPARENT_BLACK,COLOR.TRANSPARENT_BLACK,540) | |
blurs = myround(infinity_blur,0).."/"..myround(diff_blur,0).."/"..myround(total_blur,0) | |
display.print("Def/Dif/Tot Blurs: "..blurs.." um",50, 160,FONT.SANS_32,COLOR.WHITE,COLOR.TRANSPARENT_BLACK,540) | |
if infinity_blur > ML_blur then | |
display.rect(400, 240, 150, 30, COLOR.BLACK, COLOR.RED) | |
elseif infinity_blur > ML_blur/2 and infinity_blur < ML_blur then | |
display.rect(400, 240, 150, 30, COLOR.BLACK, COLOR.YELLOW) | |
elseif infinity_blur < ML_blur/2 and infinity_blur > ML_blur/4 then | |
display.rect(400, 240, 150, 30, COLOR.BLACK, COLOR.GREEN1) | |
elseif infinity_blur < ML_blur/4 and x < inf then | |
display.rect(400, 240, 150, 30, COLOR.BLACK, COLOR.ORANGE) | |
elseif x > inf then | |
display.rect(400, 240, 150, 30, COLOR.BLACK, COLOR.WHITE) | |
end | |
if timer <= 2.0 then | |
display.print("Long Half Shutter if OK",50,240,FONT.SANS_32,COLOR.WHITE,COLOR.TRANSPARENT_BLACK) | |
else | |
display.print("Long Half Shutter if OK",50,240,FONT.SANS_32,COLOR.GREEN1,COLOR.TRANSPARENT_BLACK) | |
end | |
-- display infinity defocus and diffraction blurs as a percentage of total | |
local percent1 = (infinity_blur/total_blur)*150 | |
display.rect(400, 245, percent1, 10, COLOR.DARK_GRAY, COLOR.DARK_GRAY) | |
local percent2 = (diff_blur/total_blur)*150 | |
display.rect(400, 255, percent2, 10, COLOR.GRAY, COLOR.GRAY) | |
end | |
function capture_brackets() | |
if menu.get("Get Focus Brackets","Autorun",0) == 1 then | |
menu.set("Get Focus Brackets","Autorun",0) | |
return | |
end | |
if not lv.running then lv.start() end | |
repeat sleep(0.1) until lv.running | |
if not (camera.mode == MODE.M) then my_display("Must be in M Mode",2,0,5) return end | |
if menu.visible then menu.close() end | |
if console.visible then console.hide() end | |
if not lens.af then my_display("Switch AF on",2,0,5) return end | |
calibrate() | |
if not lens_exists then return end | |
--[[ | |
Note: all calculations are relative to the lens front principal plane, which we don't know for real: so we assume a split thin (or quasi thick) lens for simplicity | |
All distances/moves are realtive to the sensor plane and Canon/ML tells us this! | |
Also: any change to focus will result in the bracketing starting at the new focus position, ie not at the position when the script started. | |
--]] | |
display.pixel(30,200,COLOR.RED) | |
last_f = lens.focal_length | |
last_x = lens.focus_distance | |
last_a = camera.aperture.value | |
repeat -- note SET and HALFSHUTTER keys are reserved/protected | |
f = lens.focal_length | |
x = lens.focus_distance | |
a = camera.aperture.value | |
last_key = key.last | |
if display.pixel(30,200) ~= COLOR.RED then | |
dirty = true | |
display.pixel(30,200,COLOR.RED) | |
end | |
if last_a ~= a or last_x ~= x or last_f ~= f or dirty or timer_on then -- need to update things | |
display.draw(update) | |
end | |
if key.last == KEY.HALFSHUTTER and not timer_on then | |
trigger_time = dryos.clock | |
timer_on = true | |
end | |
if key.last == KEY.UNPRESS_HALFSHUTTER then | |
timer_on = false | |
finish = true | |
end | |
if timer_on then timer = dryos.clock - trigger_time end | |
if (key.last == KEY.SET or key.last == KEY.UNPRESS_SET) then | |
dirty = true | |
end -- ETTR so update UI | |
last_f = f | |
last_x = x | |
last_a = a | |
sleep(0.2) -- refresh delay | |
until (last_key ~= key.last and (not (key.last == KEY.SET or key.last == KEY.UNPRESS_SET)) and (not (key.last == KEY.HALFSHUTTER or key.last == KEY.UNPRESS_HALFSHUTTER))) or finish | |
display.clear() | |
if timer <= 2 then | |
my_display("Finished",2,0,0) | |
return end | |
if bookends then bookend() end | |
f = lens.focal_length | |
x = lens.focus_distance | |
a = camera.aperture.value | |
local mag = max_mag | |
if max_fl ~= min_fl then -- linear estimate of mag at f | |
mag = min_mag+((max_mag-min_mag)*(f-min_fl))/(max_fl-min_fl) | |
end | |
lens_thickness = mfd - (f/mag)*(1+mag)*(1+mag) -- estimate of the virtual thickness at f | |
H = f + (1000*f*f)/(a*ML_blur) -- relative to the (modified) thin lens' principal plane (zero), ie not the sensor | |
start_x = x | |
u = x - lens_zero(x) -- Split thin lens lens estimate of distance between lens zero (front principal plane) and the object | |
local take_3 = false | |
if u > H/2 then take_3 = true end | |
local new_x = x | |
while u < H/3 do | |
x = set_focus(new_x) | |
my_shoot() | |
u = x - lens_zero(x) | |
new_x = lens_zero(x)+u+(2*(f-u)*(f-u))/(f+H-2*u) -- position of next focus, based on Merklinger's formulation of DoF | |
end | |
if (x - lens_zero(x)) <= H/3 or take_3 then -- reasonable to ignore magnification from now on as focusing around/beyond H | |
new_x = f + lens_thickness + H/3 | |
x = set_focus(new_x) | |
lens.focus(direction,1,true) | |
x = lens.focus_distance | |
my_shoot() | |
end | |
if (x - lens_zero(x)) <= H or take_3 then | |
new_x = f + lens_thickness + H | |
x = set_focus(new_x) | |
lens.focus(direction,1,true) | |
x = lens.focus_distance | |
my_shoot() | |
end | |
new_x = f + lens_thickness + 4*H | |
x = set_focus(new_x) | |
lens.focus(direction,1,true) | |
my_shoot() | |
if bookends then bookend() end | |
end | |
if menu.get("Focus","Bracketeer","") == "ON" then return end | |
capture_brackets() | |
set_focus(start_x) | |
my_display("Finished",2,0,0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment