Skip to content

Instantly share code, notes, and snippets.

@pigeonhill

pigeonhill/M3 Brackets.lua

Last active Apr 19, 2021
Embed
What would you like to do?
M3 Focus & Exposure Bracketing
--[[
@title M3 Bracketing
@chdk_version 1.5
@subtitle Bracketing Settings
#mode = 0 "Focus bracket?" {Off X2INF Min2INF}
#bracket = 0 "Exposure bracket?" {Off +4Ev +3Ev +2Ev 2(+2Ev) 3(+2Ev) ISO1600 auto}
#sky = 0 "Sky Bracket?" {Off Sky+2Ev Sky+3Ev Sky+4Ev Sky+5Ev Sky+6Ev}
@subtitle Capture Settings
#infinity = 3 "Infinity focus (xH)" [2 4]
#overlap = 15 "Overlap (um)" [5 30]
#hilit = 20 "% histo HiLit" [5 30]
#pmag = 1 "Assumed Pupil Mag" [1 6]
@subtitle Other Settings
#dofs = 0 "DoF display" {Blur DoFs}
#traf = 0 "Traffic Lights" {Off On}
#sleep_time = 2 "Delay (s)"
#bookends = 1 "Bookends?" {Off On}
#screen_off = 0 "Screen?" {Off On}
#log = 0 "Log?" {Off On Reset}
#help = 0 "Console" {Off On}
#lens = 0 "Lens name?" {Off On}
Notes:
* Can only use registered EF-M lenses
* Script best used with WA lenses ;-)
* For retrofocus lenses use an assumed pupil mag at least as large as your guess. For telephoto use 1
* Register your lens below in the lens_info() function
* Not designed for macro focus stacking, ie best for deep focus photography only
* To stop M3 flicker, cycle the Canon screen (INFO) to not show the Canon histogram etc
* If you reset/delete the log, you need set the log menu to on or off after
* When paused, at the assess point, you can refocus and see the impact on bracketing. You can also change focal length, but you need to press the RIGHT key to rest things
* If traffic light (TL) feedback is enabled, once an image is captured, the left hand TL shows the near DoF's focus overlap state, relative to the last image captured...
...and the right TL sows the far DoF's overlap state relative to the last image captured. Yellow = same focus; green = a positive overlap; red = a negative overlap (a gap).
In addition, as you manually focus bracket, the centre info area of the bar changes from showing the focal length and the number of bracket, to the fL and the blur at the overlap
* Use pmag to ensure focus overlap insurance. Use 1 for a telephoto lens and a value greater than what you think it is for a retrofocus lens
* Also at the assess point, you can exit ALT mode and change aperture. Reentering ALT and pressing the RIGHT key to reset things.
* Exposure bracketing and Sky Bracketing are mutually exclusive. To undertake sky bracketing you need to set exposure bracketing to zero.
* All console measurements, focus and near DoF, are relative to the sensor.
* Note: the script attempts to detect Canon mode changes, eg PLAY or MENU etc, but isn't perfect ;-) If the bar disappears, just press ALT on and off.
Release 2.2566
photography.grayheron.net
April 2021
--]]
require "drawings"
capmode = require("capmode")
bi = get_buildinfo()
set_console_autoredraw(1)
set_console_layout(0,0,45,1)
if (bi.platform ~= "m3") then error('Only runs on M3') end
if log == 2 then
print_screen(1)
print("Log reset")
print("Reset log menu")
print_screen(0)
return
end
function hH() -- calculate the full & short versions of the hyperfocal
n = av96_to_aperture(get_user_av96()) -- N*1000
h = imath.mul(f,f)
temp = imath.mul(n,overlap)
h = imath.div(h,temp)
H = (f + h)/1000 -- 'full' hyperfocal in mm, as measured from the lens front principal, ie NOT the sensor as reported by Canon
end
function x2u(xx)
local temp = xx - t - (F*(1000+(Mmax*MFD)/xx))/1000
return temp
end
press("shoot_half")
repeat sleep(10) until get_shooting()
dof = get_dofinfo()
f = dof.focal_length -- in microns
F = f / 1000 -- focal length in mm
s = get_tv96()
av = get_av96()
start_s = s
release("shoot_half")
repeat sleep(10) until (not get_shooting())
steps = 1
old_x = 0
steps_back = 0
inf = 81000
function get_focus_distance_upper() -- returns focus distance in mm, but note 'accuracy' is 1cm from Canon. NOTE - NOT USED
local x = -1
if (bi.platsub == "101a") then
x = peek(0x00244916, 2)
elseif (bi.platsub == "120f") then
x = peek(0x0024495A, 2)
else
error('Not 101a or 120f')
end
if x ~= -1 then return x*10 else return x end
end
function get_focus_distance_lower() -- returns focus distance in mm, but note 'accuracy' is 1cm from Canon
local x = -1
if (bi.platsub == "101a") then
x = peek(0x00244918, 2)
elseif (bi.platsub == "120f") then
x = peek(0x0024495C, 2)
else
error('Not 101a or 120f')
end
if x == -1 then return -1 else return x*10 end
end
function refocus_to(xx) -- Only moves towards infinity. Returns requested focus distance in mm
local current_x = get_focus_distance_lower() -- current focus distance in mm to nearest cm. Based on Canon lower EXIF value
local steps_total = 0
local last_step_count = steps_total
if xx < current_x then return -1 end -- don't use
local last_step_x = current_x
while current_x < xx do
call_event_proc("EFLensCom.MoveFocus", steps, 1) -- move smallest step you can towards infinity
steps_total = steps_total + steps
sleep(25) -- for system to catch up
current_x = get_focus_distance_lower() -- Get Canon lower EXIF value
if (current_x ~= last_step_x) and (current_x < xx) then -- just went through an intermediate step change, ie not yet reached the step that covers xx
last_step_x = current_x
last_step_count = steps_total -- close enough
end
end
-- now fine tune position backwards if required
steps_back = ((current_x - xx) * (steps_total - last_step_count)) / (current_x - last_step_x)
if steps_back > 0 then
for i = 1, steps_back do call_event_proc("EFLensCom.MoveFocus", -steps, 1) end
end
sleep(25) -- for system to catch up
current_x = get_focus_distance_lower()
return xx -- request focus position, eg the 'best' estimate in mm as to where we really are
end
function lens_name()
local get_lens_name_fn = 0xfc2f3fd5
if (bi.platsub == "101a") then get_lens_name_fn = 0xfc2f3fc3 end
if call_event_proc('System.Create') == -1 then error('Unknown Error') end
local ppname = call_event_proc('AllocateMemory', 8)
if ppname == -1 or ppname == 0 then error('Unknown Error') end
local plen = ppname + 4
poke(ppname, 0)
poke(plen, 0)
local status = call_func_ptr(get_lens_name_fn, ppname, plen)
local pname = peek(ppname)
local len = peek(plen)
call_event_proc('FreeMemory', ppname)
if ppname == -1 or ppname == 0 then error('Unknown Error') end
local i = 0
local t = {}
while i < len do
local c = peek(pname + i, 1)
if c == 0 then break end
table.insert(t, string.char(c))
i = i + 1
end
local name = table.concat(t)
return name
end
function restore()
set_tv96_direct(s)
set_av96_direct(av)
press("shoot_half")
repeat sleep(10) until get_shooting()
sleep(100)
release("shoot_half")
repeat sleep(10) until (not get_shooting())
if current_focus_mode ~= 1 then set_mf(0) end
if (current_mode ~= "M") then capmode.set(current_mode) end
set_lcd_display(1)
exit_alt()
print_screen(0)
cls()
end
function set_up()
current_focus_mode = get_focus_mode()
current_mode = capmode.get_name()
if current_focus_mode ~= 1 then set_mf(1) end
s = get_tv96()
av = get_av96()
press("shoot_half")
repeat sleep(10) until get_shooting()
if sky > 0 and bracket == 0 then -- reset exposure for the sky/infinity bracket
set_tv96_direct(s - 96*(sky+1))
sleep(100)
end
if (current_mode ~= "M") then capmode.set("M") end
s = get_tv96()
set_tv96_direct(s)
set_av96_direct(av)
sleep(100)
release("shoot_half")
repeat sleep(10) until (not get_shooting())
dof = get_dofinfo()
f = dof.focal_length -- in microns
F = f / 1000 -- focal length in mm
lens_test = lens_info()
hH() -- hyperfocal info
end
function myshoot()
local prevcnt = hook_shoot.count()
local rawprevcnt = hook_raw.count()
press 'shoot_full_only'
repeat sleep(10) until prevcnt ~= hook_shoot.count()
release 'shoot_full_only'
repeat sleep(10) until rawprevcnt ~= hook_raw.count()
end
function X_bracket()
press("shoot_half")
repeat sleep(10) until (get_shooting())
set_tv96_direct(s)
if bracket == 0 or bracket >= 8 then
myshoot()
elseif bracket == 1 then
myshoot()
set_tv96_direct(s - 96 * 4)
myshoot()
elseif bracket == 2 then
myshoot()
set_tv96_direct(s - 96 * 3)
myshoot()
elseif bracket == 3 then
myshoot()
set_tv96_direct(s - 96 * 2)
myshoot()
elseif bracket == 4 then
myshoot()
set_tv96_direct(s - 96 * 2)
myshoot()
set_tv96_direct(s - 96 * 4)
myshoot()
elseif bracket == 5 then
myshoot()
set_tv96_direct(s - 96 * 2)
myshoot()
set_tv96_direct(s - 96 * 4)
myshoot()
set_tv96_direct(s - 96 * 6)
myshoot()
elseif bracket == 6 then
local iso = get_iso_mode()
set_iso_mode(100)
myshoot()
set_iso_mode(1600)
myshoot()
set_iso_mode(iso)
elseif bracket == 7 then
shot_histo_enable(1)
myshoot()
sleep(100)
lower = 1024 - 1024*hilit/100
test=get_histo_range(lower,1024)
step = 0
while test > 0 do
step = step + 1
set_tv96_direct(s + 96 * 2 * step)
myshoot()
sleep(100)
test=get_histo_range(lower,1024)
end
shot_histo_enable(0)
end
set_tv96_direct(s)
release("shoot_half")
repeat sleep(10) until (not get_shooting())
end
function bookend()
set_tv96_direct(960)
set_av96_direct(get_max_av96())
local ecnt = get_exp_count()
shoot()
set_tv96_direct(s)
set_av96_direct(av)
repeat sleep(10) until (get_exp_count() ~= ecnt)
end
function hiatus()
local m = imath.mul(f,Mmax)
local temp = imath.mul(2000,Fmax)
temp = imath.mul(temp,Mmax)+Fmax
local temp2 = imath.mul(2000,f)
temp2 = imath.mul(temp2,Mmax)
temp = temp-temp2
local m = imath.div(m,temp) -- Note this is an estimate of the mag at f, based on a simplified thin lens model that ignores extension at f, but not at Fmax
t = (F * (1000 + m) * (1000 + m))
t = MFD - t / (1000 * m) -- this is an estimate of the spilt thin lens thickness (mm)
return t
end
function lens_info()
if lens_name() == "EF-M11-22mm f/4-5.6 IS STM" then -- calculate lens thickness from manufacturer's data
-- add other lenses as required, ie replace the following for your lens
-- For the above lens, 300 is the lens max magnification x 1000, ie from manufacturer's data or your measurement
-- 22000 is the lens maximun zoom, or the prime, focal length in microns
-- 150 is the lens minimum focus distance (mm), ie from manufacturer's data or your measurement
Fmax = 22000 -- Longest focal length (mm) * 1000
Mmax = 300 -- Max magnification * 1000
MFD = 150 -- Minimum Focus Distance in mm
t = hiatus() -- distance between front and rear principals in mm
return true
elseif lens_name() == "EF-M28mm f/3.5 MACRO IS STM" then
Fmax = 28000 -- Longest focal length (mm) * 1000
Mmax = 1000 -- Max magnification * 1000
MFD = 97 -- Minimum Focus Distance in mm
t = hiatus() -- distance between front and rear principals in mm
return true
elseif lens_name() == "EF-M55-200mm f/4.5-6.3 IS STM" then
Fmax = 200000 -- Longest focal length (mm) * 1000
Mmax = 210 -- Max magnification * 1000
MFD = 960 -- Minimum Focus Distance in mm
t = hiatus() -- distance between front and rear principals in mm
return true
elseif lens_name() == "EF-M18-55mm f/3.5-5.6 IS STM" then
Fmax = 55000 -- Longest focal length (mm) * 1000
Mmax = 240 -- Max magnification * 1000
MFD = 150 -- Minimum Focus Distance in mm
t = hiatus() -- distance between front and rear principals in mm
return true
elseif lens_name() == "EF-M15-45mm f/3.5-6.3 IS STM" then
Fmax = 45000 -- Longest focal length (mm) * 1000
Mmax = 250 -- Max magnification * 1000
MFD = 250 -- Minimum Focus Distance in mm
t = hiatus() -- distance between front and rear principals in mm
return true
--[[
elseif lens_name() == "10-20mm" then
Fmax = 20000 -- Longest focal length (mm) * 1000
Mmax = 130 -- Max magnification * 1000
MFD = 240 -- Minimum Focus Distance in mm
t = hiatus() -- distance between front and rear principals in mm
return true
elseif lens_name() == "your lens" then
copy template above
]]
else -- lens not recognised, so don't use
return false
end
end
function update()
if x > inf then
temp1 = "=> INF"
elseif x >= 5000 then
temp1 = tostring(x/1000).."."..tostring(x/100-10*(x/1000)).."m"
elseif x >= 1000 then
temp1 = tostring(x/10).."cm"
else
temp1 = x.."mm"
end
u = x2u(x) -- estimate of focus distance from split thin lens front principal in mm
old_x = x
if x < inf then
local h2=h/1000
ndof = F*F/(u-F)
ndof = ndof+(h2*(F-h2*pmag))/(pmag*(u+h2-F))
ndof = ndof+(pmag*(2*F+h2+t)-F)/pmag
andof = ndof
if ndof > 1000 then ndof = tostring(ndof/10).."cm" else ndof = tostring(ndof).."mm" end
if u < H then
fdof = F*F/(u-F)
fdof = fdof-(h2*(F+h2*pmag))/(pmag*(u-h2-F))
fdof = fdof+(pmag*(2*F-h2+t)-F)/pmag
afdof = fdof
if fdof <= 1000 then
fdof = tostring(fdof).."mm"
elseif fdof < 5000 then
fdof = tostring(fdof/10).."cm"
else
fdof = tostring(fdof/1000).."."..tostring(fdof/100-10*(fdof/1000)).."m"
end
num = (imath.div(1000*H,1000*u)+1000)
num = 1+(imath.div(num,2000)+500)/1000
else
afdof = inf
fdof = "INF"
num = 1
end
else
andof = H
if andof > 1000 then ndof = tostring(andof/10).."cm" else ndof = tostring(andof).."mm" end
afdof = inf
fdof = "INF"
num = 1
end
inf_blur = (overlap*(H-F))/(u-F)
draw.replace(obj1,"rectf",0,0,lcd,35,"black","black")
if u < H then
temp = " <H : "
else
temp = " >=H : "
end
draw.replace(obj2,"string",(30*lcd)/720,0,temp..temp1,"white","black")
temp1 = F.."mm / #"..num
draw.replace(obj3,"string",(280*lcd)/720, 0,temp1,"white","black")
if dofs == 0 then
temp1 = ndof.." / "..inf_blur.."um"
else
temp1 = ndof.." / "..fdof
end
draw.replace(obj4,"string",(490*lcd)/720,0,temp1,"white","black")
if image and not get_alt_mode() and traf == 1 and last_image_u < inf then -- display traffic lights
local clr = "black"
if x == last_image_x then
clr = "yellow"
elseif (andof >= last_ndof) and (andof <= last_fdof) then
clr = "green"
elseif (andof > last_fdof) or (andof < last_ndof) then
clr = "red"
end
draw.replace(obj5,"rectf",5,5,20,25,clr,clr) -- update left traffic light
if x == last_image_x then
clr = "yellow"
elseif (afdof <= last_fdof) and (afdof >= last_ndof) then
clr = "green"
elseif (afdof > last_fdof) or (afdof < last_ndof) then
clr = "red"
end
draw.replace(obj6,"rectf",(700*lcd)/720,5,(715*lcd)/720,25,clr,clr) -- update right traffic light
temp1 = F.."mm / "
local ee = (f*(pmag-1))/(pmag*1000) -- pupil extension in mm, ie close enough
local blur = ee*(u+last_image_u)-2*u*last_image_u
blur = blur + F*(u+last_image_u-2*ee)
blur = (blur*(n/100))/10
blur = (1000*F*F*(u-last_image_u))/blur
if blur < 0 then blur = -blur end
if x < inf then
temp1 = temp1..blur.."um"
else
temp1 = temp1.."???"
end
draw.replace(obj3,"string",(280*lcd)/720, 0,temp1,"white","black")
else
clr = "black"
draw.replace(obj5,"rectf",5,5,20,25,clr,clr) -- update left traffic light
draw.replace(obj6,"rectf",(700*lcd)/720,5,(715*lcd)/720,25,clr,clr) -- update right traffic light
end
draw.overdraw()
dirty = false
end
function refresh()
dirty = true
last_x = 0
end
-- Main Section
set_up()
if lens_test == false then
print("Can only use registered EF-M lenses")
sleep(3000)
restore()
return
end
if bracket > 0 and sky > 0 then
print("Exposure bracketing clash!")
sleep(3000)
restore()
return
end
if lens == 1 then
print_screen(-1)
print(lens_name())
restore()
return
end
if mode == 2 then -- move to focus minimum
repeat
x = get_focus_distance_lower()
call_event_proc("EFLensCom.FocusSearchNear")
sleep(50)
until x == get_focus_distance_lower()
end
print_screen(0) -- switch off logging until bracketing
last_x = 0
dirty = true
lcd = 0
if help == 0 then -- then switch off console display
set_console_layout(0,0,1,0)
print("")
set_console_autoredraw(-1)
end
-- Set up the bar drawing objects
obj1 = draw.add("rectf", 0, 0, 0, 0, "black","black")
obj2 = draw.add("string", 0, 0, "","white","black")
obj3 = draw.add("string", 0,0,"","white","black")
obj4 = draw.add("string",0,0,"","white","black")
obj5 = draw.add("rectf", 0, 0, 0, 0, "black","black")
obj6 = draw.add("rectf", 0, 0, 0, 0, "black","black")
last_alt_state = get_alt_mode()
image = false -- no image taken yet
last_ndof = 0 -- info from the last image taken
last_fdof = 0
last_image_x = 0
andof = 0
afdof = 0
ecnt = get_exp_count()
tsec = get_day_seconds()
repeat -- stay here while in asssess mode
x = get_focus_distance_lower()
if last_x ~= x then dirty = true end
last_x = x
wait_click(50) -- check for a button press
if is_key("right") and get_alt_mode() then -- re-run set up tasks
set_up()
refresh()
image = false
elseif is_key("shoot_full") and not get_alt_mode() then -- taking an image outside of ALT mode
last_ndof = andof
last_fdof = afdof
last_image_x = x
last_image_u = u
repeat sleep(50) until ecnt ~= (get_exp_count())
ecnt = get_exp_count()
image = true
set_up()
refresh()
enter_alt()
exit_alt()
end
if is_key("shoot_half") and not get_alt_mode() then refresh() end -- redraws info bar if it disappears outside of ALT mode
if get_alt_mode() ~= last_alt_state then refresh() end -- refresh info bar
if ecnt ~= get_exp_count() then refresh() end
if get_gui_screen_width() ~= lcd then -- screens have changed
if get_gui_screen_width() == 720 then -- potentially show the console if requested
set_console_autoredraw(1)
set_console_layout(0,0,45,1)
refresh()
else -- don't show console when using EVF
set_console_autoredraw(-1)
set_console_layout(0,0,1,1)
print("")
refresh()
end
end
lcd = get_gui_screen_width()
last_alt_state = get_alt_mode()
if lcd == 720 and help == 1 then -- update console to show help reminder
print("SET=Run : RIGHT=Reset : FSP = EXIT")
end
if dirty and get_mode() then update() end -- only update the info bar if things have changed
sleep(500) -- no need to check too often
if (get_day_seconds()-tsec > 3) and x == last_x then
tsec = get_day_seconds()
refresh()
end
until ((is_key("set") or is_key("shoot_full")) and get_alt_mode()) -- then take brackets or exit
if help == 1 and lcd == 720 then -- show bracketing progress in main screen
set_console_autoredraw(1)
set_console_layout(0,0,45,1)
end
if log == 1 then
set_console_autoredraw(-1)
print_screen(-1)
print("...")
print(os.date())
set_console_autoredraw(1)
end
t1=os.time() -- start the clock
draw.clear()
print_screen(0)
print("Bracketing Started")
if log == 1 then print_screen(-1) end -- switch focus bracketing logging on
u = x2u(x) -- estimate of focus distance from split thin lens front principal in mm
old_x = x
if bookends == 1 then bookend() end
t1=sleep_time-(os.time()-t1)
if t1 > 0 then sleep(t1*1000) end
if screen_off == 1 then set_lcd_display(0) end
if x >= 1000 then
temp = x/10
print("1/"..num.." @ "..temp.."cm")
else
print("1/"..num.." @ "..x.."mm")
end
X_bracket() -- First exposure(s)
if mode == 0 then -- exposure bracket only requested so exit script after taking images at current focus
if sky > 0 and bracket == 0 then -- sky bracket requested
press("shoot_half")
repeat sleep(10) until (get_shooting())
set_tv96_direct(s + 96*(sky+1))
myshoot()
set_tv96_direct(s)
release("shoot_half")
repeat sleep(10) until (not get_shooting())
end
if bookends == 1 then bookend() end
restore()
return
end
i = 1
while (u < H/3) do -- capture the rest of the focus brackets up to H/3
-- Note that focus bracketing is calculated/updated in u space, but actual refocusing is in x space
-- All console feedback is in x space, ie distances measured from the sensor
new_u = 2*F*F*(pmag-1) + F*(u-2*pmag*u) + (h/1000)*pmag*u
new_u = new_u/( F*(2*pmag-1) + pmag*(h/1000-2*u) ) -- new u in mm, using DOFIS model
if new_u <= u then new_u = u + 5 end -- move at least 5mm, just in case. Remember: script is not designed for macro work!
u = new_u
x = (1000*u + f + (f*f)/(1000*u-f) + 1000*t) / 1000 -- next focus bracket estimate in x space: in mm from f(1+m)+t+u
refocus_to(x)
old_x = x
i = i + 1
if x >= 1000 then
temp = x/10
print(i.."/"..num.." @ "..temp.."cm")
else
print(i.."/"..num.." @ "..x.."mm")
end
X_bracket()
end
if u < inf and x < inf then -- take additional images beyond H
if u < H then
refocus_to((F*F)/(H-F) + F + t + H)
temp = (F*F)/(H-F) + F + t + H
else
temp = (F*F)/(u-F) + F + t + u
end
print("@H = "..temp/10 .."cm")
X_bracket()
temp = (F*F)/(infinity*H-F) + F + t + infinity*H
if temp > inf then -- adjust
repeat
infinity = infinity - 1
temp = (F*F)/(infinity*H-F) + F + t + infinity*H
until temp < inf or infinity == 2
end
refocus_to(temp)
print("@"..infinity.."H = ".. temp/10 .. "cm")
X_bracket()
if sky > 0 and bracket == 0 then -- take an extra exposure shot for the sky
press("shoot_half")
repeat sleep(10) until (get_shooting())
set_tv96_direct(s + 96*(sky+1))
myshoot()
set_tv96_direct(s)
release("shoot_half")
repeat sleep(10) until (not get_shooting())
print("Inf X @ ".. temp/10 .."cm")
end
end
if bookends == 1 then bookend() end
restore()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment