Construyendo PanoramicCam

En una entrada anterior comente como trabaja el software de PanoramicCam, pueden encontrarlo aquí.

En este punto me quiero enfocar un poco sobre el hardware utilizado, el cual fue auspiciado por DigitSpace, los links de cada uno de los componentes están debajo:

Comencé cortando una pieza de pcb ligeramente superior al tamaño del ESP32-CAM:

Utilice el soporte impreso para marcar donde irían los agujeros para sujetar el pcb al soporte; cuidado al performar, el pcb puede quebrarse.

Luego posicionamos el ESP32-CAM junto con los headers y lo soldamos en la posición, de manera que quede de la siguiente manera:

Debemos soldar 6 cables sobre el ESP32-CAM; dos son la alimentación que van a 5V y GND, y los cuatro restantes es la comunicación con el ULN2003, van soldados a
GPIO 12,13,15 y 14.

Se utiliza el siguiente esquema de conexiones:

Noten que en la alimentación del ESP32-CAM utilice un condensador, usualmente no lo requiere y funciona correctamente, pero ayudo mucho a prevenir brownout. En mi caso utilice un condensador electrolítico, que era lo que tenia disponible en el momento.

Una vez soldados los cables, podemos comenzar a montar la base del soporte;

El soporte impreso tiene una mueca donde entra el breakout usb, antes de pegarlo a la base, es necesario soldar cuatro cables, dos para los 5V, en mi caso utilice dos tipos de conectores diferentes: uno para conectar la alimentación del ESP32-CAM y otro para conectar el ULN2003.

El pcb del ULN2003 lo atornille al soporte, aunque podría ir simplemente pegado. Si lo pegan tengan cuidado de no aplicar mucho pegamento derretido junto, el calor puede deformar el soporte.

El motor paso a paso va montado sobre el soporte con dos tornillos y tuercas; las tuercas las puse en la parte interior de manera que se sujeten contra el mismo motor y sea sencillo de ajustar.

El soporte impreso no tiene 100% la coincidencia de donde van los tornillos, los volvi a hacer con un taladro y quedo bien. Decidi que el STL no tenga las perforaciones de los tornillos sino que cada uno las haga por su cuenta, de esa manera permite reemplazar el motor por otro.

El link de thingiverse para descargar los modelos es el siguiente:
https://www.thingiverse.com/thing:4270344

El pcb donde tenemos el ESP32-CAM va montado sobre el soporte vertical utilizando un precinto; en mi caso no tenia ningún tornillo de ese diámetro y también previene de sumar mas peso al cabezal.

También utilice un precinto para mantener juntos los cables.

Es importante pasarlos por el orificio del soporte impreso antes de soldar los conectores. Luego sera imposible.

Luego solo resta poner los conectores a los cables, en mi caso emplee de diferente color y los agrupe por bobina con una cinta:

De esta manera tenemos para la bobina 1 en IN1 y IN2 y para la bobina 2 en IN3 y IN4.

Por ultimo debemos organizar un poco los cables y colocar la tapa inferior con dos tornillos.

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