LightRails – Dynamic External Exposure Control for Time-lapse
I’ve been sitting on this a little while, and I feel a bit remiss in not sharing it sooner. I had intended to make it “perfect” before sharing, but feel I’ve reached a point where I’m not ready to spend all of my effort on this project, and instead wish to work more on OpenMoco (but we’ll talk more about that soon!). So, I’ll share this as a more “rough” project.
Some time ago, I started out with the TSL230R chip from Taos with the intent of producing a (a) my own digital light meter and (b) a dynamic external control system for time-lapse photography. Certainly, the act of creating my own digital light meter was a smashing success, also having it control my dSLR via a remote cable in bulb mode was also a great success. Enough so that it should prove an invaluable tool to the DIY pinhole photographer. (Man, how do you time that 2:35:20 exposure manually? =)
The problem I wanted to solve was a difficult, but common one: how do you effectively manage exposure changes across sunrise and sunset time-lapses with a dSLR? It sounds very easy, just go into Av or Tv mode! Of course, life isn’t that easy. The standard dSLRs generally only meter and adjust exposure in the very rough terms of 1/6, 1/4, 1/3, or even 1/2 EV steps! This means it has a sledge hammer where the problem calls for a gentle tapping of a finger. I set out to create a system that would control the camera externally, metering and adjusting exposure in 1/100 EV steps…
Some more information about how this all got started, and some color commentary along the way can be found in these two forum postings over at timescapes.org:
Testing Dynamic External Exposure Control
Unfortunately, the system is still a bit rough for time-lapse photography in controlling exposure in all conditions, but it can be made to succeed in certain conditions, given practice and patience. I’m not going to go into great detail on instructions, as it should be largely evident based on the information below, but you will need the following parts at a minimum to use this code:
- (1x) Arduino board with Atmega168P chip (will probably work with 328p)
- (1x) Taos TSL230R light sensor IC
- (1x) 16×2 Parallel LCD screen
- (1x) 4N28 opto-isolator (isolating camera trigger circuitry)
- (5x) SPST mometary switches
- (1x) 0.1uF Ceramic Capacitor
Effectively, the sensor should be placed remote from the control unit (but within specs of the TSL230R), and the control unit uses an LCD display to display information, and five momentary switches to control operation and input data. The best configuration I’ve found for using this for time-lapse is to create a reflector, placing the sensor and the capacitor in a separate enclosure from the control, and place the sensor inside of the reflector. For a reflector, I simply used three pieces of wood connected like a ‘C’, painted the inside flat white, and mounted the sensor housing inside of the ‘C’, facing downwards. The reflector could then either be placed in complete shade, or pointed towards the scene. Either way, the unit should be protected from over-head light changes – it should either meter the scene on average, or the amount of light read in shade. Which is better depends on the scene in question — shooting sunny sky-scenes with dim foregrounds generally require scene metering, whereas equally-lit sky and foregrounds prefer shade-readings. It is also capable of metering through your eye-piece, but resolution suffers greatly (set TTL F/stop in the UI to enable this mode).
The system is configured with ISO, f/stop, minimum and maximum exposure times, number of EV divisions per step (best left at 100), and shot interval. The light reading is calculated once per second, and exposure time calculated from the settings and light reading. EV adjust is set to achieve the proper exposure vs. the metered exposure, and the system is left to run.
Random fluctuations and massive jumps from physical interference (say a dog sniffing at your meter while you’re napping =) are prevented through the simple application of a ‘ev change ceiling’ setting, which prevents any change in calculated EV greater than the ceiling (in x/y EV steps — e.g.: no change greater than 3/100’s EV.) — this method has proven its self as both the most simple and effective means through empirical testing, beating out averaging, weighting, and all other sorts of complicated methods.
If you find this useful, come up with a novel application, or have any questions – please don’t hesitate to drop me a comment here. I’d love to know what you do with it, or help you if you need it.
The following videos should give you some idea how to assemble and use it:
note: both videos show older versions of the UI and the device, where the sensor was integrated into the controller. This is not recommended.
And, here’s an example of its performance:
And, without further ado, here’s the code. Each section should be pasted into a seperate tab in the Arduino UI, with the first section of code in the main tab.
lightrails.pde:
/*
LightRails 1.0
A dynamic external exposure control system with
integrated intervalometer for time-lapse
photography
Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com
This program is free software: you can redistribute it
and/or modify it under the terms of the GNU General
Public License as published by the Free Software
Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License
for more details.
*/
#include <LiquidCrystal.h>
#include <avr/pgmspace.h>
#include <MsTimer2.h>
#define TSL_FREQ_PIN 2
#define TSL_S0 6
#define TSL_S1 5
#define TSL_S2 3
#define TSL_S3 4
#define CAMERA_PIN 13
// buttons
#define B_LT 9
#define B_RT 10
#define B_UP 7
#define B_DN 8
#define B_CT 11
// max number of setup steps available
#define MAX_SETUP_STEPS 12
// read frequency for how many milliseconds
// note: this code only works for 1000 any other value
// and you need to adjust your frequency calculation
#define READ_TM 1000
// low and high thresholds for adjusting sensitivity
// of the TSL230R. If freq is below LO thresh, it will
// automatically increase sensitivity. If it increases
// beyond the HI thresh, it will decrease sensitivity
#define SENS_THRESH_LO 101
#define SENS_THRESH_HI 7999
// LCD print buffer
// set to LCD width + 1
#define SIZE_OF_LCD_BUF 17
// threshold to accept a frequency change
// the frequency read off the chip must change by
// at least this many Hz to accept the change
// this prevents fluttering. However, it can cause
// problems with low-light level changes. You may need
// to adjust according to types of shooting you
// typically do (higher for more day shooting
// and lower for more night shooting)
#define FREQ_CHG_THRESH 20
// the minimum exposure gap is the minimum time
// between triggering exposures (for the cases where
// exposure_time exceeds interval_time). At a bare
// minimum, it should be the minimum amount of time
// required by your camera to consistently register
// the completion of one exposure and execution of
// another. this time is in milliseconds
#define MIN_EXP_GAP 1000
// some info about exposure data
// need iso and f/stop to calculate
// exposure time. These are just
// defaults - they are adjusted via the UI
int iso_rating = 100;
float f_stop = 8.0;
// Maximum Diff % in EV Value calculated from reading to reading
byte ev_diff_ceiling = 0;
// for through-a-lens metering - f-stop of the lens
// (light read must be calculated)
float ttl_stop = 0.0;
// default times
unsigned int camera_delay = 5;
unsigned int min_exp_tm = 10;
byte max_exp_tm = 20;
// actual delay time in mS
unsigned long real_camera_delay = camera_delay * 1000;
// running variables
float uwcm2 = 0; // uw/cm2 calculated
float lux = 0; // Lux caclulated
float ev = 0; // EV value calculated
float exp_tm = 0; // camera exposure time calculated in 1/x value
float pre_exp_tm = 0; // for latching, need previous reading
unsigned long frequency = 0; // frequency read
unsigned long cur_exp_tm = 0; // current exposure time in mS
unsigned int shots_fired = 0; // how many exposures taken since interval started
bool camera_engaged = false; // camera currently exposing?
bool light_type = false; // which spd graph to use - d65 [0] or ilA [1]
// this is used by the pulse counter.
volatile unsigned long pulse_cnt = 0;
// for calculating time differences
unsigned long pre_tm = millis();
unsigned int freq_tm_diff = 0; // how much time has passed since last frequency reading
// -- note the int size limit
unsigned long camera_tm_diff = 0; // how much time has passed since last exposure
// this is used by the sensitivity adjustment
// to record the amount to divide the freq by
// to get the uW/cm2 value at 420nm
unsigned int calc_sensitivity = 10;
// this is the frequency calculation multiplier
// based on what scaling factor is chosen.
// set_scaling() will adjust it for you.
byte freq_mult = 1;
// our previous frequency count
// -- we set it to fifty so it doesn't swing
// auto-sensitivity adjustment either way on the first
// reading
unsigned long last_cnt = 50;
// some EV tracking variables
float prev_ev = 0;
float set_ev = 0;
// our wavelengths (nm) we're willing to calculate illuminance for (lambda)
int wavelengths[18] = { 380, 400, 420, 440, 460, 480, 500, 520, 540, 560, 580, 600, 620, 640, 660, 680, 700, 720 };
// the CIE V(l) for photopic vision - CIE Vm(l) 1978 - mapping to the same (l) above
float v_lambda[18] = { 0.0002, 0.0028, 0.0175, 0.0379, 0.06, 0.13902, 0.323, 0.71, 0.954, 0.995, 0.87, 0.631, 0.381, 0.175, 0.061, 0.017, 0.004102, 0.001047 };
// CIE SPD graphs for D65 and Illuminant A light sources, again mapping to same lambda as included in wavelengths
float spd_graphs[2][18] = {
{ 49.975500, 82.754900, 93.431800, 104.865000, 117.812000, 115.923000, 109.354000, 104.790000, 104.405000, 100.000000, 95.788000, 90.006200, 87.698700, 83.699200, 80.214600, 78.284200, 71.609100, 61.604000 },
{ 9.795100, 14.708000, 20.995000, 28.702700, 37.812100, 48.242300, 59.861100, 72.495900, 85.947000, 100.000000, 114.436000, 129.043000, 143.618000, 157.979000, 171.963000, 185.429000, 198.261000, 210.365000 }
};
/*
Let's setup all of our display strings for the LCD
These are stored as PROGMEM so they don't take up valuable
SRAM
*/
// main display strings
// setup strings
const prog_char setup_str1[] PROGMEM = "ISO";
const prog_char setup_str2[] PROGMEM = "Target F/Stop";
const prog_char setup_str3[] PROGMEM = "EV Adjust";
const prog_char setup_str4[] PROGMEM = "EV Steps";
const prog_char setup_str5[] PROGMEM = "Max Exp. - Sec";
const prog_char setup_str6[] PROGMEM = "Exposure Cycle";
const prog_char setup_str7[] PROGMEM = "Latch";
const prog_char setup_str8[] PROGMEM = "Min Exp. - mSec";
const prog_char setup_str9[] PROGMEM = "Scale Read";
const prog_char setup_str10[] PROGMEM = "TTL F/Stop";
const prog_char setup_str11[] PROGMEM = "Light Source";
const prog_char setup_str12[] PROGMEM = "EV Change Max";
PGM_P setup_strings[MAX_SETUP_STEPS] PROGMEM =
{
setup_str1,
setup_str2,
setup_str3,
setup_str4,
setup_str6,
setup_str7,
setup_str8,
setup_str5,
setup_str9,
setup_str10,
setup_str11,
setup_str12
};
// buffer for above setup strings
char lcd_print_buffer[SIZE_OF_LCD_BUF];
// ev adjust can be negative
//
// how many EV steps to add or subtract
// to each EV calculation - UI moves in 1/4EV
// increments.
float ev_adjust = 0.0;
// how many steps per EV to deal with -
// calculations round to the nearest step
// and adjustments happen in these steps,
// e.g.: 10 steps = .1 EV per step,
// 1 step = 1 EV per step
byte ev_steps = 100;
// status flags:
// B0 = intervalometer enable
// B1 = latch up
// B2 = latch down
// B3 = in setup
// B4 = latched (exp tm not moved due to latch)
// B5 = latched to min/max exp tm setting
// B6 = ev adjust enabled
// B7 = update setup display
byte status = B00000000;
// current setup step
byte setup_step = 0;
// display type flags
// 0 = 1/x shutter speed/time (calculated)
// 1 = ms shutter speed (calculated)
// 2 = frequency
// 3 = EV (calculated)
// 4 = W/m2 (calculated)
// 5 = sensitivity (10, 100, 1000)
// 6 = lux (calculated)
// 7 = scale
byte disp_enable = B10000000;
// button hit flags (plus a few others)
// B0 = up
// B1 = dn
// B2 = lt
// B3 = rt
// B4 = ct
// B5
// B6 =
// B7 = camera exposing (use this to save a var elsewhere)
byte buttons = B00000000;
// time last button was hit
unsigned long button_hit = 0;
// init lcd display
LiquidCrystal lcd(14, 14, 15, 16, 17, 18, 19);
void setup() {
lcd.clear();
lcd.print("LightRails/1.0");
// attach interrupt to pin8, send output pin of TSL230R to arduino 8
// call handler on each rising pulse
attachInterrupt(0, add_pulse, RISING);
pinMode(TSL_FREQ_PIN, INPUT);
pinMode(TSL_S0, OUTPUT);
pinMode(TSL_S1, OUTPUT);
pinMode(TSL_S2, OUTPUT);
pinMode(TSL_S3, OUTPUT);
pinMode(CAMERA_PIN, OUTPUT);
// set input button pins
pinMode(B_LT, INPUT);
pinMode(B_RT, INPUT);
pinMode(B_UP, INPUT);
pinMode(B_DN, INPUT);
pinMode(B_CT, INPUT);
// enable pullup resistors
digitalWrite(B_LT, HIGH);
digitalWrite(B_RT, HIGH);
digitalWrite(B_UP, HIGH);
digitalWrite(B_DN, HIGH);
digitalWrite(B_CT, HIGH);
// write TSL sensitivity 1x
digitalWrite(TSL_S0, HIGH);
digitalWrite(TSL_S1, LOW);
// set frequency scaling
tsl_set_scaling(2);
}
void loop() {
// operate camera
// handle user input
// update light sensor data
// calculate how much time has passed
// both for camera firing and light reading
// this code requires arduino 012 or any version with
// compatible millis() overflow and behavior
freq_tm_diff += millis() - pre_tm;
camera_tm_diff += millis() - pre_tm;
pre_tm = millis();
// if intervalometer enabled, enough time has passed,
// and the camera isn't already engaged
// - fire camera (camera delay is in seconds)
if( status & B10000000 && camera_tm_diff >= real_camera_delay && camera_engaged == false ) {
// re-set camera difference timer
camera_tm_diff = 0;
// trigger camera remote
fire_camera();
// convert sec to msec for next exposure check
// always try and use the configured value first
// (see next operation)
real_camera_delay = camera_delay * 1000;
// assure MIN_EXP_GAP threshold is enforced
// between exposure triggers
//
// if the exposure time leaves less than the
// minimum gap between exposures (MIN_EXP_GAP
// must be <= real_camera_delay), add the minimum gap
// to the current exposure time, and make that the
// interval time before the next shot
if( cur_exp_tm >= ( real_camera_delay - MIN_EXP_GAP ) )
real_camera_delay = cur_exp_tm + MIN_EXP_GAP;
// update count of shots fired
shots_fired++;
} // end if( status...
// see if the user has pressed any buttons, and handle
// them if need be
check_input();
// if a second has passed, we need to
// update our calculations
if( freq_tm_diff >= READ_TM ) {
// reset time counter
freq_tm_diff = 0;
// get current frequency
frequency = tsl_get_freq();
// ignore changes in frequency below FREQ_CHG_THRESH
if( last_cnt > 0 && abs( (long) ( frequency - last_cnt ) ) < FREQ_CHG_THRESH )
frequency = last_cnt;
// set other needed values
if ( frequency > 0 ) {
// chip has given us a positive reading
// calculate power
uwcm2 = tsl_calc_uwatt_cm2(frequency);
if( uwcm2 <= 0 ) {
// handle no actual power reading available
uwcm2 = 0;
lux = 0;
}
else {
// there was a positive power reading...
// calculate lx value using gaussian formula
lux = calc_lux_gauss(uwcm2);
}
}
else {
// 0 frequency read from chip - set all values to zero
uwcm2 = 0;
lux = 0;
}
if ( lux <= 0 ) {
// don't try calculating nonsense - we cant calculate
// an EV with no light.
lux = 0;
ev = -6.0;
}
else {
// we have positive lux reading
// calculate EV from lux reading
ev = calc_ev(lux);
}
// calculate exposure time value
exp_tm = calc_exp_tm( ev, f_stop );
// determine if we need to latch on to a particular exposure
// as lowest or highest allowed
if( status & B10000000 && status & B01100000) {
// latch high or low enabled - and intervalometer on
if( status & B01000000 ) {
// latch low is enabled (don't increase exposure time)
// remember that exp_tm is a divisor - so a higher number == faster exposure
if( exp_tm > pre_exp_tm) {
exp_tm = pre_exp_tm;
// indicate that exposure is latched
status |= B00001000;
}
else {
// reset latch indicator
status &= B11110111;
// set new exposure value to keep exp from
// getting longer than current exp. (new
// ceiling)
pre_exp_tm = exp_tm;
}
}
else if( status & B00100000 ) {
// latch high enabled (don't decrease exposure time)
if( exp_tm < pre_exp_tm ) {
exp_tm = pre_exp_tm;
status |= B00001000;
}
else {
status &= B11110111;
// new floor
pre_exp_tm = exp_tm;
}
}
} // end if latch high or low enabled
// calculate exposure ms
// we can only go as short as 1ms, so don't
// try and calculate below that. Check for
// 1/1000 ceiling
if( exp_tm >= 1000 ) {
cur_exp_tm = 1;
}
else {
cur_exp_tm = calc_exp_ms( exp_tm );
}
// check for minimum/max exposure time
if( cur_exp_tm < min_exp_tm ) {
// bring time up to min. exposure time
cur_exp_tm = min_exp_tm;
// set exposure time ceiling/floor engaged
status |= B00000100;
}
// max exposure time is in seconds
else if( max_exp_tm > 0 && cur_exp_tm > ( max_exp_tm * 1000 ) ) {
cur_exp_tm = max_exp_tm * 1000;
// set exposure time ceiling/floor engaged
status |= B00000100;
}
else {
// at neither min nor max exp. time
// reset exposure time ceiling/floor flag
status &= B11111011;
}
// determine if we need to change sensitivity --
// two readings in a row must pass our thresholds
if( frequency < SENS_THRESH_LO && last_cnt < SENS_THRESH_LO && calc_sensitivity < 1000 ) {
tsl_sensitivity( HIGH );
}
else if( frequency > SENS_THRESH_HI && last_cnt > SENS_THRESH_HI && calc_sensitivity > 10) {
tsl_sensitivity( LOW );
}
// save off current reading so we can see if we need to adjust sensitivity
// on the next pass
last_cnt = frequency;
// set display to update
status |= B00000001;
} // end if(freq_tm_diff > READ_TM)
//
// update user interface as the last step in the main
// loop...
//
if( status & B00010000 && status & B00000001 ) {
// in setup and screen needs updating
// clear update flag
status &= B11111110;
// display setup info
print_setup_display();
}
else if( status & B00000001 ) {
// on main screen and need to update display
// clear update flag
status &= B11111110;
print_info();
}
} // end main loop
// interrupt handler to function as a pulse-counter
void add_pulse() {
// increase pulse count
pulse_cnt++;
return;
}
float float_abs_diff( float f1, float f2 ) {
float diff = f1 > f2 ? f1 - f2 : f2 - f1;
if( diff < 0 )
diff *= 1;
return(diff);
}
tsl230r_functions.pde:
/*
LightRails - TSL230R Control and Conversion Functions
Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com
unsigned long tsl_get_freq()
float tsl_calc_uwatt_cm2(unsigned long freq)
void tsl_sensitivity( bool dir )
void tsl_set_scaling( int what )
This program is free software: you can redistribute it
and/or modify it under the terms of the GNU General
Public License as published by the Free Software
Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License
for more details.
*/
unsigned long tsl_get_freq() {
// we have to scale out the frequency - the only
// 1:1 frequency pulse we get is 100x scale. Smaller
// scaling on the TSL230R requires us to multiply by a factor
// to get actual frequency
unsigned long freq = pulse_cnt * freq_mult;
pulse_cnt = 0;
return(freq);
}
float tsl_calc_uwatt_cm2(unsigned long freq) {
// get uW observed - assume 640nm wavelength
// calc_sensitivity is our divide-by to map to a given signal strength
// for a given sensitivity (each level of greater sensitivity reduces the signal
// (uW) by a factor of 10)
//
// note: this function does not take sensor size into account
// other functions handle this
float uw_cm2 = (float) freq / (float) calc_sensitivity;
return(uw_cm2);
}
void tsl_sensitivity( bool dir ) {
// adjust sensitivity of TSL230R in 3 steps of 10x either direction
int pin_0 = false;
int pin_1 = false;
if( dir == true ) {
// increasing sensitivity
// -- already as high as we can get
if( calc_sensitivity == 1000 )
return;
if( calc_sensitivity == 100 ) {
// move up to max sensitivity
pin_0 = true;
pin_1 = true;
}
else {
// move up to med. sesitivity
pin_1 = true;
}
// increase sensitivity divider
calc_sensitivity *= 10;
}
else {
// reducing sensitivity
// already at lowest setting
if( calc_sensitivity == 10 )
return;
if( calc_sensitivity == 100 ) {
// move to lowest setting
pin_0 = true;
}
else {
// move to medium sensitivity
pin_1 = true;
}
// reduce sensitivity divider
calc_sensitivity = calc_sensitivity / 10;
}
// make any necessary changes to pin states
digitalWrite(TSL_S0, pin_0);
digitalWrite(TSL_S1, pin_1);
return;
}
void tsl_set_scaling ( int what ) {
// set output frequency scaling for TSL230R
// when increasing the scaling for divide-by-output, you reduce the
// freq multiplier by a factor of ten.
// e.g.:
// scale = 2 == freq_mult = 100
// scale = 10 == freq_mult = 10
// scale = 100 == freq_mult = 1
byte pin_2 = HIGH;
byte pin_3 = HIGH;
switch( what ) {
case 2:
pin_3 = LOW;
freq_mult = 2;
break;
case 10:
pin_2 = LOW;
freq_mult = 10;
break;
case 100:
freq_mult = 100;
break;
default:
return;
}
digitalWrite(TSL_S2, pin_2);
digitalWrite(TSL_S3, pin_3);
return;
}
camera_controls.pde:
/*
-- Camera Control Functions
LightRails 1.0
A dynamic external exposure control system with
integrated intervalometer for time-lapse
photography
Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com
This program is free software: you can redistribute it
and/or modify it under the terms of the GNU General
Public License as published by the Free Software
Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License
for more details.
*/
void fire_camera() {
digitalWrite(CAMERA_PIN, HIGH);
// start timer to stop camera exposure
MsTimer2::set(cur_exp_tm, stop_camera);
MsTimer2::start();
// update camera currently enaged
camera_engaged = true;
return;
}
void stop_camera() {
digitalWrite(CAMERA_PIN, LOW);
// turn off timer
MsTimer2::stop();
// update camera currently enaged
camera_engaged = false;
}
lcd_display.pde:
/*
-- LCD Display Functions
LightRails 1.0
A dynamic external exposure control system with
integrated intervalometer for time-lapse
photography
Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com
This program is free software: you can redistribute it
and/or modify it under the terms of the GNU General
Public License as published by the Free Software
Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License
for more details.
*/
void print_setup_display() {
// clear out lcd print buffer
memset(lcd_print_buffer, 0, SIZE_OF_LCD_BUF);
// get setup string from hash
strcpy_P(lcd_print_buffer, (char*)pgm_read_word( &(setup_strings[setup_step])) );
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(lcd_print_buffer);
// go to second line
lcd.setCursor(0, 1);
// determine which setup step to display
switch(setup_step) {
case 0:
lcd.print(iso_rating, DEC);
break;
case 1:
// handle rounding for higher f-stops
if( f_stop > 7.1 ) {
print_float((float) int(f_stop));
}
else {
print_float(f_stop);
}
break;
case 2:
print_float(ev_adjust);
break;
case 3:
lcd.print(ev_steps, DEC);
break;
case 4:
lcd.print(camera_delay, DEC);
break;
case 5:
if( status & B01000000 ) {
lcd.print('-');
}
else if( status & B00100000 ) {
lcd.print('+');
}
else {
lcd.print('X');
}
break;
case 6:
lcd.print(min_exp_tm, DEC);
break;
case 7:
lcd.print(max_exp_tm, DEC);
break;
case 8:
lcd.print(freq_mult, DEC);
break;
case 9:
print_float(ttl_stop);
break;
case 10:
lcd.print(light_type, DEC);
break;
case 11:
lcd.print(ev_diff_ceiling, DEC);
break;
default:
break;
}
return;
}
void print_info() {
lcd.clear();
lcd.setCursor(0, 0);
// display set iso and f-stop
lcd.print(iso_rating);
lcd.print(' ');
lcd.print('f');
// handle rounding for higher f-stops
if( f_stop > 7.1 ) {
print_float((float) int(f_stop));
}
else {
print_float(f_stop);
}
// if intervalometer on, show 'I' indicator
// and count of shots fired
if( status & B10000000 ) {
lcd.setCursor(15,0);
lcd.print('I');
if( shots_fired > 999 ) {
lcd.setCursor(11, 0);
}
else if( shots_fired > 99 ) {
lcd.setCursor(12,0);
}
else if( shots_fired > 9 ) {
lcd.setCursor(13,0);
}
else {
lcd.setCursor(14,0);
}
lcd.print(shots_fired, DEC);
}
// move to second line
lcd.setCursor(0,1);
if( disp_enable & B10000000 ) {
// show exposure time in 1/x notation
if( exp_tm >= 2 ) {
float disp_v = 0;
if( exp_tm >= (float) int(exp_tm) + (float) 0.5 ) {
disp_v = int(exp_tm) + 1;
}
else {
disp_v = int(exp_tm);
}
lcd.print("1/");
lcd.print(exp_tm, DEC);
}
else if( exp_tm >= 1 ) {
// deal with times larger than 1/2 second
float disp_v = 1 / exp_tm;
// get first significant digit
disp_v = int( disp_v * 10 );
lcd.print('.');
lcd.print(disp_v, DEC);
lcd.print('"');
}
else {
int disp_v = int( (float) 1 / exp_tm);
lcd.print(disp_v, DEC);
lcd.print('"');
}
}
else if( disp_enable & B01000000 ) {
// display actual exposure mS
print_ul(cur_exp_tm);
lcd.print("mS");
}
else if( disp_enable & B00100000 ) {
// display frequency reading
lcd.print("Fq ");
lcd.print(frequency, DEC);
}
else if( disp_enable & B00010000 ) {
// display EV
lcd.print("EV ");
print_float(ev);
if( ev_adjust > 0 || ev_adjust < 0 ) {
// show any adjustment made
lcd.print('[');
print_float(ev_adjust);
lcd.print(']');
}
}
else if( disp_enable & B00001000 ) {
// uW/m2
print_float(uwcm2);
lcd.print("uW");
}
else if( disp_enable & B00000100 ) {
// sensitivity level
lcd.print('S');
lcd.print(calc_sensitivity, DEC);
}
else if( disp_enable & B00000010 ) {
// display lux
lcd.print("Lx ");
print_float(lux);
}
else if( disp_enable & B00000001 ) {
// divide-by-factor display
lcd.print('R');
lcd.print(freq_mult, DEC);
}
lcd.setCursor(14,1);
// minimum/maximum exposure time enabled?
if( status & B00000100 )
lcd.print('M');
// exposure was latched?
if( status & B00001000 )
lcd.print('L');
}
void print_float( float val ){
if ( val < 0 ) {
lcd.print('-');
val *= -1;
}
print_ul( (unsigned long int) int(val) );
lcd.print('.');
// add 1 digit for hundreths precision
if( (val * 100) - (int(val) * 100) < 10 )
lcd.print('0');
print_ul( (val * 100) - (int(val) * 100) );
}
void print_ul( unsigned long val ) {
// print unsigned long to lcd
// clear out lcd print buffer
memset(lcd_print_buffer, 0, SIZE_OF_LCD_BUF);
ultoa(val, lcd_print_buffer, 10);
lcd.print(lcd_print_buffer);
return;
}
photo_calculations.pde:
/*
LightRails 1.0
A dynamic external exposure control system with
integrated intervalometer for time-lapse
photography
Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com
This program is free software: you can redistribute it
and/or modify it under the terms of the GNU General
Public License as published by the Free Software
Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License
for more details.
*/
/*
Functions to calculate photographic values
from light readings, the following functions are
found here:
calc_lux_single*
calc_lux_gauss
calc_ev
calc_exp_tm
calc_exp_ms
Last Modified: 2/29/2009, c.a. church
Original: 10/15/2008, c.a. church
*/
/*
We always use multiple wavelengths. This function is
if you happen to be erm, you know, filming something illuminated
by a single wavelength light source. Given the rarity of this
situation, you'll have to provide the luminous efficiency function
value yourself.
float calc_lux_single(float uw_cm2, float efficiency) {
// calculate lux (lm/m^2) for single wavelength, using standard formula:
// Xv = Xl * V(l) * Km
// Xl is W/m^2 (calculate actual receied uW/cm^2, extrapolate from sensor size (0.0136cm^2)
// to whole cm size, then convert uW to W)
// V(l) = efficiency function (provided via argument)
// Km = constant, L/W @ 555nm = 683 (555nm has efficiency function of nearly 1.0)
//
// Only a single wavelength is calculated - you'd better make sure that your
// source is of a single wavelength... Otherwise, you should be using
// calc_lux_gauss() for multiple wavelengths
return( ( ( uw_cm2 * ( (float) 1 / (float) 0.0136) ) / (float) 1000000 ) * (float) 100 * efficiency * (float) 683 );
}
*/
float calc_lux_gauss( float uw_cm2 ) {
// # of wavelengths mapped to V(l) values - better have
// enough V(l) values!
int nm_cnt = sizeof(wavelengths) / sizeof(int);
// watts/m2
float w_m2 = ( uw_cm2 * ( (float) 1 / (float) 0.0136 ) / (float) 1000000 ) * (float) 100;
float result = 0;
// integrate XlV(l) dl
// Xl = uW-m2-nm caclulation weighted by the CIE lookup for the given light
// temp
// V(l) = standard luminous efficiency function
for( int i = 0; i < nm_cnt; i++) {
if( i > 0) {
result += ( spd_graphs[light_type][i] / (float) 1000000) * (wavelengths[i] - wavelengths[i - 1]) * w_m2 * v_lambda[i];
}
else {
result += ( spd_graphs[light_type][i] / (float) 1000000) * wavelengths[i] * w_m2 * v_lambda[i];
}
}
// multiply by constant Km and return
return(result * (float) 683);
}
float calc_ev( float lux ) {
// calculate EV using APEX method:
// Ev = Av + Tv = Bv + Sv
// Bv = log2( B/NK )
// Sv = log2( NSx )
// K = Meter Calibration Constant
// 14 = Pentax standard
// N = constant relationship between Sx and Sv
float ev = ( log( (float) 0.3 * (float) iso_rating ) / log(2) ) + ( log( lux / ( (float) 0.3 * (float) 14.0 ) ) / log(2) );
// round down if EV step
// value is set to whole steps
if( ev_steps == 1 )
ev = (float) int(ev);
// convert to positive value temporarily, if needed
bool neg_ev = false;
if( ev < 0 ) {
neg_ev = true;
ev *= -1;
}
if( ev > int(ev) ) {
// if ev has a decimal value, determine nearest
// fraction of EV to round to, based on ev step
// setting
// handle rounding to nearest step
int rem = ( ( ev - int(ev) ) * 100 );
int step = 100 / ev_steps;
for (int i = ev_steps; i > 0; i-- ) {
if( rem >= step * i) {
rem = step * i;
break;
}
}
ev = (float) int(ev) + ((float) rem / 100);
}
// reset back to negative EV value
if( neg_ev == true )
ev *= -1.0;
// is there a ceiling set for maximum EV change?
// if so, handle only moving a maximum amount of <ceiling>
// steps between readings
if( ev_diff_ceiling > 0 ) {
float diff = float_abs_diff( ev, prev_ev );
float ceiling = (float) ev_diff_ceiling / (float) ev_steps;
if( diff > ceiling ) {
if( ev < prev_ev ) {
ev = prev_ev - ceiling;
}
else {
ev = prev_ev + ceiling;
}
}
}
prev_ev = ev;
// if ev adjust enabled - apply it now
ev += ev_adjust;
// deal with TTL compensation
// each full aperture step results in one less
// EV read in, so adjust output
// up by one EV per stop
if( ttl_stop > 0 )
ev += log( ttl_stop ) / log(sqrt(2));
return(ev);
}
float calc_exp_tm ( float ev, float aperture ) {
// Ev = Av + Tv = Bv + Sv
// need to determine Tv value, so Ev - Av = Tv
// Av = log2(Aperture^2)
// Tv = log2( 1/T ) = log2(T) = 2 ^^ (Ev - Av)
float exp_tm = ev - ( log( pow(aperture, 2) ) / log(2) );
float exp_log = pow(2, exp_tm);
return( exp_log );
}
unsigned long calc_exp_ms( float exp_tm ) {
return( (unsigned long) ( 1000 / exp_tm ) );
// if you wish to round actual mS for exposure to nearest
// exposure step (the actual exposure step displayed in 1/x
// format), un-comment the following code and comment the
// line above out.
//
// Mind you, that doing so will make the device
// no more accurate than your standard camera's meter.
/*
// deal with times less than or equal to half a second
if( exp_tm >= 2 ) {
if( exp_tm >= (float) int(exp_tm) + (float) 0.5 ) {
exp_tm = int(exp_tm) + 1;
}
else {
exp_tm = int(exp_tm);
}
return(1000 / exp_tm);
}
else if( exp_tm >= 1 ) {
// deal with times larger than 1/2 second
float disp_v = 1 / exp_tm;
// get first significant digit
disp_v = int( disp_v * 10 );
return( ( 1000 * disp_v ) / 10 );
}
else {
// times larger
int disp_v = int( (float) 1 / exp_tm);
return((unsigned long) 1000 * (unsigned long) disp_v);
}
*/
}
user_input.pde:
/*
-- user input functions
LightRails 1.0
A dynamic external exposure control system with
integrated intervalometer for time-lapse
photography
Copyright (c) 2008-2009 C. A. Church drone< a_t >dronecolony.com
This program is free software: you can redistribute it
and/or modify it under the terms of the GNU General
Public License as published by the Free Software
Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License
for more details.
*/
void check_input() {
// this function determines if any button has been hit,
// and what to do about it
// up and down do not require a HIGH (unpressed) reading
// so that you can hold them down and quickly cycle through
// values (about 3 changes per second)
if( digitalRead(B_UP) == LOW && millis() - button_hit > 300) {
button_hit = millis();
take_button_action(0);
return;
}
if( digitalRead(B_DN) == LOW && millis() - button_hit > 300 ) {
//pre_dn = true;
take_button_action(1);
button_hit = millis();
return;
}
if( digitalRead(B_LT) == LOW) {
if(buttons & B00100000 && millis() - button_hit > 300) {
buttons &= B11011111;
take_button_action(2);
button_hit = millis();
return;
}
buttons &= B11011111;
}
else {
buttons |= B00100000;
}
if( digitalRead(B_RT) == LOW ) {
if( buttons & B00010000 && millis() - button_hit > 300) {
buttons &= B11101111;
take_button_action(3);
button_hit = millis();
return;
}
buttons &= B11101111;
}
else {
buttons |= B00010000;
}
if( digitalRead(B_CT) == LOW ) {
if( buttons & B00001000 && millis() - button_hit > 300) {
buttons &= B11110111;
take_button_action(4);
button_hit = millis();
return;
}
buttons &= B11110111;
}
else {
buttons |= B00001000;
}
}
void take_button_action( byte button ) {
// this function operates whatever action is desired when
// a button is pressed. called by check_input()
switch(button) {
case 0:
// up hit
if( status & B00010000 ) {
// we're in a setup screen...
// increasing value at current cursor position
change_setup_value(setup_step, 1);
// update flag to show change in display value
status |= B00000001;
return;
}
else {
// on main screen - up turns intervalometer on
// if intervalometer on already, do nothing
if( status & B10000000 )
return;
// turn on intervalometer
status |= B10000000;
// save current exposure reading for latch
pre_exp_tm = exp_tm;
}
break;
case 1:
// down hit
if( status & B00010000 ) {
// we're in a setup screen
// decreasing value at current cursor position
change_setup_value(setup_step, -1);
status |= B00000001;
return;
}
else {
// main menu - turn off intervalometer
// re-set latch matched (if set)
status &= B01110111;
shots_fired = 0;
}
break;
case 2:
// left button was hit
// do nothing if in setup menu
if( status & B00010000 )
return;
// main display, cycle through display values
if( disp_enable & B10000000 ) {
// we were already left, move to the furthest right
// value
disp_enable = B00000001;
// set screen to update
status |= B00000001;
}
else {
// shift current display value left
disp_enable = disp_enable << 1;
status |= B00000001;
}
break;
case 3:
// right button was hit
// do nothing if in setup menu
if( status & B00010000 )
return;
// main display, cycle through display values
if( disp_enable & B00000001 ) {
// we were already at furthest right value,
// need to move to furthest left value
disp_enable = B10000000;
// set screen to update
status |= B00000001;
}
else {
// shift current display right
disp_enable = disp_enable >> 1;
// set screen to update
status |= B00000001;
}
break;
case 4:
// center button hit
if( status & B00010000 ) {
// in setup mode - we're going to change which
// setup value we're displaying
// set display to update
status |= B00000001;
if( setup_step == MAX_SETUP_STEPS - 1 ) {
// no more steps to go, exit menu
setup_step = 0;
// set that we're no longer in setup
status &= B11101111;
return;
}
setup_step++;
return;
}
else {
// not in setup or input -
// go into setup mode, tell display
// to refresh
status |= B00010001;
return;
}
break;
}
}
void change_setup_value( byte which, int what ) {
switch( which ) {
case 0:
{
// change iso value, convert to logarithmic
// scale first, then add or subtract one step
// and convert back to arithmetic scale
float foo = log(iso_rating) / log(10);
int din = ( 10 * foo ) + 1;
din += what;
if( din < 1 )
din = 1;
float bar = pow(10, (float) ( (float) din - 1) / (float) 10 );
if( bar > int(bar) ) {
iso_rating = int(bar) + 1;
}
else {
iso_rating = int(bar);
}
}
break;
case 1:
{
// move f_stop value - move one third step
// either direction
float steps = log( f_stop ) / log( sqrt(2) );
if( what > 0 ) {
steps += 0.3333;
}
else {
steps -= 0.3333;
}
if( steps < 0 )
steps = 0;
f_stop = pow( sqrt(2), steps );
}
break;
case 2:
// ev adjust moes in 1/4 EV increments
ev_adjust += (float) what * 0.25;
break;
case 3:
// ev steps determines how many divisions of an EV
// there are when calculating automatic exposure
// values. more steps = better granularity in exposure
// timing.
ev_steps += what;
if( ev_steps < 0 )
ev_steps = 0;
break;
case 4:
camera_delay += what;
if( camera_delay < 1 )
camera_delay = 1;
// ms delay time
real_camera_delay = camera_delay * 1000;
break;
case 5:
// cycle between latch settings
if( status & B01000000 ) {
status &= B10111111;
status |= B00100000;
}
else if( status & B00100000 ) {
status &= B10011111;
}
else {
status |= B01000000;
}
break;
case 6:
min_exp_tm += what;
// don't allow min time to be less than 0
if( min_exp_tm <= 0 )
min_exp_tm = 0;
break;
case 7:
max_exp_tm += what;
// don't allow max time to be less than 0
if( max_exp_tm <= 0 )
max_exp_tm = 0;
break;
case 8:
// change scaling:
// scaling value can be 2, 10, or 100
// allow wrap around from highest to lowest
// and vice-versa
freq_mult += what;
// we only get to 11 by going up from 10 and
// we only 1 by going down from 2 - next step
// either way is 100
if( freq_mult == 11 || freq_mult == 1 ) {
tsl_set_scaling(100);
}
else if( freq_mult == 9 || freq_mult == 101 ) {
// if we're moving down from 10, or up from 100
// move to lowest setting
tsl_set_scaling(2);
}
else {
// if neither of the above cases is true, then our only
// destination is 10
tsl_set_scaling(10);
}
break;
case 9:
{
// move ttl_stop value - move one third step
// either direction
float steps = log( ttl_stop ) / log(sqrt(2));
if( steps < 0 )
steps = 0;
if( what > 0 ) {
steps += 0.3333;
}
else {
steps -= 0.3333;
}
// for ttl stop, we want to get to zero, if possible
// for non-ttl readings. We go ahead and skip every stop below
// 1.0 for the heck of it. (Very few lenses go below 1.0)
if( steps < 0 ) {
ttl_stop = 0.0;
}
else {
ttl_stop = pow( sqrt(2), steps );
// handle rounding for higher f-stops
if( ttl_stop > 7.1 )
ttl_stop = int(ttl_stop);
}
}
break;
case 10:
light_type = light_type == 1 ? 0 : 1;
break;
case 11:
ev_diff_ceiling += what;
break;
default:
break;
}
return;
}

Leave a Reply