Una cámara conectada a un Raspberry Pi deja de ser una cámara en el momento en que Python puede leer cada fotograma. Ahí ya no estás sacando fotos: estás midiendo luz, comparando imágenes y decidiendo qué hacer con lo que ve el sensor. Esa es la diferencia entre rpicam-still y Picamera2.

Esta guía recorre Picamera2. la biblioteca oficial de Raspberry Pi para sus módulos de cámara. desde la primera foto hasta un detector de movimiento que dispara solo. Al final vas a poder:

  • Tomar fotos a resolución completa del sensor y entender por qué la resolución que pides cambia el ángulo de visión.
  • Grabar video MP4 que se reproduce en cualquier teléfono, no un .h264 que solo abre VLC.
  • Transmitir la imagen en vivo al navegador de cualquier equipo de tu red.
  • Entregarle cada fotograma a OpenCV como array de NumPy y detectar movimiento sin usar inteligencia artificial.

Todos los ejemplos corren sin pantalla conectada, por SSH.

Picamera2, libcamera y rpicam: quién hace qué

Antes de escribir código conviene separar tres capas que suelen confundirse, porque los mensajes de error vienen de distintos niveles y apuntar al equivocado te hace perder una tarde:

  • libcamera es la biblioteca que habla con el sensor y hace el procesamiento de imagen: exposición, balance de blancos, reducción de ruido. Es la que escribe esas líneas INFO en el terminal.
  • rpicam apps (rpicam-hello, rpicam-still, rpicam-vid) son programas de línea de comandos construidos sobre libcamera. Sirven para probar rápido.
  • Picamera2 es la interfaz Python hacia esa misma libcamera. Su API es deliberadamente corta: abrir la cámara, configurarla, iniciarla, pedir la imagen.

Si vienes de tutoriales antiguos, hay algo que tienes que saber de entrada: la biblioteca picamera (sin el 2) ya no funciona con el stack actual. El código que empieza con from picamera import PiCamera no se puede reutilizar. Los conceptos se parecen, la API es otra. raspistill y raspivid tampoco existen más.

Picamera2 viene preinstalada en Raspberry Pi OS con escritorio, es software libre y corre en todos los modelos, del Zero al Pi 5.

Lo que necesitas antes de escribir la primera línea

  • Un Raspberry Pi. Picamera2 corre en cualquier modelo, pero para video Full HD y procesamiento con OpenCV un Pi 4 o Pi 5 trabaja mucho más holgado. Los ejemplos de esta guía fueron probados sobre un Pi 3 con un módulo IMX219.
  • Un módulo de cámara con conector CSI. El Camera Module 3 trae autofoco motorizado; el Camera Module 2 y los clones compatibles (como el IMX219 NoIR de 8 MP) tienen foco fijo. Una webcam USB no sirve acá: esa se controla directamente con OpenCV, no con Picamera2.
  • En el Pi 5, el cable adaptador CSI de 22 a 15 pines. Los puertos de cámara del Pi 5 son más chicos y no aceptan el cable estándar de los módulos. Es el error de compra más frecuente.
  • Raspberry Pi OS de 64 bits actualizado. Da lo mismo Desktop o Lite; la diferencia está en la instalación y la vemos en un minuto.
  • Acceso por SSH o un terminal en el Pi.

Verifica que la cámara existe antes de culpar a Python

Este paso se salta siempre y es el que más tiempo ahorra. Antes de importar nada, pregúntale al sistema si ve el sensor:

Bash
rpicam-hello --list-cameras

Si la respuesta es No cameras available!, Picamera2 tampoco va a poder hacer nada: el problema está en el cable, en el puerto o en config.txt, y hay que resolverlo ahí. Cuando la cámara sí está conectada, la salida se ve así:

Text
Available cameras
-----------------
0 : imx219 [3280x2464 10-bit RGGB] (/base/soc/i2c0mux/i2c@1/imx219@10)
    Modes: 'SRGGB10_CSI2P' : 640x480 [200.16 fps - (1000, 752)/1280x960 crop]
                             1640x1232 [81.07 fps - (0, 0)/3280x2464 crop]
                             1920x1080 [47.57 fps - (680, 692)/1920x1080 crop]
                             3280x2464 [21.19 fps - (0, 0)/3280x2464 crop]

Esa lista de Modes no es decorativa: son los únicos modos de operación que el sensor conoce de verdad, y más abajo vas a ver por qué determinan el resultado de tus fotos.

Instalación: apt sí, pip no

En Raspberry Pi OS con escritorio ya está instalada. Un par de líneas te dicen si la tienes y en qué versión:

Bash
python3 -c "import picamera2; print('ok')"
dpkg -l python3-picamera2 | grep ^ii

Si está, la primera línea responde ok y la segunda muestra el paquete:

Text
ok
ii  python3-picamera2  0.3.37-1  all  Libcamera based python library

Si falta, la primera termina en ModuleNotFoundError: No module named 'picamera2' y la segunda queda vacía.

En Raspberry Pi OS Lite hay que instalarla, y con apt, no con pip. Raspberry Pi lo recomienda de forma explícita: solo el paquete de apt está garantizado para coincidir con la versión de libcamera del sistema. Instalar Picamera2 con pip sobre una libcamera distinta es la receta para errores que no se entienden. La opción --no-install-recommends deja fuera las dependencias de Qt y escritorio, que no necesitas si trabajas sin pantalla:

Bash
sudo apt update
sudo apt install -y python3-picamera2 --no-install-recommends

Para la parte de visión por computador necesitas además OpenCV, la biblioteca open source más usada para procesamiento de imagen. Trae resuelto todo lo que va más allá de "guardar la foto": convertir espacios de color, encontrar bordes y contornos, comparar imágenes, reconocer rostros u objetos, dibujar texto y formas. En Python el módulo se llama cv2, y sus imágenes son simplemente arrays de NumPy, que es exactamente el formato que entrega Picamera2. También por apt:

Bash
sudo apt install -y python3-opencv

Tu primera foto en ocho líneas

Todo script de Picamera2 sigue el mismo patrón de cuatro pasos: crear el objeto de cámara, elegir y aplicar una configuración, iniciar, pedir la imagen. Guarda esto como photo.py:

Python
from picamera2 import Picamera2

picam2 = Picamera2()
config = picam2.create_still_configuration()
picam2.configure(config)
picam2.start()
picam2.capture_file("photo.jpg")
picam2.stop()
print("saved: photo.jpg")
Bash
python3 photo.py

Al cabo de un segundo tienes photo.jpg en resolución completa del sensor: con el IMX219 son 3280 × 2464 píxeles, algo más de 1,9 MB. Las muchas líneas INFO que libcamera escribe en el terminal son normales, no son errores. Si te molestan, define la variable de entorno LIBCAMERA_LOG_LEVELS=*:ERROR antes del import.

Primera foto tomada con Picamera2: una figura de Mario frente a un mueble blanco, resultado de capture_file()

Dos detalles que vale la pena mirar. capture_file() decide el formato por la extensión del nombre de archivo: .jpg, .png o .bmp. Y entre start() y la captura, Picamera2 le da por su cuenta unos cuantos fotogramas al sensor para que la exposición y el balance de blancos se estabilicen. No tienes que esperar a mano como en la biblioteca antigua.

Configuraciones: preview, still y video

Una configuración define qué flujos de imagen entrega la cámara, en qué tamaño y en qué formato. Picamera2 trae tres plantillas que se diferencian sobre todo en resolución y cantidad de buffers:

Plantilla Stream principal (main) Buffers Pensada para
create_preview_configuration() 640 × 480, XBGR8888 4 Vista previa, evaluación rápida, detección de movimiento
create_still_configuration() resolución completa, BGR888 1 Fotos individuales en la mejor calidad
create_video_configuration() 1280 × 720, XBGR8888, 30 fps 6 Grabación de video y streaming

Las plantillas son puntos de partida, no límites: cualquier valor se puede sobrescribir por argumento. Además del stream principal main puedes pedir un segundo stream más chico llamado lores. Ese vale oro cuando quieres guardar la imagen completa y al mismo tiempo calcular sobre una copia pequeña, que es justo lo que necesita la detección de movimiento más abajo:

Python
from picamera2 import Picamera2

picam2 = Picamera2()
config = picam2.create_still_configuration(
    main={"size": (1640, 1232)},
    lores={"size": (320, 240), "format": "YUV420"},
)
picam2.configure(config)
print(picam2.camera_configuration()["main"])
print(picam2.camera_configuration()["lores"])

Modos del sensor: resolución contra ángulo de visión

Acá está el detalle que explica la mitad de las decepciones con la cámara del Pi. El sensor conoce solo unos pocos modos fijos de operación, y libcamera elige automáticamente el que mejor calce con el tamaño que pediste. Para el IMX219 son estos:

Modo del sensor Cuadros por segundo Recorte
640 × 480 200 fps muy recortado (1280 × 960 desde el centro)
1640 × 1232 81 fps sensor completo, agrupado 2:1
1920 × 1080 47 fps recortado a 16:9
3280 × 2464 21 fps sensor completo

De ahí sale una regla práctica: si quieres el ángulo de visión completo, usa 1640 × 1232 (rápido) o la resolución completa (lenta). 1920 × 1080 suena a Full HD, pero en el IMX219 recorta un trozo bastante más cerrado desde el centro del sensor: la imagen se ve "con zoom" sin que hayas pedido zoom.

Comparación de dos modos del sensor IMX219: a la izquierda 1640 × 1232 con el ángulo de visión completo, a la derecha 1920 × 1080 con un recorte bastante más cerrado

En el Camera Module 3, con sensor IMX708, los modos son otros pero el principio es idéntico (estos valores salen del driver del sensor y de la salida de rpicam-hello --list-cameras):

Modo del sensor (IMX708) Cuadros por segundo Recorte
1536 × 864 120 fps recortado (3072 × 1728 desde el centro), agrupado 2:1
2304 × 1296 56 fps sensor completo, agrupado 2:1
4608 × 2592 14 fps sensor completo

Para video y streaming en el Camera Module 3, el punto dulce es 2304 × 1296: ángulo completo a 56 fps. La resolución máxima de 4608 × 2592, con sus 14 fps, sirve solo para fotos.

Controlar la cámara en marcha

Todo lo que afecta a la cámara en tiempo de ejecución pasa por set_controls() con un diccionario. Los nombres son los de libcamera; los más importantes son ExposureTime (en microsegundos), AnalogueGain, AwbMode, ColourGains, y además Brightness, Contrast, Saturation y Sharpness. Lo que la cámara está haciendo realmente lo lees con capture_metadata(), y esa es la herramienta para averiguar por qué una imagen sale oscura o con dominante de color.

Exposición manual

Python
import time
from picamera2 import Picamera2

picam2 = Picamera2()
picam2.configure(picam2.create_still_configuration(main={"size": (1640, 1232)}))
picam2.start()
time.sleep(1)

meta = picam2.capture_metadata()
print(f"auto: {meta['ExposureTime']} us, gain {meta['AnalogueGain']:.2f}, lux {meta['Lux']:.0f}")
picam2.capture_file("auto.jpg")

# desactiva la exposicion automatica y fija los valores
picam2.set_controls({"AeEnable": False, "ExposureTime": 10000, "AnalogueGain": 1.0})
time.sleep(0.5)   # espera unos frames hasta que los valores nuevos tomen efecto
picam2.capture_file("manual_10ms.jpg")

picam2.set_controls({"ExposureTime": 60000, "AnalogueGain": 4.0})
time.sleep(0.5)
picam2.capture_file("manual_60ms.jpg")
picam2.stop()

En una prueba con luz de interior tenue (unos 50 lux), el automático eligió 66 ms y ganancia 8. Ese ya es el tiempo de exposición más largo que el sensor permite en ese modo: de ahí en adelante solo queda subir la ganancia, y más ganancia es más ruido. La imagen de 10 ms sale casi negra, como era de esperar; la de 60 ms con ganancia 4 queda utilizable.

Dos tomas con exposición manual: a la izquierda 10 ms y ganancia 1,0 casi negra, a la derecha 60 ms y ganancia 4,0 bien expuesta

El medio segundo de espera después de set_controls() no es superstición: los valores nuevos recién rigen después de algunos fotogramas, y un capture_file() inmediato te devuelve todavía la imagen anterior. Si tus fotos "no obedecen", esta es casi siempre la causa.

Balance de blancos

Python
from libcamera import controls

# presets: Auto, Tungsten, Fluorescent, Indoor, Daylight, Cloudy
picam2.set_controls({"AwbMode": controls.AwbModeEnum.Tungsten})

# o ganancias de color fijas (rojo, azul) - apaga el AWB automaticamente
picam2.set_controls({"ColourGains": (1.6, 1.4)})

# volver al balance de blancos automatico
picam2.set_controls({"AwbEnable": True})

Las enumeraciones de estos presets vienen de libcamera.controls, no de Picamera2, y por eso hace falta el import extra. Las ganancias de color fijas son la opción correcta para timelapse y para todo lo que después vaya a comparar imágenes: si dejas el balance en automático, se mueve de foto en foto y la secuencia parpadea.

Cuatro tomas de la misma escena con distinto balance de blancos: AwbMode Auto, Tungsten, Daylight y ColourGains fijos

Brillo, contraste, saturación y nitidez

Estos cuatro controles también se fijan con set_controls() y actúan dentro del procesamiento de imagen de la cámara, no sobre el archivo ya guardado. Un detalle útil: Saturation: 0 te entrega una imagen en escala de grises limpia directamente desde la cámara, sin conversión posterior.

Cuatro tomas con distintos ajustes de imagen: estándar, contraste y saturación 1,6, saturación 0 en escala de grises, y nitidez 4,0

Autofoco: solo en el Camera Module 3

El Camera Module 3 trae un motor de enfoque que se controla con tres controles (esto sigue el manual oficial de Picamera2; un IMX219 no tiene motor de foco):

Python
from libcamera import controls

# autofoco continuo (para video y streaming)
picam2.set_controls({"AfMode": controls.AfModeEnum.Continuous})

# enfoca una vez y dispara - autofocus_cycle() espera el resultado
picam2.set_controls({"AfMode": controls.AfModeEnum.Auto})
success = picam2.autofocus_cycle()
picam2.capture_file("focused.jpg")

# foco manual en dioptrias: 0 = infinito, 10 = unos 10 cm
picam2.set_controls({"AfMode": controls.AfModeEnum.Manual, "LensPosition": 2.0})

En módulos sin motor de foco. Camera Module 2, HQ Camera, la mayoría de los clones. estos controles simplemente se ignoran. El Camera Module 2 y sus compatibles tienen focal fija que enfoca nítido desde cerca de un metro; para primeros planos, el Camera Module 3 es la mejor opción.

La imagen sale de cabeza

Si montaste la cámara invertida, no gires la imagen después: deja que lo haga libcamera. La rotación es parte de la configuración y vale para todos los streams a la vez, también para video y streaming:

Python
from libcamera import Transform
from picamera2 import Picamera2

picam2 = Picamera2()
config = picam2.create_still_configuration(transform=Transform(hflip=1, vflip=1))
picam2.configure(config)

hflip espeja en horizontal, vflip en vertical, y los dos juntos son el giro de 180 grados. Una rotación de 90 grados el sensor no la puede hacer: esa la tienes que aplicar sobre la imagen, por ejemplo con OpenCV.

La misma escena dos veces: a la izquierda sin Transform la imagen está de cabeza, a la derecha con hflip y vflip queda derecha

Módulos NoIR: el tono rosado no es una falla

Si tu módulo es la variante NoIR. sin filtro de corte infrarrojo. vas a notar que todas las fotos bajo luz artificial salen con un tono rosado fuerte. No es un defecto: es física. Sin filtro, la luz infrarroja llega al canal rojo y lo satura.

libcamera trae archivos de tuning específicos para este caso, pero elige automáticamente siempre el archivo normal según el nombre del sensor, o sea imx219.json en vez de imx219_noir.json. El correcto se indica al crear el objeto de cámara:

Python
from picamera2 import Picamera2

tuning = Picamera2.load_tuning_file("imx219_noir.json")
picam2 = Picamera2(tuning=tuning)

Módulo NoIR bajo luz artificial: a la izquierda con el archivo de tuning estándar un fuerte tono rosado, a la derecha con imx219_noir.json colores neutros

La diferencia es clara: con el archivo NoIR los colores bajo luz artificial quedan neutros en vez de magenta, y los metadatos muestran que la ganancia de azul baja de 1,07 a 0,75. Para ver qué archivos de tuning hay disponibles, lista ls /usr/share/libcamera/ipa/rpi/vc4/ (en el Pi 5 el directorio es pisp en lugar de vc4). Para el Camera Module 3 NoIR el archivo se llama imx708_noir.json.

Con luz de día el tono nunca desaparece del todo, y tampoco es para eso que existe el módulo NoIR: su razón de ser son las tomas nocturnas con iluminación infrarroja.

Grabar video: MP4 que se reproduce en cualquier parte

Para video entran dos piezas nuevas: un encoder, que comprime las imágenes, y un output, que escribe el resultado en alguna parte. El ejemplo más simple graba cinco segundos como flujo H.264 crudo:

Python
import time
from picamera2 import Picamera2
from picamera2.encoders import H264Encoder
from picamera2.outputs import FileOutput

picam2 = Picamera2()
picam2.configure(picam2.create_video_configuration(main={"size": (1280, 720)}))

encoder = H264Encoder(bitrate=5_000_000)
picam2.start_recording(encoder, FileOutput("clip.h264"))
time.sleep(5)
picam2.stop_recording()

El problema es que un archivo .h264 es solo el flujo de video, sin contenedor. VLC lo reproduce; la mayoría del resto de los reproductores y cualquier teléfono, no. Para obtener un MP4 de verdad basta con cambiar el output.

PyavOutput en vez de FfmpegOutput

Picamera2 ofrece dos clases para esto, y la elección no da lo mismo. FfmpegOutput le pasa el flujo a un ffmpeg externo que vuelve a poner marcas de tiempo: el resultado típico es que cinco segundos de grabación terminan en un archivo marcado como 60 fps que dura apenas cuatro. PyavOutput escribe el contenedor directamente desde Python y conserva las marcas de tiempo reales de la cámara, así que la duración y la tasa de cuadros quedan correctas. Los propios desarrolladores de Picamera2 describen ese problema como la razón para haber construido la clase nueva.

Python
import time
from picamera2 import Picamera2
from picamera2.encoders import H264Encoder
from picamera2.outputs import PyavOutput

picam2 = Picamera2()
picam2.configure(picam2.create_video_configuration(main={"size": (1280, 720)}))

picam2.start_recording(H264Encoder(bitrate=5_000_000), PyavOutput("clip.mp4"))
time.sleep(5)
picam2.stop_recording()

Si solo necesitas un clip rápido de duración fija, hay una llamada única que también entrega un MP4:

Python
picam2.start_and_record_video("clip.mp4", duration=10)

Encoder por hardware o por software: Pi 4 contra Pi 5

El nombre de clase H264Encoder es el mismo en todos los modelos, pero detrás no hay la misma tecnología, y esto sorprende a mucha gente que actualiza su Pi:

  • Del Pi Zero al Pi 4, Picamera2 usa el encoder por hardware del chip de video (/dev/video11, driver bcm2835-codec). La CPU queda prácticamente libre.
  • El Pi 5 ya no tiene encoder H.264 por hardware. Detrás del mismo nombre, Picamera2 pone automáticamente un encoder por software basado en libav. Tu código corre igual, sin cambiar una línea, pero ahora cuesta tiempo de CPU.

Para 1080p a 30 fps el Pi 5 está dimensionado incluso por software, según Raspberry Pi. En un Pi 3, en cambio, 720p es el máximo razonable.

Livestream MJPEG al navegador

Una imagen que puedas abrir desde cualquier equipo de la red es el proyecto de cámara más pedido, y con Picamera2 son unas 60 líneas. El principio: un JpegEncoder entrega imágenes JPEG de forma continua y un pequeño servidor HTTP de la biblioteca estándar de Python se las manda al navegador como flujo MJPEG. Guarda el script como stream.py:

Python
import io
import logging
import socketserver
from http import server
from threading import Condition

from picamera2 import Picamera2
from picamera2.encoders import JpegEncoder
from picamera2.outputs import FileOutput

PAGE = """\
<html>
<head><title>Picamera2 live stream</title></head>
<body style="margin:0;background:#111">
<img src="stream.mjpg" style="max-width:100%;height:auto;display:block;margin:auto" />
</body>
</html>
"""


class StreamingOutput(io.BufferedIOBase):
    def __init__(self):
        self.frame = None
        self.condition = Condition()

    def write(self, buf):
        with self.condition:
            self.frame = buf
            self.condition.notify_all()


class StreamingHandler(server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(301)
            self.send_header('Location', '/index.html')
            self.end_headers()
        elif self.path == '/index.html':
            content = PAGE.encode('utf-8')
            self.send_response(200)
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', len(content))
            self.end_headers()
            self.wfile.write(content)
        elif self.path == '/stream.mjpg':
            self.send_response(200)
            self.send_header('Age', 0)
            self.send_header('Cache-Control', 'no-cache, private')
            self.send_header('Pragma', 'no-cache')
            self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')
            self.end_headers()
            try:
                while True:
                    with output.condition:
                        output.condition.wait()
                        frame = output.frame
                    self.wfile.write(b'--FRAME\r\n')
                    self.send_header('Content-Type', 'image/jpeg')
                    self.send_header('Content-Length', len(frame))
                    self.end_headers()
                    self.wfile.write(frame)
                    self.wfile.write(b'\r\n')
            except Exception as e:
                logging.warning('Removed streaming client %s: %s', self.client_address, str(e))
        else:
            self.send_error(404)
            self.end_headers()


class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True


picam2 = Picamera2()
picam2.configure(picam2.create_video_configuration(main={"size": (1280, 720)}))
output = StreamingOutput()
picam2.start_recording(JpegEncoder(), FileOutput(output))

try:
    address = ('', 8000)
    server = StreamingServer(address, StreamingHandler)
    print("Stream running on http://<pi-ip>:8000 - Ctrl+C to stop")
    server.serve_forever()
finally:
    picam2.stop_recording()

Después de python3 stream.py abres http://<IP-del-Pi>:8000 en el navegador.

El stream va sin cifrado y sin contraseña: está pensado para la red de tu casa, no para exponerlo a internet. Si quieres verlo desde afuera, usa una VPN o Raspberry Pi Connect, no abras el puerto en el router.

En un Pi 3 la carga para 720p con este script se mantiene en un rango razonable. El JpegEncoder trabaja por software, así que para varios espectadores simultáneos o para Full HD conviene un Pi 4 o Pi 5.

Que el stream arranque solo al encender

Algo que el tutorial original no cubre y que vas a necesitar en cuanto el proyecto salga del escritorio: dejar el stream corriendo como servicio, para que vuelva solo después de un corte de luz. Crea /etc/systemd/system/picam-stream.service:

Ini
[Unit]
Description=Picamera2 MJPEG stream
After=network-online.target

[Service]
User=pi
WorkingDirectory=/home/pi
ExecStart=/usr/bin/python3 /home/pi/stream.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Y lo activas:

Bash
sudo systemctl daemon-reload
sudo systemctl enable --now picam-stream.service
sudo systemctl status picam-stream.service

Ojo con un detalle que cuesta depurar: solo un proceso puede tener la cámara abierta a la vez. Si el servicio está corriendo y después ejecutas otro script a mano, el segundo falla. Detén el servicio con sudo systemctl stop picam-stream.service antes de probar otra cosa.

Cada fotograma como array de NumPy: OpenCV

Esta es la verdadera razón para usar Python en vez de las herramientas rpicam: capture_array() entrega la imagen como array de NumPy, una tabla de números con forma (alto, ancho, 3), un valor de 8 bits por canal de color y píxel. Con eso queda abierto todo el procesamiento de imagen, y la herramienta para eso es OpenCV, que recibe esos arrays y calcula sobre ellos sin que tengas que escribir ciclos por píxel.

Como primer ejemplo convertimos una imagen a escala de grises y dejamos que OpenCV encuentre los bordes con el algoritmo de Canny. El resultado es una imagen en blanco y negro donde solo quedan los contornos. Esas mismas tres líneas son la entrada a todo lo demás: contar contornos, reconocer colores, leer códigos QR o alimentar un modelo de reconocimiento facial.

Python
import cv2
from picamera2 import Picamera2

picam2 = Picamera2()
picam2.configure(picam2.create_still_configuration(main={"size": (1640, 1232)}))
picam2.start()

frame = picam2.capture_array("main")
print(frame.shape, frame.dtype)   # (1232, 1640, 3) uint8

gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
edges = cv2.Canny(gray, 100, 200)
cv2.imwrite("edges.jpg", edges)
picam2.stop()

Imagen de bordes en blanco y negro de una figura de Mario, generada con el algoritmo Canny de OpenCV a partir de un array de Picamera2

La trampa RGB/BGR

Acá hay una trampa que atrapa a todos los que guardan imágenes en color con OpenCV: la configuración estándar reporta el formato BGR888, pero el array llega en orden RGB. Es que libcamera nombra sus formatos al revés. Si guardas directo con cv2.imwrite() —que espera BGR. el rojo y el azul se intercambian: un gorro rojo sale azul.

Para escala de grises, entonces, COLOR_RGB2GRAY es lo correcto. Y si escribes o muestras imágenes en color con OpenCV, conviertes antes:

Python
bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
cv2.imwrite("colour.jpg", bgr)

La misma toma dos veces: a la izquierda con los canales de color intercambiados y el gorro de la figura azul, a la derecha con los colores correctos tras la conversión

La alternativa es pedir el array directamente en el orden que OpenCV quiere. El formato entonces se llama, haciendo juego con la confusión, RGB888: main={"size": (1640, 1232), "format": "RGB888"}.

Detección de movimiento con el stream lores

Para una detección de movimiento sencilla no necesitas inteligencia artificial: comparas dos imágenes consecutivas en escala de grises y, si cambiaron suficientes píxeles en promedio, pasó algo.

Lo importante es dónde haces ese cálculo. Comparar dos imágenes de 1640 × 1232 significa recorrer unos 2 millones de píxeles por cuadro; hacerlo sobre el stream lores de 320 × 240 son 76.800, unas 26 veces menos trabajo. Por eso el patrón correcto es evaluar en lores y, solo cuando se dispara, guardar el cuadro completo desde main.

El truco con YUV420: las primeras filas del array son la luminancia (el plano Y), o sea una imagen en escala de grises directa, sin conversión:

Python
import time
import numpy as np
from picamera2 import Picamera2

picam2 = Picamera2()
config = picam2.create_still_configuration(
    main={"size": (1640, 1232)},
    lores={"size": (320, 240), "format": "YUV420"},
)
picam2.configure(config)
picam2.start()
time.sleep(1)

THRESHOLD = 6.0         # diferencia media de pixeles que cuenta como movimiento
previous = None
while True:
    yuv = picam2.capture_array("lores")
    current = yuv[:240, :320].astype(np.int16)   # plano Y = imagen en escala de grises
    if previous is not None:
        diff = np.abs(current - previous).mean()
        if diff > THRESHOLD:
            print(f"motion detected (difference {diff:.1f})")
            picam2.capture_file(time.strftime("motion_%Y%m%d_%H%M%S.jpg"))
            time.sleep(2)               # pausa para que no dispare en cada frame
    previous = current
    time.sleep(0.1)

El umbral lo tienes que medir para tu montaje, porque depende de la luz. En una escena quieta la diferencia se mantuvo constante entre 2,1 y 2,6 (ese es el ruido del sensor con unos 50 lux); agitar la mano frente a la cámara dio valores entre 15 y 54. El 6 queda con holgura hacia ambos lados. Con mucha luz el sensor hace menos ruido y puedes bajarlo. Antes de fijar el umbral, deja correr el script una vez con print(diff) en el ciclo y lee los números reales de tu escena.

Foto en resolución completa disparada por la detección de movimiento: una figura de Mario frente a un mueble blanco

Cuánto espacio va a ocupar esto

Una cuenta que conviene hacer antes de dejar el detector corriendo una semana, y que el tutorial original no incluye. Una foto del IMX219 a resolución completa pesa alrededor de 1,9 MB. Con la pausa de 2 segundos del script, un evento de movimiento sostenido puede generar hasta 30 fotos por minuto, o sea unos 57 MB por minuto en el peor caso.

En la práctica, un pasillo con tránsito normal produce entre 50 y 300 disparos al día: entre 95 MB y 570 MB diarios. Con una microSD de 32 GB. de la cual el sistema ya ocupa unos 3 GB. tienes entre 50 y 300 días de margen, pero hay dos cosas que conviene resolver desde el principio:

  • Guarda a resolución reducida si no necesitas el detalle: pasar main a 1640 × 1232 baja el archivo a cerca de 600 KB, un tercio del espacio.
  • No dejes que la tarjeta se llene. Una microSD sin espacio no solo detiene el script: puede corromper el sistema de archivos. En la sección de variantes hay un control de espacio libre de cuatro líneas.

Timelapse con exposición congelada

rpicam-still --timelapse también hace timelapse, pero en tu propio script tienes el control sobre lo que hace que un timelapse se vea bien: exposición y balance de blancos congelados, para que la secuencia no parpadee. Para eso lees los valores del automático una vez que se estabilizó y los escribes de vuelta como controles fijos:

Python
import time
from picamera2 import Picamera2

INTERVAL = 30      # segundos entre dos fotogramas
FRAMES = 240       # 240 fotogramas = 2 horas, dan 10 s de video a 24 fps

picam2 = Picamera2()
picam2.configure(picam2.create_still_configuration(main={"size": (1920, 1440)}))
picam2.start()
time.sleep(2)

meta = picam2.capture_metadata()
picam2.set_controls({
    "AeEnable": False, "AwbEnable": False,
    "ExposureTime": meta["ExposureTime"], "AnalogueGain": meta["AnalogueGain"],
    "ColourGains": meta["ColourGains"],
})

for i in range(FRAMES):
    start = time.time()
    picam2.capture_file(f"tl_{i:04d}.jpg")
    time.sleep(max(0, INTERVAL - (time.time() - start)))

Con los valores del ejemplo. un cuadro cada 30 segundos, 240 cuadros. cubres dos horas de grabación y obtienes unos 10 segundos de video a 24 fps.

Errores comunes y cómo salir de ellos

Síntoma Causa probable Solución
No cameras available! Cable mal puesto, puerto equivocado o, en Pi 5, cable de 15 pines en vez del adaptador 22 a 15 Revisa el cable y el adaptador antes de tocar software
ModuleNotFoundError: No module named 'picamera2' Estás en Raspberry Pi OS Lite, o dentro de un venv sin acceso a los paquetes del sistema Instala con apt; para venv usa python3 -m venv --system-site-packages
La foto no refleja el set_controls() que acabas de aplicar Capturaste antes de que los valores nuevos tomaran efecto Espera medio segundo después de set_controls()
El rojo sale azul al guardar con OpenCV El array llega en RGB y cv2.imwrite() espera BGR Convierte con cv2.COLOR_RGB2BGR o pide el formato RGB888
La imagen se ve "con zoom" sin haberlo pedido Pediste 1920 × 1080 y el sensor recorta desde el centro Usa 1640 × 1232 para el ángulo completo
Todo sale con tono rosado Módulo NoIR con el archivo de tuning estándar Carga imx219_noir.json con load_tuning_file()
El segundo script falla al abrir la cámara Otro proceso (o un servicio systemd) ya la tiene tomada Detén el otro proceso; la cámara es de un solo usuario
El .h264 no se reproduce en el teléfono Es un flujo crudo sin contenedor Graba con PyavOutput a .mp4

Variantes y mejoras

El detector de movimiento de más arriba guarda fotos en el directorio actual y no le avisa a nadie. Tres extensiones concretas para convertirlo en algo que sirva de verdad:

1. Aviso por Telegram cuando se dispara. Un bot de Telegram es la forma más simple de recibir la foto en el teléfono sin montar servidores ni abrir puertos. Crea el bot con @BotFather, anota el token, y agrega esta función al script de movimiento para llamarla justo después de capture_file():

Python
import requests

BOT_TOKEN = "123456789:AA-token-que-te-da-BotFather"
CHAT_ID = "987654321"

def avisar_telegram(ruta_foto, texto):
    url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto"
    with open(ruta_foto, "rb") as foto:
        requests.post(
            url,
            data={"chat_id": CHAT_ID, "caption": texto},
            files={"photo": foto},
            timeout=20,
        )

Necesitas sudo apt install -y python3-requests. Envuelve la llamada en un try/except: si se cae el wifi, el detector tiene que seguir grabando igual, no morirse por un timeout.

2. Guardar en una microSD aparte y no llenar el sistema. Monta una segunda tarjeta (o un pendrive) en /mnt/capturas y revisa el espacio antes de cada escritura. Así el detector deja de grabar antes de corromper nada:

Python
import shutil

DESTINO = "/mnt/capturas"
MINIMO_MB = 200

def hay_espacio():
    libre_mb = shutil.disk_usage(DESTINO).free / (1024 * 1024)
    return libre_mb > MINIMO_MB

3. Visión nocturna de verdad con el módulo NoIR. El NoIR sin iluminación no ve en la oscuridad: solo deja pasar el infrarrojo. Agregando un foco de LEDs infrarrojos de 850 nm apuntando a la escena, el detector funciona con la luz apagada y en imagen monocroma limpia. Combínalo con Saturation: 0 para obtener escala de grises directa desde la cámara y ahorrarte la conversión en OpenCV.

Personalización para Chile

El tutorial original enlaza tiendas alemanas. En Chile puedes armar el proyecto completo en MechatronicStore; estos son los componentes equivalentes y a qué prestar atención al elegirlos:

  • Raspberry Pi 5 o Raspberry Pi 4. el Pi 4 rinde muy bien para foto, stream 720p y detección de movimiento, y tiene encoder H.264 por hardware. El Pi 5 es más rápido en OpenCV, pero recuerda que codifica video por software.
  • Raspberry Pi Camera Module 3. la opción recomendada si necesitas autofoco o primeros planos.
  • Cámara IMX219 NoIR. el módulo con el que se probó este tutorial. Es la alternativa económica y la única de las dos que sirve para visión nocturna con iluminación infrarroja.
  • Cable adaptador CSI de 22 a 15 pines. imprescindible si compras un Pi 5. Los módulos de cámara vienen con el cable de 15 pines, que no entra en el puerto del Pi 5. Es el componente que más se olvida en el carro.
  • MicroSD de 32 GB clase 10 / A1. con menos de 32 GB te queda muy poco margen para las capturas del detector de movimiento.
  • Fuente USB-C de 5 V. usa una fuente de verdad, no un cargador de teléfono cualquiera: con alimentación insuficiente la cámara es de lo primero que falla, muchas veces con errores que parecen de software.

Equivalencias: donde el tutorial original menciona "Arducam 8MP IMX219 NoIR", cualquier módulo IMX219 NoIR del catálogo cumple exactamente la misma función, porque el sensor es el mismo y el archivo de tuning imx219_noir.json aplica igual. Los precios y SKU vigentes los ves en la ficha de cada producto en mechatronicstore.cl.

Recursos

Versión chilena con componentes en stock local en MechatronicStore, inspirada en el tutorial de raspberry.tips y extendida con el arranque por systemd, el cálculo de almacenamiento, una tabla de errores comunes y las variantes de aviso por Telegram y visión nocturna.