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); } [/sourcecode] <em>tsl230r_functions.pde:</em> /* 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; } [/sourcecode] <em>photo_calculations.pde:</em> /* 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; }
I’m looking to start this for my 11×14 pinhole, from what I gather this is designed to take photos at intervals, this should easily be removed to take one photo and adjust the exposure time throughout the shot, right?
Also do you have any software/hardware refinements since this was posted?
William Rea said this on January 15, 2010 at 1:15 am |
Hi,
great thing.
I am new to this one.It will be useful for my DIY time lapse projects, but it´s interesting to know how to wire all this switches and the LCD to arduino. If it´s possible to post some kind of wiring diagram or something similar? I look whit interest your project for time lapse, but can´t figure out how all of this is connected in one machine to work fine, like I see in your videos. If are any other links to your project or similar to see all in one piece?
Regards
Deyan
Deyan said this on February 15, 2010 at 8:46 am |
Deyan,
See this tutorial on how to hook up the LCD:
http://www.arduino.cc/en/Tutorial/LiquidCrystal
Line 279 in the main PDE (in this post) shows how I had my LCD wired up (I just used different pins). The buttons and camera pin assignments are defined on lines 34-42, and the TSL230R pins are right above that. The first tutorial (Arduino and the TSL230R) talks about how to wire up the TSL230R.
Note that all of the buttons connect to GND when pressed.
!c
c.a. church said this on February 15, 2010 at 10:57 am |
Thanks for the post!
Other things to know if it´s possible. Do you use 2 different arduino,one for the LightRails and one for the engine whit the motors?
As I understand, one is controlling the metering and triggering the camera and the other one the movements of the rig. They are programed separately to perform there tasks.I live in Spain and if I can´t find the TSL230R in local store, is possible to replace it with other model or this will cause different light metering? I sou one wiring diagram for stepper motors in other post, and if I understand right, this one is for the engine that dives the rig and needs the isye drivers or other ones hooked up to the motors to perform the movements.
It will be interesting to program all this from PDA in the future. So will be much is ye to move the rig from one location to other, whiteout need of laptop or programing it at home.
Thank´s one more time!
Deyan said this on February 16, 2010 at 8:21 am |
I love the project but I think the premise of 1/100EV may be a little over the top. DSLRs do only meter in 1/2EV increments but they can also only expose in 1/2EV aperture/shutter speed increments. Also the general rule is accuracy greater than 1/2EV isn’t necessary. That doesn’t mean that two images 1/4EV apart are going to look identical. What it means is that given two camera RAW files that are 1/4EV apart you should be able to post-process them to look pretty much exactly identical. Of course this rule came from the film world where a negative has almost twice the dynamic range of digital sensors..
Eric D. said this on April 13, 2010 at 5:47 pm |
Hi Eric!
In practice, the actual change is coarser than 1/100 EV as the reality is that the light levels do not follow a nice, linear curve as day goes to night and vice-versa. The underlying issue is that most people are shooting JPG’s for timelapse, and want to avoid the flicker that the 1/4 or 1/2EV changes result in. This is a real problem in timelapse video that people are facing each day. If it weren’t a problem, I would’ve never taken on the challenge to try and solve it *grin*
Of course, we could all shoot raw, and then use some software to provide the “right” change in exposure levels after leaving our camera on auto-mode. But it doesn’t seem to exist (not without a lot of manual intervention). The goal here was to provide a light meter that one can get more granular (auto) exposure adjustments out of than 1/4-1/2EV – which do fail to produce the desired results.
In practice, my testing revealed that most of the time you’re going to see realistic changes on the order of 1/10th to 1/20th EV. Mostly because of the weighting and EV change limits one applies to make it survivable in real-world situations. (One issue I had was brief changes in scene luminance would have disappeared between the metering and actual shooting, bringing flicker back.)
So yeah, 1/100EV is unnecessary in most cases, but *shrug* it’s there! And, by shooting in bulb, we do our own timing, so the only limit is the upper shutter speed (due to the fact that the analog inputs must be de-bounced, and cameras try to avoid responding to noisy remotes, etc.)
!c
c.a. church said this on April 13, 2010 at 6:09 pm |
[…] intervalometer has been built and based on code kindly provided “the roaming drone” and then modified . The main function of the intervalometer system is to allow automated […]
Arduino intervalometer and ramping for Digital Cameras | PhotosbyKev said this on May 20, 2010 at 1:34 pm |
Hi, great work inventing this awesome device!
Perhaps my question is a bit impudent, but:
Could you maybe build me another one and if yes: how much would it cost? 🙂
Best regards,
Daniel
Daniel said this on May 30, 2010 at 8:27 am |
Hi Daniel, sorry for the slow response! Currently, I am unable to make any of these for others as I am tied up with other projects, sorry!
!c
c.a. church said this on August 2, 2010 at 10:40 am |
In the process of building my own system based on your work and on the work of PhotosbyKev… Thanks for posting such an informative project description! It’s really given my experimentation a head-start…
I’m such a visual guy that I’m having a hard time envisioning the reflector you built for your light sensor and was wondering if you’d be able to post a picture to help me out?
Best Wishes,
~J
Jeff.L. said this on July 28, 2010 at 1:31 pm |
Hi Jeff,
I don’t have any pictures of the mount its self, and I’ve since recycled the lumber, but it was really quite simple: just an 8″ high “C” made of three pieces of 1×6″ wood, painted white on the inside. Then the sensor just mounted to the back piece (inside of the “C”) and then pointed towards the bottom.
!c
c.a. church said this on August 2, 2010 at 10:41 am |
Your work inspired me to get an arduino, go to my local electronics shop to buy all the required components, and use a soldering iron for the first time! Thanks so much for this. Having now built bulb-ramping intervalometer, I have a few questions. I have done a little experimentation and I am not yet pleased with my results. I hope with your experience you are able to give me a few pointers.
I have had a little bit of a problem with flicker. I presume that it is because of the way I have mounted the TSL230 chip and will be trying various alternatives.
I am using a Canon camera, and with shutter speeds below 1/2 seconds, it seems that exposure is not very accurate. Have you had any similar experience?
My last question is about the battery life of this arduino project. After running it for an hour, my backlight on the LCD no longer lights up (it slowly becomes dim over the course of an hour) and I can no longer read it. I really seem to be going through batteries. I am using a 9v battery like this: http://www.propersonaldefense.com/store1/36-77-thickbox/eveready-energizer-9-volt.jpg. Do you think this is normal?
I plan to write my experiences up and put them on my website. I’ll be sure to let you know and link through to your excellent work when I do.
Thanks in advance.
John
John said this on October 9, 2010 at 2:52 pm |
Hi John,
Sorry for the slow response! A 9V battery won’t be providing power for very long, I’d suggest a handful of AA batteries, or some C/D batteries. I usually use 12V SLA batteries (8+ Ah rating) in the field for all day power for my moco rigs.
The LightRails is definitely not perfected, and in fact, there are a few errors in the code (see the comments on the first two tutorials). I spent many weeks getting this far, and largely got tired of working on the same thing every day =)
If you see PhotoKev’s implementation, it is improved and likely performs much better.
!c
c.a. church said this on November 12, 2010 at 12:56 pm |