Skip to content

Instantly share code, notes, and snippets.

@pigeonhill
Last active May 1, 2021 11:31
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/e40b415b8f751e8369c0edc9d09003c6 to your computer and use it in GitHub Desktop.
Save pigeonhill/e40b415b8f751e8369c0edc9d09003c6 to your computer and use it in GitHub Desktop.
fmath version of M3 Bracketing
--[[
@title Brackets (M3)
@chdk_version 1.5
@subtitle Bracketing Options
#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 Bracketing 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:
* This version only runs on the M3 with the fmath library version of CHDK
* 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 EF-M lens below in the lens_info() function
* Not designed for macro focus stacking, ie best for deep focus photography only
* To stop flickering, cycle the Canon screen (INFO), eg to not show the Canon histogram
* 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 reset 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 changes, eg PLAY or MENU etc, but isn't perfect ;-) If the bar disappears, just press ALT on and off, or wait for M3 Bracketing to auto refresh
Release 1.07
photography.grayheron.net
April 2021
--]]
capmode = require("capmode")
bi = get_buildinfo()
if (bi.platform ~= "m3") then
print("Only runs on M3")
sleep(3000)
return
end
local s,e = pcall(function() local w=fmath.new(0) end)
if s == false then
print("Can't use script")
sleep(3000)
return
end
require "drawings"
props=require'propcase'
set_console_autoredraw(1)
set_console_layout(0,0,45,1)
if log == 2 then
print_screen(1)
print("Log reset")
print("Reset log menu")
print_screen(0)
return
end
press("shoot_half")
repeat sleep(10) until get_shooting()
dof = get_dofinfo()
F = fmath.new(dof.focal_length,1000) -- focal length in mm (real)
s = get_prop(props.TV)
av = get_prop(props.AV)
n = fmath.new(av96_to_aperture(av),1000) -- aperture number as a real
start_s = s
s_ev = s
release("shoot_half")
repeat sleep(10) until (not get_shooting())
steps = 1
steps_back = 0
inf = 81000 -- infinity trap
function units(xx)
local xr =""
if xx:int() > 5000 then
xr=(xx/1000):tostr(1).."m"
elseif xx:int() > 1000 then
xr=(xx/10):tostr(0).."cm"
else
xr=(xx):tostr(0).."mm"
end
return xr
end
function hH() -- calculate the full & short versions of the hyperfocal
n = fmath.new(av96_to_aperture(get_user_av96()),1000) -- convert to a real aperture number
h = (1000*F*F)/(n*overlap) -- short hyperfocal (mm)
H = h + F -- 'full' hyperfocal (mm), as measured from the lens front principal, ie NOT the sensor as reported by Canon
end
function update_DoFs(xx) -- update stuff for DoFs etc
local q = xx-t
u = (((q*(q-4*F)):sqrt() + q)/2):int() -- focus distance in mm from front principal
if xx < inf then
ndof = F*F/(u-F)
ndof = ndof+(h*(F-h*pmag))/(pmag*(u+h-F))
ndof = (ndof+(pmag*(2*F+h+t)-F)/pmag)
else
ndof = H
end
if u < H:int() then
fdof = F*F/(u-F)
fdof = fdof-(h*(F+h*pmag))/(pmag*(u-h-F))
fdof = (fdof+(pmag*(2*F-h+t)-F)/pmag)
num = (1+(H/u + 1)/2):int()
else
fdof = fmath.new(inf+1)
num = 1
end
end
function get_focus_distance_upper() -- returns upper focus distance in mm, but note 'accuracy' is 1cm from Canon. NOTE - NOT USED IN THIS SCRIPT
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 lower 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 = fmath.new(dof.focal_length,1000) -- focal length in mm (real)
lens_test = lens_info()
hH() -- update 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 = fmath.new(Mmax,1000)
local Fr = fmath.new(Fmax,1000)
-- Note the following is an estimate of the mag at f, based on simply scaling the max mag by the focal length ratio
-- A better model would be to explicity measure the mag at a focal length, or at least at the zooms and interpolate
-- Maybe in the future ;-)
m=M*F/Fr
t = (F * (1 + m) * (1 + m))
t = MFD - t/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
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
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
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
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
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
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()
update_DoFs(x) -- update u and DoFs
draw.replace(obj1,"rectf",0,0,lcd,35,"black","black") -- clear the info bar
if x > inf then
temp = ">=H : INF"
elseif u >= H:int() then
temp = ">=H : "..units(fmath.new(x,1))
else
temp = " <H : "..units(fmath.new(x,1))
end
draw.replace(obj2,"string",(30*lcd)/720,0,temp,"white","black") -- draw focus position info
inf_blur = ((overlap*(H-F))/(u-F)):int()
if not get_alt_mode() and traf == 1 and not image then -- change to Ev feedback
temp_s = ((get_user_tv96()-s_ev)*10)/96
local sign = -1
if temp_s < 0 then
sign = 1
temp_s = -temp_s
end
temp = ((sign*temp_s)/10).."."..(temp_s-10*(temp_s/10)).."ev / #"..num
else
temp = F:int().."mm / #"..num
end
draw.replace(obj3,"string",(280*lcd)/720, 0,temp,"white","black")
if dofs == 0 then
temp = units(ndof).." / "..inf_blur.."um"
else
if u < H:int() then
temp = units(ndof).." / "..units(fdof)
else
temp = units(ndof).." / INF"
end
end
draw.replace(obj4,"string",(490*lcd)/720,0,temp,"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 (ndof >= last_image_ndof) and (ndof <= last_image_fdof) then
clr = "green"
elseif (ndof > last_image_fdof) or (ndof < last_image_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 (fdof <= last_image_fdof) and (fdof >= last_image_ndof) then
clr = "green"
elseif (fdof > last_image_fdof) or (fdof < last_image_ndof) then
clr = "red"
end
draw.replace(obj6,"rectf",(700*lcd)/720,5,(715*lcd)/720,25,clr,clr) -- update right traffic light
local ee = (F*(pmag-1))/(pmag) -- pupil extension in mm
local blur = ee*(u+last_image_u)-2*u*last_image_u
blur = blur + F*(u+last_image_u-2*ee)
blur = blur*n
blur = ((1000*F*F*(u-last_image_u))/blur):int() -- blur at overlap in microns
if blur < 0 then blur = -blur end
if x < inf then
temp = F:int().."mm / "..blur.."um"
else
temp = F:int().."mm / ".."???"
end
draw.replace(obj3,"string",(280*lcd)/720, 0,temp,"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
-- Main Section
if lens == 1 then
print_screen(-1)
print(lens_name())
sleep(3000)
restore()
return
end
if bracket > 0 and sky > 0 then
print("Exposure bracketing clash!")
sleep(3000)
restore()
return
end
set_up()
if lens_test == false then
print("Can only use registered EF-M lenses")
sleep(3000)
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_image_ndof = fmath.new(0)
last_image_fdof = fmath.new(0)
last_image_x = 0
last_image_u = 0
ecnt = get_exp_count()
tsec = get_day_seconds()
repeat -- stay here while in asssess/shooting 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()
dirty = true
elseif is_key("shoot_full") and not get_alt_mode() then -- taking an image outside of ALT mode
last_image_ndof = ndof
last_image_fdof = fdof
last_image_x = x
last_image_u = u
repeat sleep(50) until ecnt ~= (get_exp_count())
ecnt = get_exp_count()
image = true
dirty = true
elseif is_key("shoot_half") and not get_alt_mode() then
dirty = true
end
if get_alt_mode() ~= last_alt_state then -- refresh a few things if ALT state changes
dirty = true
last_alt_state = get_alt_mode()
image = false
s_ev = get_user_tv96() -- reset ev feedback
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)
dirty = true
else -- don't show console when using EVF
set_console_autoredraw(-1)
set_console_layout(0,0,1,1)
print("")
dirty = true
end
end
lcd = get_gui_screen_width()
if lcd == 720 and help == 1 then -- update console to show help reminder
print("SET=Run : RIGHT=Reset : FSP = EXIT")
end
if ((get_day_seconds()-tsec > 2) and x == last_x) then
dirty = true
tsec = get_day_seconds()
end
if dirty then update() end -- update the info bar
sleep(500) -- no need to check too often
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
update_DoFs(x) -- update stuff
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
print("1/"..num.." @ "..units(fmath.new(x)))
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):int()) 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 distance estimates measured from the sensor
new_u = 2*F*F*(pmag-1) + F*(u-2*pmag*u) + h*pmag*u
new_u = (new_u/( F*(2*pmag-1) + pmag*(h-2*u))):int() -- 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 = (u + F + (F*F)/(u-F) + t) -- next focus bracket estimate in x space: in mm from f(1+m)+t+u
refocus_to(x:int())
i = i + 1 -- keep track of number of images taken
print(i.."/"..num.." @ "..units(x))
X_bracket()
end
if u < inf and x:int() < inf then -- take additional images at/beyond H
if u < H:int() then
temp = ((F*F)/(H-F) + F + t + H)
refocus_to(temp:int())
else
temp = ((F*F)/(u-F) + F + t + u)
end
print("@H = "..units(temp))
X_bracket()
temp = ((F*F)/(infinity*H-F) + F + t + infinity*H)
if temp:int() > inf then -- adjust as at/beyond lens infinity
repeat
infinity = infinity - 1
temp = ((F*F)/(infinity*H-F) + F + t + infinity*H)
until temp:int() < inf or infinity == 2
end
refocus_to(temp:int())
print("@"..infinity.."xH = "..units(temp))
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 @ "..units(temp))
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