Skip to content

Instantly share code, notes, and snippets.

@pigeonhill
Last active May 31, 2019 17:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pigeonhill/d02e5d5a88516c67e929397d74af0142 to your computer and use it in GitHub Desktop.
Save pigeonhill/d02e5d5a88516c67e929397d74af0142 to your computer and use it in GitHub Desktop.
Get Focus Bracket (ML)
--[[
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