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

Instalar Mosquitto fácil y rápido

La parte de rápido depende mucho de la conexión de internet de cada uno para poder descargar los archivos, habiendo hecho esta salvedad.

Nos encontramos que para la mayoría de los proyectos IoT en los que trabajamos necesitamos tener disponible un servidor broker para poder enviar mensajes con el dispositivo que estamos armando; en muchas ocasiones con un broker publico de internet solucionamos el problema, pero otras veces necesitamos tener algo local (a raíz que nuestra conectividad no es estable o simplemente no disponemos de acceso a internet donde vamos a instalar el dispositivo).

Mosquitto es uno de los brokers mas utilizado por ser opensource y ser mantenido por la fundación Apache, para instalarlo es entornos linux es sencillo simplemente lo hacemos por medio del gestor de paquetes.

Pero la realidad es que en la mayoría de los laboratorios en escuelas o universidades las pcs tiene Windows; e instalar el servicio del broker resulta un poco engorroso, particularmente si no se va a usar de manera dedicada y no queremos el servicio corriendo.

La forma mas sencilla de instalarlo es utilizando el esquema de contenedores de Docker, tres sencillos pasos:

  1. Será necesario instalar primero Docker en nuestra pc, la instalación es sencilla simplemente damos continuar y al finalizar nos pedirá reiniciar el sistema.
  2. Luego que tengamos Docker corriendo en el sistema (en la traybar veremos el icono de la ballena) ejecutamos desde la consola:
docker pull eclipse-mosquitto

Para que descargue el contenedor de mosquitto en nuestra pc.

3. Creamos una instancia del contenedor, especificando el puerto sobre el que va a trabajar, utilizaremos los puertos 9001 y 1883 que son estándar para este servicio:

docker run -p 1883:1883 -p 9001:9001 eclipse-mosquitto

Esto crea un contenedor que sirve para entornos de prueba y laboratorio, no se guarda las configuraciones, ni tienen ningún mecanismo de seguridad.

Pero la puesta en marcha es rápida y sin complicaciones; luego de que se haya descargado la primera vez el contenedor las siguientes ejecuciones son aun mas ágiles.