Mostrando entradas con la etiqueta gigapanoramico. Mostrar todas las entradas
Mostrando entradas con la etiqueta gigapanoramico. Mostrar todas las entradas

martes, 2 de julio de 2013

Gigapan Terrassa

Sábado 22 de junio, astronomicamente ha entrado el verano y parece que este año tendremos verano (días atrás nadie apostaba por ello), toca hacer la que en teoría será la última Gigapan por unos cuantos meses. Esto es debido a que para hacer una foto que abarque un gran espacio necesito sitios totalmente despejado (lo que significa sin sombra, y paso de incluir en mi kit panorámico una sombrilla de playa!) accentuado por las fuertes condiciones de luz y la necesidad de dedicar unas 3 horas sólo para realizar las tomas, acabo con la decisión Mediamarkt: Yo no soy tonto!

Hace mas de un año que un compañero de trabajo me sugirió realizar la Gigapan en su pueblo, e incluso me sugirió el sitio que no fue de mi agrado, pero con el paso del tiempo encontré este lugar gracias a la ayuda de las fotos que cuelga las fotos en Google earth. Aquí vi una posible localización con el que podía ser su resultado. Dias atrás examiné el terreno y me gustó.


Llegado el dia 22 de junio por la tarde, yo requería de unas dos horas según mis cálculos (finalmente fue 1h 51m realizando fotos, la preparación y desmontaje me supondría algo menos de otra hora adicional). Dada la posición del sol (siempre prefiero tenerlo en mis espaldas, así significa que tengo la luz incidiendo en el paisaje a fotografiar y las sombras proyectadas por los edificios no me oscurecen la foto final). Como que según mis cálculos tardo dos horas..pues decido que debo estar en el sitio X algo antes de las 6 de la tarde para asegurar que dos horas más tarde las condiciones de luz serán casi las mismas y que la influencia del sol no me provocará un catapsmo! 


Aparco el coche, y ando unos 500 metros hasta un extremo en un campo arado, la sombra más proxima a 100 metros y bajo 30ºC empiezo a montar el soporte en el filó de un terraplén de unos 7 metros en vertical, nivelo todo el conjunto mediante una aplicación en el teléfono móvil y cruzo los dientes para que no se desequilibre nada y acabe con mi robot + cámara nikon d5200 + Sigma 150-500 + teleconversor 1.4x en un viaje a la chatarra en el supuesto caso de un accidente a esa altura.


A continuación ajusto los parámetros fotograficos: RAW, f11, ISO 640 y focal sobre los 550 mm (equivalentes a 825 mm), todo en manual y bloqueando los anillos de zoom y de enfoque en el Sigma 150-500 para que no se mueva nada. Esta vez realicé un doble ajuste de enfoque: por el visor y por la pantalla,no fuera caso como en la GigapanMontserrat que tenia el ajuste de dioptrías desajustado y luego tuve que corregirlas con el Photoshop como pude para mejorar el desenfoque...

Una vez todo montado empieza el juego. Programo extremos en mi Arduino mediante la LCD shield y pulsamos sobre ENTER. Serian 2125 tomas repartidas en una cuadricula de 25 filas por 85 columnas. Parece que todo va bien, y esta vez no tengo ningún contratiempo. Como curiosidad, esta vez me acompañaban hormigas que se subían por el robot. 


Esta vez y después de mi previsión preferí realizar las tomas en RAW, bonita elección ...jeje! menos mal que las tarjetas SD son rápidas! Observo que llega un punto en que el led del buffer se queda fijo y se descarga cuando retrocede el robot. Esto significa que tardo casi lo mismo en grabar que en realizar una captura (poco menos de 3 segundos por foto).

Aprovecho para realizar fotos con el telefóno, twittear el evento y grabar un pequeño video:

Mientras realizo las tomas controla la temperatura del driver del motor, esta vez calculo que el ventilador de disipación  funcionó el 80% del tiempo bajo una tarde calurosa. Menos mal que no se rompieron los drivers (en la última versión del programa de Arduino incorporé varias controles y analisis de temperaturas del refrigerados para evitar posibles accidentes como el ocurrido en la GigapanSúria-2) .




Transcurridas las casi dos horas y tras aparecer en la pantalla la frase "Panorámica completa!" es momento de desmontarlo todo , no sin antes verificar en la pantalla de la D5200 algunas de las capturas tomadas... vuelta a casa que lo siguiente será procesar!


Aqui surgieron nuevos problemas... los RAWs de la d5200 no me los reconocía el iMAC ! Estuve intentando actualizar el CameraRAW de Photoshop (hasta ese día utilizaba el CS5) y la máxima versión que admitía el CS5 de CameraRAW se me quedaba aun que no los reconocía.

Hacia un par de días que leí algo sobre un bug en la Suite del  Photoshop CC y tras un par de horas, instalado y funcionando ! Ahora ya podía trabajar con los archivos.


Después de varios ajustes en el revelado y tras 6 horas y media ya tenia a punto los JPG resultantes a punto , ahora tocaba esperar a recibir  un disco duro externo nuevo, con esto de las Gigapan's , backups, time-machine me zampo los Terabites que es un contento! Una vez con el disco duro nuevo  ya sólo fué trabajo del software AutoPanoGiga que tras 15 horas de renderizado ya tenia el archivo .kro resultante. 


Y Finalmente ya sólo necesité 153633 segundos (unas 43 horas) para subir los 146 GB al servidor de Gigapan  

Y aqui teneis el resultado que espero que os guste. 


Para la próxima temporada ya tengo en vista otro gran proyecto... ahora a pasar el verano !


sábado, 8 de junio de 2013

Gigapan Montserrat (49.86 GPx)

Este proyecto ha sido uno de los que me ha hecho perder más tiempo buscando el sitio perfecto para fotografiarlo. Desde que me planteé fotografiar estas montañas rocosas tan singulares de Cataluña y que tan cerca tengo de casa, han sido muchas las visitas a varios puntos hasta decidirme por este último situado cerca de St. Cristófol  (recomiendo que para la ubicación exacta os fijéis en detalle en el mapa de geolocalización que hay en la propia web de Gigapan, junto a la foto). 

Todo empezó en verano del 2012 cuando empecé a buscar sitios, pero siempre había cosas negativas: imposibilidad de acceso en vehículo, necesidad de arrastrar todo el robot cuestas arriba (subiendo sin él ya era un buen sacrificio), arboles que me tapaban, etc...

Sabia que no podia hacerla en pleno verano por las altas temperaturas, prefería hacerlo entre primavera y principio de verano, entre otras cosas por dos factores:

* El color del paisaje , sin duda es siempre mejor la primavera que le da un toque más verde, mientras que en verano es más árido y seco, colores pastel que a mi no me llenan!
* La posición del sol : (aquí si que era más complicado) pues las montañas más bien están  situadas entre sureste y oeste, y como todos sabemos, el sol sale por el este, y a medida que pasan las horas va hacia el sur (mira que bién: con el sol de cara!) . Necesitaba hacer las capturas a primera hora de la mañana para evitar encontrarme que el sol me quemara las capturas. Otro factor importante es la inclinación del sol, cuanto más nos aproximamos a verano el sol está más vertical y eso me era positivo para reducir las sombras proyectadas por las propias montañas. Tenia que buscar un compromiso! Menos mal que mediante la aplicación SunSurveyor para Android y el ThePhotographerEphemerids para muchos sistemas operativos me han ayudado a todos estos planteamientos. 


Pasó el verano y otoño del 2012, re-diseñé el robot y ahora era mucho más ligero, pero aun no tenia el lugar perfecto. Fueron varias las excursiones visitando lugares en final de invierno y principios de primavera. Muchas ojeadas al mapa cartográfico y al Google earth , hasta que un dia me subí a una montaña y ya le daba mi visto bueno con algo de resignación. Seria una ascensión fuerte y me planteaba subir todo el material haciendo dos viajes hasta el posible aparcamiento.

Luego, una vez en casa , mirando distancias con el Google earth , y mediante la capa de fotografias de Panoramio descubrí que una persona había realizado una panorámica justo a pocos centenares de metros de dónde había aparcado, pues al dia siguiente, de nuevo , otra excursión para buscar este sitio: era perfecto !



Ya tenia el lugar, luego el dia: domingo 26 de mayo. Lo teníamos todo a punto. Emma había preparado unas magdalenas para matar el hambre mientras haríamos las fotos. Salimos pasadas las 8 de la mañana de Manresa, habia mala visión pero todo y eso recorrimos los 20 kms hasta llegar al lugar y cada vez estaba la montaña más tapada . Esperemos un rato, miremos la previsión meteorológica y pintaba que deberíamos esperarnos almenos unas horas para ganar nitidez, y muy resignadamente cancelemos la panorámica.


Probaríamos una semana más tarde... y entre semana, me puse a modificar el programa del robot. Desde el primer diseño que tenia un error con el solape de imágenes y mi cabeza me decía que debía buscar el error, no podía ser que "mágicamente" coincidiera el solape de imágenes.

Tenia un error de concepto: Cuando nos hablan de los grados de visión de un objetivo, se refieren al angulo que se provoca en la diagonal de la imagen, lo que "engloba" el angulo vertical y al horizontal. 

Después de plantearme un excell y verificarlo con informaciones de varias webs, ya tenia la fórmula, sólo era faena de implementarla en el Arduino, y así fue: programé y probé sólo con la electrónica del robot (sin conectar motores, sólo Arduino y LCD alimentados por el USB). La cosa cuadraba, y yo contento tras creer haber encontrado con la solución !


Al próximo sábado, repetimos la excursión (esta vez sin magdalenas!) y empecé a montarlo todo...
y el allen del 6 ? mierda ! me la había dejado en casa, tocaría realizar el ajuste basto de alineamiento a base de piedras en la base.

Enfoco a un punto de compromiso (Santuario de St. Benet) y bloqueo con cinta aislante los anillos de zoom y enfoque.  Programo los parámetros tanto en la cámara como en el robot. 


Focal equivalente 900 mm (zoom a 400mm * Teleconversor 1.4x * factor recorte de la Nikon D5200)
ISO 640, f9 y 1/1000" , balance de blancos nublado y todo en JPG (debería comprar otra SD para poder disparar en RAW, pero con las tarjetas que tengo actualmente me quedaría al límite de capacidad y no vale la pena jugársela y quedarte Out of memory). Programo un extremo, luego otro, la nueva función solape la ajusto al 25%... el LCD me informa del número resultante de capturas y tiempo: buff... tengo miedo de no tener capacidad de baterías suficiente en la cámara (tengo 3 EN-EL14). Decido re-programar las tareas del arduino , reduciendo el solape al 20% y "recortando" la imagen resultante. Ahora si me gusta...


Pulso Select y empieza...bién! Me empiezo a olvidar del invento y voy ordenando todo lo innecesario y aprovecho para tweetear el evento. Emma me dice " si que vas a pillar terreno, casi salgo yo "....ein? ostras ! el robot ha girado más de la cuenta ! Un poco más y se me para el corazón al ver que los cables estaban casi al límite, menos mal que veo que acaba la fila y retrocede....empiezo a analizarlo todo  y observo que el robot recorre un 50% de los límites prefijados más de lo que esperaba, todo y ello el solape es bueno.

Dejo que pase el tiempo y tras cercionarme que fila tras fila llega al mismo punto decido que vaya haciendo y yo ya me encargaré luego de recortar la imagen final.


Cuando llevamos 2726 disparos (29 filas x 94 columnas) damos por capturadas todas las imágenes necesarias, si fuera por programación habrían sido unas 3.478 capturas. Es momento de desmontarlo todo y tiempo para empezar con la segunda parte: el procesado.


Una vez en casa, empiezo visualizar varios JPG y los veo borrosos, como desenfocados. Empiezo a intentar buscar una lógica aplastante y no la veo. He realizado varias pruebas y la única opción que veo es que tuviera desajustado las dioptrías situado junto al visor de la cámara (si hubiera enfocado por pantalla...eso no habría pasado!).

Mediante CameraRAW re-nombro los archivos y aprovecho para jugar un poco para darle un poco de nitidez y vidilla, a ver si salvaba las imágenes de la borrosidad modificando claridad, intensidad y saturación. 

Luego , y tras más de tres horas de reconversión ya es tiempo para AutopanoGiga. La previsualización tardó poco más de una hora y la renderización a formato .kro (199.5 Gb) fueron unas 18 horas con el iMac core i5 a 3.6GHz (+16 GB RAM) .


Luego , subirla al servidor de Gigapan , otras 50 horas . En este paso, el servidor de Gigapan se colgó varias veces , menos mal que no se abortó el proceso en ningún momento.


Finalmente el resultado podeis ver el resultado final en :



Por cierto: Ya tengo resueltos los problemas de programación de Arduino, ahora sólo falta salir otra vez a la calle para disfrutar y pasar nuevas aventuras haciendo otra Gigapanorámica.








lunes, 18 de febrero de 2013

Panorama Robot 1.7 , el programa



Quizás, y para estar un poco a la moda le podría llamar 2.0 y hasta 3.0 , aunque en realidad el programa es el mismo de la versión 2011-2012 del robot original, este ha sufrido ligeros cambios y correcciones. En realidad tampoco le debería llamar 1.7, le podría decir versión 2013 y no por el año en que estamos sino por los pequeños cambios que he hecho desde finales del 2011 semana tras semana hasta que funcionara y tuviera un  mejor aspecto. A dia de hoy creo que me funciona todo y que es posible que deba de corregir cosas, pero para los inquietos ya pueden ir copiando y haciendo sus modificaciones pertinentes.

Actualmente está volcado en un ChipKit 32 y si no recuerdo mal, excede de los 34kb.

Algunos de los errores corregidos respecto la versión inicial han sido :

·      Cuando el lcd mostraba el % del proyecto llegaba a un punto en que este valor de volvía loco. @engeeknyer me dio la solución : cambia el int por un long int .
·     * Ya no utilizo mi “keypad” casero, ahora todas las entradas de posicionamiento y validación funcionan mediante el LCD Shield con el que gano espacio, peso y orden.
·     * Antes el robot se pasaba un paso al final de cada línea , se ha corregido modificando el programa.
·      Antes cuando terminaba toda la secuencia de fotografías, el robot al cabo de un rato reiniciaba sólo el ciclo. Ahora se quita todo del loop y al final se queda el sistema bloqueado hasta que le provoquemos un reset.
·       * Optimización de tiempos de espera entre movimientos. Aproximadamente se ha “regateado” entre 1-1.5 segundos/foto.

Las nuevas implementaciones:
·      
*  * Se programa mediante el uso de varias subrutinas, trabajando el programa en diferentes bloques, según como se mire es más fácil de entender dónde empieza y dónde acaba cada cosa.
·      Se puede modificar la focal, antes era focal fija definida por variable.
·      Se puede elegir el formato de cámara entre 4 modelos diferentes y comunes  de formato que hay en el mercado. Viendo el programa es fácil modificar e incluir otros.
    Dado que tenemos más posibilidad de definir parámetros mediante el uso del Shield LCD, provoca que existan más variables “automatizadas” como:
·      Calculo automático del ángulo , provocando que se deba auto-definir los impulsos que deben dar hacia los motores paso a paso.
·      Calculo automático del tiempo de duración del proyecto ya que el tiempo entre capturas no es el mismo al variar los ángulos y tiempos de desplazamiento.

NOTA > Por si hay alguien que se quiere “copiar” el proyecto, informaros que debido a un problema con la Shield LCD tuve que insertar una resistencia entre el pulsador de SELECT y la entrada AnalogRead(A0) ya que no me respondía esta tecla.

Cabecera del programa: Sirve para anotar informaciones que no afectaran al funcionamiento del programa pero que nos puede servir para tenerlas junto al programa cuando lo grabamos. Muchas veces nos puede servir para hacernos recordar que es lo que hace el programa, sobretodo cuando no ponemos títulos muy claros a la hora de guardar el programa.

/*

 Photo Robot 2x L298N + Nema 17
 language: Wiring/Arduino

 This program drives two bipolar stepper motor.
 Photo-shots signals it's actuated by relay --
 Movement manual motor by array switchs of keypad

 Created 28 Dec. 2012
 Last updated 13 Feb. 2013
 v1.7

 by XavierGP

 */

Habilitar uso de librerias y constantes: Una librería es un conjunto de instrucciones que no viene por defecto en la programación de Arduino y que nos simplificaran mucho el trabajo y tamaño del proyecto. En este caso incluimos la librería Stepper.h que hace referencia al movimiento de los motores paso a paso , y la librería LiquidCrystal.h que es la encargada de facilitarnos la faena a la hora de mostrar caracteres en la Shield LCD.

También aprovechamos para definir las siguientes constantes:

·      _360div2xPI la utilizaremos para convertir los angulos de radianes a formato estandar de una vuelta de 360º.
·      motorStepsX es el numero de impulsos que debe dar nuestro motor paso a paso para dar una vuelta, es un valor que en principio nos lo debe dar el fabricante. En este caso lo usaremos para el motor del movimiento horizontal (eje X). En nuestro caso corresponde a 200 impulsos.
·      motorStepsY lo mismo que la anterior constante motorStepsX, pero en este caso es para el motor del movimiento horizontal.
·      fan se trata del numero de salida del Arduino que da orden de funcionamiento del ventilador de refrigeración de los drivers del motor (2x L298N)


#include <Stepper.h>        // incluimos la Stepper library
#include <LiquidCrystal.h>  // incluimos la LiquidCrystal LCD library
#define _360div2xPI     57.29577951
#define motorStepsX 200     // Pasos motor X 360º/1.8º
#define motorStepsY 200     // Pasos motor Y 360º/1.8º                     
#define fan 29              // Pin OUT 29 habilita tensión en ventilador disipador

Definición de variables: definimos las variables que vamos a utilizar. A diferencia de las constantes esto son valores que puede ser que se modifiquen en el transcurso del programa.
·      pulsosvueltaejeX : es el numero de pulsos que debemos dar al motor para que de una vuelta entera el eje X. En teoria deberia ser 200 multiplicado por la reducción que tengamos. Yo preferí generarme un programa para determinar con exactitud el numero de impulsos para dar una vuelta entera.
·       pulsosvueltaejeY: idem , como en pulsosvueltaejeX pero para el motor del movimiento vertical (eje Y).  En mi caso fueron 6444 impulsos.
·       impX es el numero de impulsos que corresponden a 1 grado del ejeX
·       impY es el numero de impulsos que corresponden a 1 grado del ejeY
·       pasoX son los impulsos que realizará entre capturas del eje X y que tiene relación entre los grados que hay en el eje horizontal entre diferentes capturas.
·       pasoY idem pasoX pero para el eje vertical.
·       xact posición actual en grados del eje X.
·       yact posición actual en grados del eje Y.
·       xmax hace referencia a la posición máxima de la captura expresada en grados del eje X.
·       xmin hace referencia a la posición mínima de la captura expresada en grados del eje X.
·       ymax hace referencia a la posición máxima de la captura expresada en grados del eje Y.
·       ymin hace referencia a la posición mínima de la captura expresada en grados del eje Y.
·       xtemp posición actual del robot en grados del eje X.
·       ytemp posición actual del robot en grados del eje Y.
·       nshot numero de foto actual del proyecto.
·       filasX (debería de ser columnas en realidad…) es el numero de fotos a realizar en cada fila horizontal.
·       filasXX usada como variable temporal.
·       columnasY es el numero de todas las filas.
·       columnasYY usada como variable temporal.
·       previsionShot numero total de fotografías a realizar durante el proyecto.
·       segundos duración del proyecto expresada en segundos.
·       minutos duración del proyecto en minutos.
·       horas duración del proyecto en horas.
·       pause variable para mantener o reanudar una pausa.
·       analogico0 es el valor correspondiente a la lectura analógica numero 0 correspondiente a los valores entregados por los diferentes pulsadores de la Shield LCD.
·       temperatura valor de temperatura corregido.
·       voltage es el valor entregado por la bateria usando un divisor de tensión para tener un control sobre la misma.
·       valorTemp lectura del LM35DT encargado de mostrarnos la temperatura del disipador de los drivers L298N.
·       progreso es el valor en % del proyecto realizado.
·       abajo es el valor de la entrada analogica 0 correspondiente al pulsador DOWN.
·       arriba es el valor de la entrada analogica 0 correspondiente al pulsador UP.
·       enter  es el valor de la entrada analogica 0 correspondiente al pulsador SELECT.
·       left es el valor de la entrada analogica 0 correspondiente al pulsador LEFT.
·       right es el valor de la entrada analogica 0 correspondiente al pulsador RIGHT.
·       margen es el valor de la tolerancia que le damos al valor de los pulsadores.
·       d1 es el valor del lado largo del sensor fotografico CMOS expresado en mm.
·       d2 es el valor del lado alto del sensor fotografico CMOS expresado en mm.
·       diagonal_sensor es la diagonal resultante (hipotenusa) del triangulo d1 d2.
·       grados_horizontales es el valor resultante a la variables focal y factor (multiplicación o recorte) en horizontal.
·       grados_verticales es el valor resultante a la variables focal y factor (multiplicación o recorte) en vertical
·       factor es el factor multiplicación o recorte del sensor CMOS.
·       focal es el valor de la focal del objetivo que utilizaremos.
·       paso es una variable que nos ayudará a ir más rápido a la hora de definir la focal. Cuanto mas grande es el factor focal , más incrementa el paso (para no subir todo el rato en pasos de un milímetro)
·       menu es una variable para memorizar el tipo de formato de cámara usado.
·       logo1a – logo1h sirven para definir el gif de la presentación del inicio, en grupos de 5 columnas x 8 filas de pixeles, dónde un 0 es píxel apagado, y un 1 píxel oscuro.


int pulsosvueltaejex=10180;           // 200impulsos x reduccion mecanica
int pulsosvueltaejey=6444;            // 200impulsos x reduccion mecanica
float impX=pulsosvueltaejex/360;      // número de impulsos que corresponden a 1º del eje X   51.9*200/360  
float impY=pulsosvueltaejey/360;      // número de impulsos que corresponden a 1º del eje Y   32.2*200/360
int pasoX;                            // pasos motor entre capturas eje X
int pasoY;                            // pasos motor entre capturas eje Y 
float xact;               // º actual eje X
float yact;               // º actual eje Y
float xmax;               // º posición máxima eje X
float xmin;               // º posición mínima eje X
float ymax;               // º posición máxima eje Y
float ymin;               // º posición mínima eje Y
int xtemp;
int ytemp;
int nshot;
int filasX;                 // número de filas eje X >> (xmax-xmin)/pasoX
int filasXX;
int columnasY;              // número de columnas eje Y    >> (ymax-ymin)/pasoY
int columnasYY;
int previsionShot;          // prevision número shots   filasX * columnasY
int segundos ;
int minutos ;
int horas ;
int pause;
int analogico0;     // variable to store the value coming from the keypad
float temperatura;  // variable to store the value of radiator
float voltage;      // variable to store the voltage battery supply
float valorTemp;
int progreso;

int abajo=450;      //definimos valores teclas
int arriba=210;
int enter=945;
int left=680;
int right=0;
int margen=50;    //abans era 30

float d1;         // distancia 1 sensor CMOS (ancho)
float d2;         // distancia 2 sensor CMOS (alto)
float diagonal_sensor;
float grados_horizontales;
float grados_verticales;
float factor;     // en su defecto : FF = 1, Nikon DX=1.5 , Canon DX=1.6 , Olympus= 2
float focal = 700; 
int paso;
int menu=2;

byte logo1a[8] = {B01111, B00110, B01111, B11111, B11111, B11111, B11110, B11110};
byte logo1d[8] = {B00000, B00000, B11110, B11111, B11111, B11111, B11111, B11111};
byte logo1e[8] = {B11110, B11110, B11110, B11111, B11111, B11111, B11111, B01111};
byte logo1h[8] = {B01111, B01111, B01111, B11111, B11111, B11111, B11111, B11110};


Inicialización de comandos: Direccionamos las entradas/salidas que corresponden a cada librería y a cada salida. En este caso definimos las dos salidas de los Steppers y luego las salidas correspondientes al LCD.
variables que vamos a utilizar. Seguidamente le decimos que el pin 28 lo utilizaremos como una salida al igual que el pin fan (definido en variables como numero 29), y acto seguido le decimos que por defecto queremos que estén en LOW (salidas apagadas). Acto seguido habilitamos la velocidad de los dos steppers al valor de 20 y también “arrancamos” el LCD.

Stepper myStepperX(motorStepsX, 31, 32, 30, 33); //  initialize the library motor X
Stepper myStepperY(motorStepsY, 38, 41, 39, 40); // initialize the library motor Y

LiquidCrystal lcd(8, 9, 4, 5, 6, 7);  // initialize the library with the numbers of the interface pins

void setup() {
pinMode(28, OUTPUT);      // Output digital  shot
pinMode(fan, OUTPUT);     // Output digital  fan
digitalWrite(28, LOW);
digitalWrite(fan,LOW);

myStepperX.setSpeed(20);  // set the motor X speed at 15 RPMS:
myStepperY.setSpeed(20);  // set the motor Y speed at 10 RPMS:
 
lcd.begin(16, 2);

Creamos los caracteres en base a las variables logo1a,1d,1e y 1h:

lcd.createChar(1, logo1a);
lcd.createChar(4, logo1d);
lcd.createChar(5, logo1e);
lcd.createChar(8, logo1h);

Ejecutamos las subrutinas presentacioninicio y defineparametro: lease presentacioninicio y defineparametro . Estas subrutinas se encargan de que nos aparezca el mensaje de bienvenida con el logo de la cámara fotográfica y de definir los parámetros por el usuario correspondiente al formato de cámara y focal que utilizaremos.

presentacioninicio();
defineparametro();

Determinación de los puntos inicio y final de la panorámica: mediante la ayuda de dos subrutinas buscaorigen y buscafinal servirá para determinar la cobertura de nuestra foto. El programa está pensado para que el valor final tanto en X como de Y sea superior al final , es decir si entendemos que la posición inicial seria la esquina inferior izquierda de la fotografía y el punto final seria la esquina superior derecha. El programa NO está pensado para trabajar en otras opciones (habría que mejorarlo o probarlo como responde). Empezamos borrando la pantalla y apareciendo mediante un scroll lateral las instrucciones para localizar el punto de inicio (esquina inferior izquierda): mover mediante flechas y validar con tecla Select. Una vez validado nos aparecerá un mensaje de confirmación, y nos invitará a buscar el punto final (esquina superior derecha) mediante la subrutina buscafinal.  De toda esta parte acabaremos obteniendo el valor de filas y columnas totales.


lcd.clear();
lcd.print("Define posicion origen mediante flechas");
delay (1000);
for (int positionCounter = 0; positionCounter < 23; positionCounter++) {
lcd.scrollDisplayLeft(); // scroll one position left:
delay(350);              // wait a bit:
}
delay (1000);
lcd.setCursor(23,1);
lcd.print("[SELECT]  valida");
delay (3000);
buscaorigen();
lcd.clear();
lcd.print("Define posicion final mediante flechas");
delay (1000);
for (int positionCounter = 0; positionCounter < 22; positionCounter++) {
lcd.scrollDisplayLeft();    // scroll one position left:
delay(350);                 // wait a bit:
}
delay (1000);
lcd.setCursor(22,1);
lcd.print("[SELECT]  valida");
delay (3000);
buscafinal();
xact=-xmin;
yact=-ymin;
filasX=((xmax-xmin)/pasoX)+1;
filasXX=filasX;
columnasY=((ymax-ymin)/pasoY)+1;
columnasYY=columnasY;

Una vez tenemos el numero de filas y columnas es fácil determinar el numero de capturas (previsionShot) pues es una simple multiplicación, y a continuación calcularemos el tiempo requerido para realizar el proyecto en segundos.

previsionShot=filasX*columnasY;
segundos=previsionShot+(0.5*(columnasY-1))+(0.04*(((columnasY-1)*pasoY)+((filasX-1)*columnasY*pasoX))); 


Nuestro robot se mueve hacia el punto de origen mientras el LCD se borra varias veces y nos informa del numero de capturas, filas, columnas y grados correspondientes.



lcd.clear();
lcd.print("Moviendo a posicion de inicio         ");
lcd.setCursor(0,1);
lcd.print("<<< << <   < < <    < <     <         ");
delay(500);
for (int positionCounter = 0; positionCounter < 23; positionCounter++) {
lcd.scrollDisplayLeft(); // scroll one position left:
delay(350);              // wait a bit:
}
lcd.clear();
lcd.print("espera...");
myStepperX.step(-(filasX-1)*pasoX);
myStepperY.step(-(columnasY-1)*pasoY);   

lcd.clear();
lcd.print("Home position !");
delay(3000);
lcd.clear();
lcd.print(previsionShot);
lcd.print(" capturas:");   
lcd.setCursor(0, 1);
lcd.print(filasX);
lcd.print("H * ");
lcd.print(columnasY);
lcd.print("V");
delay (5000);
lcd.setCursor(0,1);
  
lcd.print((-xmin+xmax)/impX);
lcd.print(char(223));
lcd.print("H ");
lcd.print((-ymin+ymax)/impY);
lcd.print(char(223));
lcd.print("V     ");
delay (5000);
 
Ejecutamos la subrutina conversiontiempo para que nos pueda expresar la duración del proyecto en horas, minutos y segundos . Léase conversiontiempo.

conversiontiempo();

Tenemos todas las variables definidas y nuestro robot a punto, sólo falta que pulsemos SELECT para iniciar. El LCD nos informa de cómo empezar y como pausar el proyecto.

lcd.clear();
lcd.print("Pulse [SELECT] para iniciar y para pausar ");
lcd.setCursor(0,1);
lcd.print("               ");
delay (1000);
for (int positionCounter = 0; positionCounter < 24; positionCounter++) {
lcd.scrollDisplayLeft();   // scroll one position left:
delay(350);                // wait a bit:
}

delay(2000);
lcd.clear();
lcd.setCursor(0,1);
lcd.print("[SELECT] = start");
pause=1;

while(pause!=0){
if((analogRead(0)/margen)==(enter/margen)){   // Tecla SELECT
pause=0;
}
}
xmax=xmax-xmin;
ymax=ymax-ymin;
xmin=0;
ymin=0;
xact=0;
yact=0;
 

Rutina de captura de fotografía: movimiento de motores: Empieza el ciclo disparando una foto y acto seguido el robot se mueve en sentido hacia la derecha un PasoX (grados resultantes de la focal y factor cámara definidos) y a continuación otra foto hasta llegar al valor máximo programado como valor máximo de X (numero de filasX), momento en que retrocederá el robot hasta el punto de inicio e incrementará el ángulo en el eje vertical un PasoY antes de empezar otra vez a realizar todas las fotos de la misma fila.

for(int i=0; i<columnasY; i++){      // empieza secuencia panoramica        
delay(1000);
for(int j=0; j<(filasX-1); j++){   //FILASX

Analizamos la temperatura del disipador (señal del LM35) y decidimos si activamos el ventilador (temperatura superior a 33ºC) o si lo paramos (temperatura inferior a 29ºC)

temperatura=analogRead(A6);         // comprobamos temperatura en disipador LM35DT
valorTemp = ((500 * temperatura * 5 )/1024.0);
if(valorTemp<290){digitalWrite(fan,LOW);}              // deshabilita tensión en ventilador - energy saving - si temperatura < 29ºC
if(valorTemp>330){digitalWrite(fan, HIGH);}            // habilita tensión en ventilador si temperatura > 33ºC

Activamos la señal hacia el disparador remoto de la camara durante 500 ms.

digitalWrite(28, HIGH);
delay(500);                         // original 500 aquest timer es pot reduir !!!!!!!!!!!!!!!
nshot=nshot++;

mientras ejecuta el proyecto, en el LCD nos muestra el progreso, numero de foto , la fila y la columna.

lcd.clear();
progreso=((nshot*100)/(previsionShot));
lcd.print(progreso);
lcd.print("%");
lcd.setCursor(7,0);
lcd.print("X:   Y:");
lcd.setCursor(0,1);
lcd.print(nshot);
lcd.setCursor(7,1);
lcd.print(j+1);
lcd.setCursor(12,1);
lcd.print(i+1);

desactivamos la señal del disparador remoto y analizamos si se está pidiendo de realizar una pausa en el proyecto (subrutina peticiopausa)

digitalWrite(28, LOW);
peticiopausa();  
delay(500);
if(j!=(filasX+0)){  
myStepperX.step(pasoX);
xact=xact+(pasoX);
}
delay(500);                                                                                 // original 750 aquest timer es pot reduir !!!!!!!!
}

nshot=nshot++;
digitalWrite(28, HIGH);
lcd.clear();
progreso=((nshot*100)/(previsionShot));
lcd.print(progreso);
lcd.print("%");
lcd.setCursor(7,0);
lcd.print("X:   Y:");
lcd.setCursor(0,1);
lcd.print(nshot);
lcd.setCursor(7,1);
lcd.print(filasX);
lcd.setCursor(12,1);
lcd.print(i+1);
delay(500);                         // original 500 aquest timer es pot reduir !!!!!!!!!!!!!!!
digitalWrite(28, LOW);
delay(500);

una vez llegamos a la ultima captura de cada fila, el LCD nos mostrará la temperatura del disipador y el voltage de la bateria mediante una llamada a la subrutina pantallatemperatura a la vez que el robot retorna al origen en X y acto seguido incrementa un paso en el eje Y.

pantallatemperatura();
      
myStepperX.step((-(filasXX)+1)*pasoX);   //////////////+0
xact=0;
if(i!=columnasY-1){myStepperY.step(pasoY);}                                
yact=yact+(pasoY);
delay(1000);
}

Una vez se han completado todas las filas, columnas y fotos, tan sólo nos queda esperar a que el robot vuelva a la posición inicial y nos aparezca el mensaje de “Panorámica Completa ! “ y acto seguido entraremos en un loop del cual es imposible salir hasta que no pulsemos el botón de reset, con esto evitaremos reinicios automáticos como hacia la versión anterior de este programa. Ahora ya sólo nos queda procesar todas las capturas en nuestro ordenador.

lcd.clear();
lcd.print("Panoramica");
lcd.setCursor(5,1);
lcd.print("completa !!");
myStepperY.step((-columnasYY+1)*pasoY);
}
void loop(){
}

Subrutina defineparametro: En esta subrutina vamos a definir el tipo de cámara réflex que usamos. En principio se han programado los 4 tipos más conocidos según el formato de recorte (Full Frame, Nikon DX, Canon DX y Olympus 4/3). Es muy fácil insertar más tipos, tan solo tocará buscar las medidas de los sensores CMOS y calcular su diagonal. En esta programación por defecto parto de que utilizaré una Nikon DX y un zoom de 700mm. Si queréis definir otra tan sólo hay que cambiar los valores en las variables al inicio del programa.


void defineparametro(){ // rutina definimos tipo de camara y focal
lcd.clear();
lcd.print(" Formato camara");
lcd.setCursor(2, 1);
lcd.print("[UP] & [DOWN]");
delay(1500);
while((analogRead(0))/margen!=(enter/margen)){ //enter
delay(300);
if((analogRead(0)/margen)==(arriba/margen)){  //arriba
menu=menu+1;
if(menu>=4){menu=4;}
lcd.clear();
}
if((analogRead(0)/margen)==(abajo/margen)){   //abajo
menu=menu-1;
if(menu<=1){menu=1;}
lcd.clear();
}
if(menu==1){ // FF
lcd.print("Full Frame 35 mm.");
d1=35.8;         //canon 5d
d2=23.9;         //canon 5d
factor=1;
diagonal_sensor=43.0447441623248;
}
if(menu==2){ // Nikon DX
lcd.print(" Nikon DX  1.5x");
factor=1.5;
d1=23.6;         // distancia 1 sensor CMOS (ancho)   Valor de la Nikon d90 !
d2=15.8;         // distancia 2 sensor CMOS (alto)    Valor de la Nikon d90 !
diagonal_sensor=28.40070421662111;
}
if(menu==3){ // Canon DX
lcd.print(" Canon DX  1.6x");
factor=1.6;
d1=22.2;         // eos1000d
d2=14.8;         // eos1000d
diagonal_sensor=26.68107943843352;
}
if(menu==4){ // Olympus 4/3
lcd.print("Olympus 4/3   2x");
factor=2;
d1=17.3;         // e-520
d2=13.0;         // e-520
diagonal_sensor=21.6400092421422;
}
lcd.setCursor(0, 1);
lcd.print("[SELECT]  valida");
}

En la segunda parte de esta subrutina, definiremos la focal que vamos a utilizar. Aquí se utiliza la variable paso que tiene la función de cambiar el incremento de la focal a medida que va creciendo o decreciendo el valor con la finalidad de tener velocidad a la hora de ajuste sin perder precisión en el valor.



lcd.clear();     // rutina definimos focal
lcd.print("Longitud  focal:");
lcd.setCursor(2, 1);
lcd.print("[UP] & [DOWN]");
delay(1500);
while((analogRead(0))/margen!=(enter/margen)){ //enter
if (focal<100){
paso=1;
delay(75);
}
if (focal>=100){
paso=25;
delay(120);
}
if (focal>399){
paso=50;
delay(150);
}
if((analogRead(0)/margen)==(arriba/margen)){  //arriba
focal=focal+(paso);
lcd.clear();
}
if((analogRead(0)/margen)==(abajo/margen)){   //abajo
if(focal>1){focal=focal-(paso);}
lcd.clear();
}
lcd.print(focal);
lcd.print(" mm.");
lcd.setCursor(0, 1);
lcd.print("[SELECT]  valida");
}
// diagonal_sensor=sqrt((d1*d1)+(d2*d2));
//factor =35/d1;

Seguidamente y haciendo uso de las variables de focal y el formato de cámara, el programa calculará la equivalencia en grados tanto en vertical como en horizontal (en principio la programación esta pensada en que la  cámara esté situada en horizontal!), y acto seguido recalculará cuantos impulsos debe ejercer en cada uno de los dos motores para lograr un solape optimo).


grados_horizontales= 2* (atan(diagonal_sensor/(2*focal*factor))*_360div2xPI);
grados_verticales = grados_horizontales * d2 / d1;

pasoX=(pulsosvueltaejex*(grados_horizontales/360));
pasoY=(pulsosvueltaejey*(grados_verticales/360));
lcd.clear();
lcd.print ("Focal equivalente");
lcd.setCursor(2, 1);
lcd.print(focal*factor);
lcd.setCursor(10, 1);
lcd.print("mm.");
delay(3000);
}


Subrutina presentacioninicio: básicamente es la presentación del inicio dónde nos da la bienvenida el LCD y acto seguido muestra la versión del programa y un gif animado de una cámara haciendo una foto. Dentro de esta subrutina, llamaremos a otras dos logo2 y logo3 que es dónde contiene parte de la programación del gif. Según mis pruebas sólo pueden haber 8 caracteres definidos por el usuario a la vez, es por ello que utilizo subrutinas para acortar el programa.



void presentacioninicio(){
lcd.clear();
lcd.print(" Hola XavierGP!");
delay(3000);
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Panorama");
lcd.setCursor(0,1);
lcd.print("Robot");
logo3();
delay(800);
logo2();
delay(400);
logo3();
lcd.setCursor(6,1);
lcd.print("v1.7");
delay(800);
logo2();
delay(400);
logo3();
delay(3000);  // fin GIF
}

void logo2(){
byte logo1b[8] = {B00000, B00111, B11100, B10001, B00111, B01111, B01111, B11111};
byte logo1c[8] = {B00000, B11100, B00111, B10001, B11100, B11110, B11110, B11111};
byte logo1f[8] = {B11110, B11111, B01111, B01111, B00111, B10001, B11100, B11111};
byte logo1g[8] = {B01111, B11111, B11111, B11110, B11100, B10001, B00111, B11111};
byte logo1d[8] = {B00000, B00000, B11110, B11111, B11111, B11111, B11111, B11111};
lcd.createChar(2, logo1b);
lcd.createChar(3, logo1c);
lcd.createChar(6, logo1f);
lcd.createChar(7, logo1g);
lcd.createChar(4, logo1d);
lcd.setCursor(12,0);
lcd.write(1);
lcd.setCursor(13,0);
lcd.write(2);
lcd.setCursor(14,0);
lcd.write(3);
lcd.setCursor(15,0);
lcd.write(4);
lcd.setCursor(12,1);
lcd.write(5);
lcd.setCursor(13,1);
lcd.write(6);
lcd.setCursor(14,1);
lcd.write(7);
lcd.setCursor(15,1);
lcd.write(8);
}

void logo3(){
byte logo1b[8] = {B00000, B00111, B11100, B10001, B00110, B01100, B01000, B10000};
byte logo1c[8] = {B00000, B11100, B00111, B10001, B01100, B00110, B00010, B00001};
byte logo1f[8] = {B10000, B10000, B01000, B01100, B00110, B10001, B11100, B11111};
byte logo1g[8] = {B00001, B00001, B00011, B00110, B01100, B10001, B00111, B11111};
byte logo1d[8] = {B00000, B00000, B11110, B11111, B11111, B11111, B11111, B11111};
lcd.createChar(2, logo1b);
lcd.createChar(3, logo1c);
lcd.createChar(6, logo1f);
lcd.createChar(7, logo1g);
lcd.createChar(4, logo1d);
lcd.setCursor(12,0);
lcd.write(1);
lcd.setCursor(13,0);
lcd.write(2);
lcd.setCursor(14,0);
lcd.write(3);
lcd.setCursor(15,0);
lcd.write(4);
lcd.setCursor(12,1);
lcd.write(5);
lcd.setCursor(13,1);
lcd.write(6);
lcd.setCursor(14,1);
lcd.write(7);
lcd.setCursor(15,1);
lcd.write(8);
}

Subrutina conversiontiempo: Tal y como dice el nombre, esta subrutina sólo tiene la finalidad de convertir el tiempo de duración del proyecto que teníamos en formato segundos a formato horas, minutos y segundos.


void conversiontiempo(){  // convertimos tiempo necesario en realizar proyecto
while(segundos>=3600){  
horas = horas++;
segundos = segundos - 3600;
}
while(segundos>=60){
minutos = minutos++;
segundos = segundos - 60;
}
lcd.clear();
lcd.print("Duracion prevista");
lcd.setCursor(0,1);
lcd.print(horas);
lcd.print("hrs ");
lcd.print(minutos);
lcd.print("min ");
lcd.print(segundos);
lcd.print("seg");
delay(5000);
}


Subrutina pantallatemperatura: esta es la pantalla que nos mostrará el LCD cuando está retornando el robot a la columna de inicio. Podremos ver la temperatura del disipador y el voltaje de la batería a modo de información. En el valor de voltaje he tenido de corregirlo para que se asemejara lo más posible a mi realidad.


void pantallatemperatura(){
lcd.clear();
lcd.print("Voltage ");
voltage=analogRead(A7)*1.212;
lcd.print(voltage/100);  
lcd.print(" v.");
lcd.setCursor(0,1);
lcd.print("Temp.:  ");
lcd.print(valorTemp/10);
lcd.print(" ");
lcd.print(char(223));
lcd.print("C");
}


Subrutina peticiopausa: preguntamos al pic si hay la entrada analógica 0 en valor de Select durante 10 veces y si es asín provocamos una pausa forzada de 10 segundos antes no podamos volver a pulsar la misma tecla para reprender con nuestro proyecto fotográfico. El por que de las 10 veces es porque me he encontrado que o el pic corre mucho y “no se entera” o el valor no era el exacto al programar la pausa.


void peticiopausa(){
for (int k=0; k<10; k++){ //prova pausa
if((analogRead(0)/margen)==(enter/margen)){     // tecla SELECT si apretem provoquem una pausa fins que no apretem ENTER
lcd.clear();
lcd.print("Pausa!");
lcd.setCursor(0,1);
lcd.print("[SELECT]=reStart");
delay(10000);  //abans no hi era i em rearrancaba
pause=2;
while(pause>1){
if((analogRead(0)/margen)==(enter/margen)){   // tecla SELECT
pause=0;
lcd.clear();
lcd.print("SHOT   X:   Y:");
delay(1000);
}
}
}
} //prova pausa
}

Subrutina buscaorigen: hasta que no pulsemos select no nos validará la posición que deseemos y que podemos desplazar mediante el uso de los pulsadores up, down, left y right mediante la subrutina movimiento. A continuación memorizará estos valores como la xmin y la ymin.

void buscaorigen(){
while((analogRead(A0)/margen)!=(enter/margen)){
movimiento();
}
xmin=xact;
ymin=yact;
lcd.clear();
lcd.print("Posicion  origen");
lcd.setCursor(0, 1);
lcd.print(" - CONFIRMADA - ");
delay(3500);
}


Subrutina buscafinal: lo mismo que en buscainicio pero definiremos el punto final de nuestras capturas, memorizándolos como xmax e ymax.


void buscafinal(){
while((analogRead(A0)/margen)!=(enter/margen)){
movimiento();
}
xmax=xact;
ymax=yact;
lcd.clear();
lcd.print("Posicion  final");
lcd.setCursor(0, 1);
lcd.print(" - CONFIRMADA - ");
delay(3500);
}
 
Subrutina movimiento: tiene la función de hacer mover el robot manualmente mediante los pulsadores up, down, left y right y acto seguido informándonos en el LCD la posición del mismo.



void movimiento(){
analogico0 = analogRead(A0);   
if((analogRead(A0)/margen)==(right/margen)) {xact=xact+pasoX;}    //  derecha +X
if((analogRead(A0)/margen)==(left/margen))  {xact=xact-pasoX;}    //  izquierda -X  
if((analogRead(A0)/margen)==(abajo/margen)) {yact=yact-pasoY;}    //   abajo -Y
if((analogRead(A0)/margen)==(arriba/margen)){yact=yact+pasoY;}    //   arriba +Y
lcd.clear();
lcd.print("X:      Y:");
lcd.setCursor(0, 1);
lcd.print(xact/impX);
lcd.print(char(223));
lcd.setCursor(8, 1);
lcd.print(yact/impY);
lcd.print(char(223));
myStepperX.step(xact-xtemp);
xtemp=xact;
myStepperY.step(yact-ytemp);
ytemp=yact;
delay(20);
                     }


Con todo esto ya tenemos el programa enterito y funcional, aunque seguro que muchos de vosotros podéis optimizarlo mucho más y corregir errores, todo es cuestión de pasarse horas pensando , modificando y probando.

Lógicamente no lo he probado con todas las focales y ni mucho menos con los 4 formatos de factor recorte , pero en teoría y repito: en teoría debería ir…

Personalmente creo que hay un error al no definir el solape entre capturas pero después de varias pruebas, aun no lo veo claro ! y como que es un proyecto personal sin ningún animo de lucro ni de proyecto de carrera ni nada que se le parezca creo que ya está bastante bien, al menos era la intención , pero todo conlleva mucho tiempo, horas ya sea haciendo agujeros, buscando material por eBay, documentando y buscando fórmulas, haciendo videos y luego editarlos, fotos, entrada al blog…na que hay un buen curro que espero que algunos sepáis valorar.

Finalmente os dejo un video en que podéis medio ver como funciona el tema de pantallas, deseo que os guste.


Tengo previsto alojar el programa entero en Internet, pero aun no tengo claro dónde para poder tener un control a título personal del número de descargas…¿alguna idea/propuesta?

¿ Alguien se anima a crear la versión 2.0 ?

Saludos a todos los visitantes!