Fotos panorámicas con ESP32-CAM y Python

La idea es sencilla tomar una foto, rotar la cámara ciertos pasos, volver a tomar una foto y repetir el proceso hasta lograr tener suficientes fotos para construir una imagen panorámica.

Mi intención es poder realizar timelapse panorámicos repitiendo este método en intervalos de 15 minutos.

Voy a comenzar diciendo que se puede mejorar, y bastante, pero es más una prueba de concepto que un producto terminado; y como prototipo al menos funciona bastante bien.

El circuito es bastante sencillo:

Los componentes utilizados son los siguientes en la lista provistos por DigitSpace:

En el siguiente post se pueden encontrar el detalle de la construcción/ensamble del hardware.

Tome de base el programa que tenia previamente para obtener imágenes con el ESP32-CAM:


https://github.com/gsampallo/esp32cam-python

El programa esta armado sobre Python 3.7.4 y OpenCV 4.1.1, utilice docker para evitar la instalación de OpenCV y las librerías, el detalle de la instalación y build de las imágenes pueden encontrarlo aquí.

Comencemos con el programa que corre el ESP32-CAM, se debe ocupar de tres tareas:

  1. Tomar imágenes.
  2. Mover el motor para reposicionar la cámara.
  3. Mantener la posición de la cámara, para pode reubicarla.

El punto 1 lo tenemos resuelto en el programa linkeado anteriormente.

Sobre el punto 2 debemos:

const int motorPin1 = 12;
const int motorPin2 = 13;
const int motorPin3 = 15;
const int motorPin4 = 14;
int origin = 0;

int motorSpeed = 1200;   //spped
int stepCounter = 0;     //step counter
int stepsPerRev = 512;  //number of steps per lap
 
const int numSteps = 8;
const int stepsLookup[8] = { B1000, B1100, B0100, B0110, B0010, B0011, B0001, B1001 };

Se definen los GPIO donde estan conectados el ULN2003. de manera que motorPïn1 este conectado a IN1.

La variable origin indica la posición actual del motor luego de que se haya llamada a cada una de las funciones que mueven el motor.

void clockwise() {
  Serial.println("Girando derecha");
  stepCounter++;
  if (stepCounter >= numSteps) stepCounter = 0;
  setOutput(stepCounter);
}


void anticlockwise() {
  Serial.println("Girando izquierda");
  stepCounter--;
  if (stepCounter < 0) stepCounter = numSteps - 1;
  setOutput(stepCounter);
}
 
void setOutput(int step) {
  digitalWrite(motorPin1, bitRead(stepsLookup[step], 0));
  digitalWrite(motorPin2, bitRead(stepsLookup[step], 1));
  digitalWrite(motorPin3, bitRead(stepsLookup[step], 2));
  digitalWrite(motorPin4, bitRead(stepsLookup[step], 3));
}
void moveLeft() {
  for (int i = 0; i < 128; i++) { //tenia 256
    anticlockwise();
    delayMicroseconds(motorSpeed);
  }  
  
}

void moveRight() {
  for (int i = 0; i < 128; i++) {
    clockwise();
    delayMicroseconds(motorSpeed);
  }  

}

Con las funciones definidas anteriormente combinadas podemos mover el motor en ambas direcciones. Al ser un motor paso a paso debemos encender y apagar cada una de las bobinas del mismo de manera alternada; para ello utilizamos la función moveRight/moveLeft combinada con clockwise y la cantidad de pasos. En este caso utilizamos 128 paso en cada iteración. Si aumentamos ese valor el angulo entre giro y giro sera mayor.

En el método setup adicionalmente a lo que ya teníamos se suma:

  pinMode(motorPin1, OUTPUT);
  pinMode(motorPin2, OUTPUT);
  pinMode(motorPin3, OUTPUT);
  pinMode(motorPin4, OUTPUT);  

Se definen que los GPIO que controlan al motor sean salidas.

  server.on("/info", info);
  server.on("/moverIzquierda", moverIzquierda);
  server.on("/moverDerecha", moverDerecha);
  
  initialMove();

/info llama a la función info.

void info() {
  server.send(200, "text/plain", "{ \"pos\":\""+String(origin)+"\"}");
}

Esta función devuelve un JSON con la posición actual del motor.

El dispositivo al no tener un mecanismo para determinar donde esta el HOME, toma el valor 0 (Home) al recibir alimentación, es decir que cuando lo prendemos toma esa posición como Home. Es importante al momento de que no se enreden los cables.

/moverIzquierda mueve la cámara hacia la izquierda. Dependerá de como hayamos conectados los cables (GPIO 12,13,15,14) al ULN2003 para determinar cual es derecha y cual es izquierda. Si se invierten cambia la dirección.

void moverIzquierda() {
  moveLeft();
  origin--;
  off();
  info();
}

Esta función llama a moveLeft() que mueve la cámara 128 pasos, luego resta en 1 a la posición, pone en 0 la salida de los GPIO conectados al ULN2003 (para ahorrar energía, puesto que la placa tiene unos leds) y por ultimo llama a la función info() para devolver la posición actual.

Lo mismo realiza moverDerecha() pero en sentido inverso.

Con una combinación de las funciones info() y moverDerecha() y moverIzquierda() podemos controlar el movimiento de la camara y saber donde esta ubicada, cumplimos con el punto 3.

Respecto al programa en Python, debe cumplir con dos tareas:

  1. Mover la cámara a la posición Home antes de comenzar a tomar fotos.
  2. Mover la cámara, aguardar que este en posición y tomar la foto.
  3. Combinar todas las fotos en una sola ( stitch ).

Dentro de los parámetros que se definen, quizás el mas importante es url y nPhoto:

url = "http://192.168.1.111/"
nPhoto = 16

Es importante reemplazar url por la dirección correcta, dependerá de lo que tome dentro de la red.Otro parámetro es nPhoto es la cantidad de fotos que se tomaran para armar la composición final.

Para cumplir con el punto 1, nos ayudamos de la función moveHome(), cuya tarea es mover la cámara hasta la posición 0 (Home).

#According to the position move the camera to home position
def moveHome():
    response = requests.get(info)
    data = json.loads(response.content.decode())

    pos = int(data["pos"])
    print(pos)
    
    while(pos > 0):
        response = requests.get(left)
        data = json.loads(response.content.decode())

        pos = int(data["pos"])     

Luego que la cámara este en Home, utilizamos moveRight() para reposicionar la cámara para una nueva foto:

#Move the camera to right
def moveRight():
    response = requests.get(right)
    data = json.loads(response.content.decode())

    pos = int(data["pos"])
    print(pos)  

Para tomar una nueva foto utilizaremos takePhoto(n), la cual descarga la foto (gracias a /cam), la rota 270°, debido a que el ESP32-CAM esta en posición vertical y luego la guarda dentro de la carpeta temp.

def takePhoto(n): #Take the photo, rotate 270 degrees and save in temp folder

    response = requests.get(cam)
    img = Image.open(BytesIO(response.content))
    transposed  = img.transpose(Image.ROTATE_270) 
    transposed.save(temp+str(n)+".jpg")   

Para cumplir el punto dos, utilizamos una combinación de las anteriores:


moveHome()

n = 0
while(n < nPhoto):
    takePhoto(n)
    moveRight()
    n += 1

En este punto tenemos todas las imagenes parciales dentro de la carpeta /temp, es momento de cumplir con el punto 3 de unir ( stitch ) las mismas para formar una sola.

#Read each of the images and then put it in the array
n = 0
imgs = []
while(n < nPhoto):
    archivo = "temp/"+str(n)+".jpg"
    print(archivo)
    img = cv2.imread(archivo)
    imgs.append(img)
    n += 1

#Stitch the images on the array, and save the output image. 
stitcher = cv2.Stitcher.create(cv2.Stitcher_PANORAMA)
status, pano = stitcher.stitch(imgs)

if status != cv2.Stitcher_OK:
    print("Can't stitch images, error code = %d" % status)
    sys.exit(-1)

cv2.imwrite(output+getFileName(), pano);

La foto final sera almacenada en la carpeta output, tiene como nombre la fecha+hora.



https://github.com/gsampallo/PanoramicCam

ESP32-Cam: Subir fotos a Google Drive

En el articulo anterior sobre el ESP32-CAM vimos cuales son los primeros pasos para comenzar a utilizarlo y como obtener imágenes desde el dispositivo con un sencillo script en Python; en esta ocasión veremos como tomar fotos y subirlos a una carpeta de Google Drive de manera de que nos sirva de punto de partida para otras aplicaciones.

El primer paso consiste en utilizar el script de Google para construir un sencillo programa que reciba la imagen y la almacene dentro de una carpeta determinada, para ello debemos ir a Google Drive – Nuevo – Más – Google Apps Scripts

Se va a presentar un editor para que escribamos el código como el siguiente::

Reemplazaremos el contenido total por el siguiente Reem:

function doPost(e) {
  var data = Utilities.base64Decode(e.parameters.data);
  var nombreArchivo = Utilities.formatDate(new Date(), "GMT-3", "yyyyMMdd_HHmmss")+".jpg";
  var blob = Utilities.newBlob(data, e.parameters.mimetype, nombreArchivo );
  
  
  var folder, folders = DriveApp.getFoldersByName("ESP32-CAM");
  if (folders.hasNext()) {
    folder = folders.next();
  } else {
    folder = DriveApp.createFolder("ESP32-CAM");
  }
  var file = folder.createFile(blob); 
  
  return ContentService.createTextOutput("Completo.")
}

El programa lo que hace es recibir tres parámetros:
1. La data en si, que consiste en la imagen, codificada en base64
2. El tipo de dato que se envía, en este caso sera image/jpeg
3. También recibe el nombre del archivo, aunque no lo utilizamos porque tomamos la fecha y hora para darle el nombre, de manera que quede organizado.

El paso siguiente es guardar el script y publicarlo como aplicación web, esto ultimo lo hacemos desde Publicar – Implementar como aplicación web; debemos elegir que se ejecute como nuestro usuario y que cualquier persona incluso los anónimos tengan acceso a la aplicación; luego confirmamos las opciones que se nos presenta.

En la ultima opción Google nos mostrar una url donde estará publicada nuestra aplicación; debemos copiar esta url, la utilizaremos en el programa del ESP32-CAM.

La url provista tendra el siguiente formato:

https://script.google.com/macros/s/XXXXXXXXXXXXXX/exec

Donde las XXXXXXXXXXXXXX serán reemplazadas por una secuencia alfanumérica que identifica a la aplicación.

En este punto estamos listos para cargar el programa en el ESP32-CAM, crearemos un nuevo programa con el IDE de Arduino llamado ESP32-CAM_Drive, de manera que nos cree la carpeta donde esta el archivo ESP32-CAM_Drive .ino; en dicha carpeta copiaremos los archivos Base64.cpp y Base64.h.

Dentro del archivo ESP32-CAM_Drive.ino al comienzo debemos reemplazar tres parámetros importantes: el nombre y contraseña de la red WiFiy la url del script que creamos anteriormente.

const char* ssid     = "SSID";   //your network SSID
const char* password = "PASSWORD";   //your network password
String myScript = "/macros/s/XXXXXXXXXXXXXX/exec";    //Replace with your own url

La lineas anteriores debemos reemplazar el código por los propios.

Para cargar el programa puede repasar el articulo anterior o ver video. Luego que se cargue el programa, desconectamos el GND de IO0 y reiniciamos el ESP32, al cabo de unos instantes se conectara a la red inalambrica, obtendra la foto y la subira al Drive, si vemos en el Monitor serie veremos algo similar a esto:

El programa creara una carpeta llamada ESP32-CAM dentro de Google Drive y en la misma guardara las imagenes, si revisamos la carpeta tendremos las fotos que saco el dispositivo:

La foto del Nano Osciloscopio.

Pueden encontrar el repositorio en GitHub.