Pular para o conteúdo
🧠Inteligência em Código
📚 Tutoriais17 min de leitura

Visão Computacional com OpenCV e Python: Guia Prático

Aprenda visão computacional do básico ao avançado — instalação do OpenCV, manipulação de imagens, filtros, detecção de bordas, faces com Haar Cascades e DNN, e detecção de objetos com YOLO

#opencv#visao-computacional#python#deep-learning#yolo

Visão Computacional com OpenCV e Python: Guia Prático

Visão computacional é o campo da inteligência artificial que ensina computadores a "ver" — interpretar e extrair informação de imagens e vídeos. É a tecnologia por trás de carros autônomos, reconhecimento facial, diagnósticos médicos por imagem, sistemas de vigilância e muito mais.

O OpenCV (Open Source Computer Vision Library) é a biblioteca mais popular e completa para visão computacional, com mais de 2.500 algoritmos otimizados. Neste tutorial, vamos explorar desde o básico — leitura e manipulação de imagens — até técnicas avançadas como detecção de faces com redes neurais e detecção de objetos com YOLO.


Instalação e Configuração

Instalando o OpenCV

# Instalação básica
pip install opencv-python

# Instalação com módulos extras (recomendado)
pip install opencv-contrib-python

# Bibliotecas auxiliares
pip install numpy matplotlib pillow

Verificando a Instalação

import cv2
print(f"OpenCV versão: {cv2.__version__}")
print(f"Módulos disponíveis: {[m for m in dir(cv2) if not m.startswith('_')][:10]}...")

Estrutura do Projeto

visao-computacional/
├── src/
│   ├── basics.py             # Operações básicas
│   ├── filters.py            # Filtros e transformações
│   ├── edge_detection.py     # Detecção de bordas
│   ├── face_detection.py     # Detecção de faces
│   ├── object_detection.py   # Detecção de objetos (YOLO)
│   └── utils.py              # Utilitários
├── images/                    # Imagens de teste
├── models/                    # Modelos pré-treinados
├── output/                    # Resultados
└── requirements.txt

Parte 1: Operações Básicas com Imagens

Leitura, Exibição e Salvamento

"""Operações fundamentais com imagens no OpenCV."""

import cv2
import numpy as np
import matplotlib.pyplot as plt


def load_and_display(image_path: str) -> np.ndarray:
    """Carrega e exibe uma imagem."""
    # Carregar imagem (OpenCV usa BGR por padrão, não RGB!)
    img = cv2.imread(image_path)
    
    if img is None:
        raise FileNotFoundError(f"Imagem não encontrada: {image_path}")
    
    print(f"Dimensões: {img.shape}")  # (altura, largura, canais)
    print(f"Tipo: {img.dtype}")        # uint8 (0-255)
    print(f"Tamanho: {img.size} pixels")
    
    # Exibir com matplotlib (converter BGR -> RGB)
    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    plt.figure(figsize=(10, 8))
    plt.imshow(img_rgb)
    plt.title("Imagem Original")
    plt.axis("off")
    plt.show()
    
    return img


def save_image(img: np.ndarray, output_path: str) -> None:
    """Salva uma imagem em disco."""
    cv2.imwrite(output_path, img)
    print(f"Imagem salva em: {output_path}")


def create_blank_image(width: int, height: int, color: tuple = (0, 0, 0)) -> np.ndarray:
    """Cria uma imagem em branco (ou com cor sólida)."""
    img = np.full((height, width, 3), color, dtype=np.uint8)
    return img

Manipulação Básica

def basic_manipulations(img: np.ndarray) -> dict[str, np.ndarray]:
    """Demonstra manipulações básicas de imagem."""
    results = {}
    
    # 1. Redimensionar
    height, width = img.shape[:2]
    resized = cv2.resize(img, (width // 2, height // 2), interpolation=cv2.INTER_AREA)
    results["resized"] = resized
    
    # 2. Recortar (crop)
    h, w = img.shape[:2]
    cropped = img[h // 4 : 3 * h // 4, w // 4 : 3 * w // 4]
    results["cropped"] = cropped
    
    # 3. Rotacionar
    center = (width // 2, height // 2)
    matrix = cv2.getRotationMatrix2D(center, angle=45, scale=1.0)
    rotated = cv2.warpAffine(img, matrix, (width, height))
    results["rotated"] = rotated
    
    # 4. Espelhar
    flipped_h = cv2.flip(img, 1)    # Horizontal
    flipped_v = cv2.flip(img, 0)    # Vertical
    results["flipped_h"] = flipped_h
    results["flipped_v"] = flipped_v
    
    # 5. Converter para escala de cinza
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    results["gray"] = gray
    
    # 6. Ajustar brilho e contraste
    alpha = 1.3  # Contraste (1.0 = sem mudança)
    beta = 30    # Brilho (0 = sem mudança)
    adjusted = cv2.convertScaleAbs(img, alpha=alpha, beta=beta)
    results["brightness_contrast"] = adjusted
    
    return results


def draw_on_image(img: np.ndarray) -> np.ndarray:
    """Desenha formas e texto em uma imagem."""
    canvas = img.copy()
    
    # Retângulo
    cv2.rectangle(canvas, (50, 50), (200, 200), (0, 255, 0), 2)
    
    # Círculo
    cv2.circle(canvas, (300, 150), 80, (0, 0, 255), 3)
    
    # Linha
    cv2.line(canvas, (0, 0), (400, 300), (255, 0, 0), 2)
    
    # Texto
    cv2.putText(
        canvas,
        "OpenCV Python",
        (50, 350),
        cv2.FONT_HERSHEY_SIMPLEX,
        1.2,
        (255, 255, 255),
        2,
        cv2.LINE_AA,
    )
    
    return canvas

Espaços de Cores

def color_spaces(img: np.ndarray) -> dict[str, np.ndarray]:
    """Demonstra diferentes espaços de cores."""
    results = {}
    
    # BGR (padrão OpenCV)
    results["bgr"] = img
    
    # RGB
    results["rgb"] = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    
    # HSV (Hue, Saturation, Value)
    results["hsv"] = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    
    # LAB
    results["lab"] = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
    
    # Separar canais
    b, g, r = cv2.split(img)
    results["blue_channel"] = b
    results["green_channel"] = g
    results["red_channel"] = r
    
    return results


def color_segmentation(img: np.ndarray, color: str = "blue") -> np.ndarray:
    """Segmentação por cor usando espaço HSV."""
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    
    # Ranges de cores em HSV
    color_ranges = {
        "blue": ((100, 50, 50), (130, 255, 255)),
        "green": ((35, 50, 50), (85, 255, 255)),
        "red_low": ((0, 50, 50), (10, 255, 255)),
        "red_high": ((170, 50, 50), (180, 255, 255)),
        "yellow": ((20, 50, 50), (35, 255, 255)),
    }
    
    if color == "red":
        # Vermelho precisa de duas faixas (wraparound no H)
        mask1 = cv2.inRange(hsv, np.array(color_ranges["red_low"][0]),
                            np.array(color_ranges["red_low"][1]))
        mask2 = cv2.inRange(hsv, np.array(color_ranges["red_high"][0]),
                            np.array(color_ranges["red_high"][1]))
        mask = cv2.bitwise_or(mask1, mask2)
    else:
        lower, upper = color_ranges.get(color, color_ranges["blue"])
        mask = cv2.inRange(hsv, np.array(lower), np.array(upper))
    
    # Aplicar máscara
    result = cv2.bitwise_and(img, img, mask=mask)
    return result

Parte 2: Filtros e Transformações

Filtros de Suavização

"""Filtros de suavização (blur) e nitidez."""

import cv2
import numpy as np


def apply_blur_filters(img: np.ndarray) -> dict[str, np.ndarray]:
    """Aplica diferentes filtros de suavização."""
    results = {}
    
    # 1. Blur médio (averaging)
    results["average_blur"] = cv2.blur(img, (7, 7))
    
    # 2. Gaussian Blur (mais natural)
    results["gaussian_blur"] = cv2.GaussianBlur(img, (7, 7), 0)
    
    # 3. Median Blur (excelente para remover ruído salt-and-pepper)
    results["median_blur"] = cv2.medianBlur(img, 7)
    
    # 4. Bilateral Filter (suaviza mantendo bordas nítidas)
    results["bilateral"] = cv2.bilateralFilter(img, 9, 75, 75)
    
    return results


def apply_morphology(img: np.ndarray) -> dict[str, np.ndarray]:
    """Aplica operações morfológicas."""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
    
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    results = {}
    
    # Erosão: diminui regiões brancas
    results["erosion"] = cv2.erode(binary, kernel, iterations=1)
    
    # Dilatação: aumenta regiões brancas
    results["dilation"] = cv2.dilate(binary, kernel, iterations=1)
    
    # Opening: erosão seguida de dilatação (remove ruído pequeno)
    results["opening"] = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
    
    # Closing: dilatação seguida de erosão (fecha buracos pequenos)
    results["closing"] = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
    
    # Gradient: diferença entre dilatação e erosão (detecta bordas)
    results["gradient"] = cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, kernel)
    
    return results


def sharpen_image(img: np.ndarray) -> np.ndarray:
    """Aumenta a nitidez de uma imagem usando kernel de convolução."""
    kernel = np.array([
        [0, -1, 0],
        [-1, 5, -1],
        [0, -1, 0]
    ])
    return cv2.filter2D(img, -1, kernel)


def apply_thresholds(img: np.ndarray) -> dict[str, np.ndarray]:
    """Aplica diferentes técnicas de threshold (limiarização)."""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    results = {}
    
    # Threshold global simples
    _, results["binary"] = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
    
    # Threshold inverso
    _, results["binary_inv"] = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
    
    # Threshold de Otsu (automático)
    _, results["otsu"] = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    
    # Threshold adaptativo (melhor para iluminação desigual)
    results["adaptive_mean"] = cv2.adaptiveThreshold(
        gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2
    )
    
    results["adaptive_gaussian"] = cv2.adaptiveThreshold(
        gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2
    )
    
    return results

Parte 3: Detecção de Bordas e Contornos

Detectores de Bordas

"""Detecção de bordas com Sobel, Laplacian e Canny."""

import cv2
import numpy as np


def detect_edges(img: np.ndarray) -> dict[str, np.ndarray]:
    """Aplica diferentes detectores de bordas."""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # Suavizar para reduzir ruído
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    results = {}
    
    # 1. Sobel (detecta bordas em X e Y separadamente)
    sobel_x = cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize=3)
    sobel_y = cv2.Sobel(blurred, cv2.CV_64F, 0, 1, ksize=3)
    sobel_combined = cv2.magnitude(sobel_x, sobel_y)
    results["sobel"] = cv2.convertScaleAbs(sobel_combined)
    
    # 2. Laplacian (detecta bordas em todas as direções)
    laplacian = cv2.Laplacian(blurred, cv2.CV_64F)
    results["laplacian"] = cv2.convertScaleAbs(laplacian)
    
    # 3. Canny (o mais popular — multi-estágio, bordas finas)
    results["canny"] = cv2.Canny(blurred, 50, 150)
    
    # 4. Canny com auto-threshold
    median = np.median(blurred)
    lower = int(max(0, 0.7 * median))
    upper = int(min(255, 1.3 * median))
    results["canny_auto"] = cv2.Canny(blurred, lower, upper)
    
    return results


def find_and_draw_contours(img: np.ndarray) -> tuple[np.ndarray, list]:
    """Encontra e desenha contornos na imagem."""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    edges = cv2.Canny(blurred, 50, 150)
    
    # Encontrar contornos
    contours, hierarchy = cv2.findContours(
        edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )
    
    # Ordenar por área (maior primeiro)
    contours = sorted(contours, key=cv2.contourArea, reverse=True)
    
    # Desenhar contornos
    result = img.copy()
    cv2.drawContours(result, contours, -1, (0, 255, 0), 2)
    
    # Informações sobre cada contorno
    contour_info = []
    for i, contour in enumerate(contours[:10]):  # Top 10
        area = cv2.contourArea(contour)
        perimeter = cv2.arcLength(contour, True)
        x, y, w, h = cv2.boundingRect(contour)
        
        # Aproximação poligonal
        approx = cv2.approxPolyDP(contour, 0.02 * perimeter, True)
        n_vertices = len(approx)
        
        # Classificar forma
        if n_vertices == 3:
            shape = "triangulo"
        elif n_vertices == 4:
            aspect_ratio = w / float(h)
            shape = "quadrado" if 0.9 <= aspect_ratio <= 1.1 else "retangulo"
        elif n_vertices > 8:
            shape = "circulo"
        else:
            shape = f"poligono ({n_vertices} lados)"
        
        contour_info.append({
            "index": i,
            "area": area,
            "perimeter": round(perimeter, 2),
            "bounding_box": (x, y, w, h),
            "vertices": n_vertices,
            "shape": shape,
        })
        
        # Desenhar bounding box e label
        cv2.rectangle(result, (x, y), (x + w, y + h), (255, 0, 0), 2)
        cv2.putText(result, shape, (x, y - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
    
    print(f"Contornos encontrados: {len(contours)}")
    for info in contour_info:
        print(f"  #{info['index']}: {info['shape']}, área={info['area']:.0f}")
    
    return result, contour_info

Detecção de Linhas e Círculos (Hough Transform)

def detect_lines(img: np.ndarray) -> np.ndarray:
    """Detecta linhas usando Hough Transform."""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    edges = cv2.Canny(gray, 50, 150)
    
    result = img.copy()
    
    # Hough Lines Probabilístico (mais prático)
    lines = cv2.HoughLinesP(
        edges,
        rho=1,
        theta=np.pi / 180,
        threshold=100,
        minLineLength=50,
        maxLineGap=10,
    )
    
    if lines is not None:
        for line in lines:
            x1, y1, x2, y2 = line[0]
            cv2.line(result, (x1, y1), (x2, y2), (0, 0, 255), 2)
        print(f"Linhas detectadas: {len(lines)}")
    
    return result


def detect_circles(img: np.ndarray) -> np.ndarray:
    """Detecta círculos usando Hough Circle Transform."""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.medianBlur(gray, 5)
    
    result = img.copy()
    
    circles = cv2.HoughCircles(
        blurred,
        cv2.HOUGH_GRADIENT,
        dp=1,
        minDist=50,
        param1=100,
        param2=30,
        minRadius=10,
        maxRadius=200,
    )
    
    if circles is not None:
        circles = np.uint16(np.around(circles))
        for circle in circles[0, :]:
            center = (circle[0], circle[1])
            radius = circle[2]
            cv2.circle(result, center, radius, (0, 255, 0), 2)
            cv2.circle(result, center, 3, (0, 0, 255), -1)
        print(f"Círculos detectados: {len(circles[0])}")
    
    return result

Parte 4: Detecção de Faces

Haar Cascades (Método Clássico)

"""Detecção de faces com Haar Cascades e DNN."""

import cv2
import numpy as np
from pathlib import Path


def detect_faces_haar(img: np.ndarray) -> tuple[np.ndarray, list]:
    """
    Detecta faces usando Haar Cascade Classifier.
    Rápido mas menos preciso que métodos baseados em DNN.
    """
    # Carregar classificador pré-treinado (incluso no OpenCV)
    face_cascade = cv2.CascadeClassifier(
        cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
    )
    eye_cascade = cv2.CascadeClassifier(
        cv2.data.haarcascades + "haarcascade_eye.xml"
    )
    
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = cv2.equalizeHist(gray)  # Melhorar contraste
    
    # Detectar faces
    faces = face_cascade.detectMultiScale(
        gray,
        scaleFactor=1.1,
        minNeighbors=5,
        minSize=(30, 30),
        flags=cv2.CASCADE_SCALE_IMAGE,
    )
    
    result = img.copy()
    detections = []
    
    for (x, y, w, h) in faces:
        # Desenhar retângulo na face
        cv2.rectangle(result, (x, y), (x + w, y + h), (0, 255, 0), 2)
        
        # Detectar olhos dentro da região da face
        roi_gray = gray[y:y + h, x:x + w]
        eyes = eye_cascade.detectMultiScale(roi_gray, scaleFactor=1.1, minNeighbors=3)
        
        for (ex, ey, ew, eh) in eyes:
            center = (x + ex + ew // 2, y + ey + eh // 2)
            radius = min(ew, eh) // 2
            cv2.circle(result, center, radius, (255, 0, 0), 2)
        
        detections.append({
            "bbox": (x, y, w, h),
            "eyes_detected": len(eyes),
            "confidence": None,  # Haar não fornece confiança
        })
    
    print(f"Faces detectadas (Haar): {len(faces)}")
    return result, detections

DNN Face Detection (Método Moderno)

def detect_faces_dnn(
    img: np.ndarray,
    confidence_threshold: float = 0.5,
    model_path: str | None = None,
    config_path: str | None = None,
) -> tuple[np.ndarray, list]:
    """
    Detecta faces usando Deep Neural Network (DNN) do OpenCV.
    Mais preciso e robusto que Haar Cascades.
    """
    # Usar modelo Caffe pré-treinado
    if model_path is None:
        # Baixar se necessário
        model_url = (
            "https://raw.githubusercontent.com/opencv/opencv_3rdparty/"
            "dnn_samples_face_detector_20170830/"
            "res10_300x300_ssd_iter_140000.caffemodel"
        )
        config_url = (
            "https://raw.githubusercontent.com/opencv/opencv/master/"
            "samples/dnn/face_detector/deploy.prototxt"
        )
        
        models_dir = Path("models")
        models_dir.mkdir(exist_ok=True)
        
        model_path = str(models_dir / "face_detector.caffemodel")
        config_path = str(models_dir / "face_detector.prototxt")
        
        # Verificar se precisa baixar
        if not Path(model_path).exists():
            import urllib.request
            print("Baixando modelo de detecção de faces...")
            urllib.request.urlretrieve(model_url, model_path)
            urllib.request.urlretrieve(config_url, config_path)
            print("Download concluído!")
    
    # Carregar rede neural
    net = cv2.dnn.readNetFromCaffe(config_path, model_path)
    
    h, w = img.shape[:2]
    
    # Preparar input (blob)
    blob = cv2.dnn.blobFromImage(
        img, scalefactor=1.0, size=(300, 300),
        mean=(104.0, 177.0, 123.0), swapRB=False, crop=False
    )
    
    # Inferência
    net.setInput(blob)
    detections_raw = net.forward()
    
    result = img.copy()
    detections = []
    
    for i in range(detections_raw.shape[2]):
        confidence = detections_raw[0, 0, i, 2]
        
        if confidence > confidence_threshold:
            box = detections_raw[0, 0, i, 3:7] * np.array([w, h, w, h])
            x1, y1, x2, y2 = box.astype(int)
            
            # Garantir que está dentro da imagem
            x1 = max(0, x1)
            y1 = max(0, y1)
            x2 = min(w, x2)
            y2 = min(h, y2)
            
            # Desenhar detecção
            cv2.rectangle(result, (x1, y1), (x2, y2), (0, 255, 0), 2)
            label = f"Face: {confidence:.2%}"
            cv2.putText(result, label, (x1, y1 - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
            
            detections.append({
                "bbox": (x1, y1, x2 - x1, y2 - y1),
                "confidence": float(confidence),
            })
    
    print(f"Faces detectadas (DNN): {len(detections)}")
    return result, detections

Parte 5: Detecção de Objetos com YOLO

Configuração do YOLOv8

# Instalar ultralytics (YOLOv8)
pip install ultralytics

Detecção de Objetos

"""Detecção de objetos com YOLOv8."""

import cv2
import numpy as np
from pathlib import Path
from ultralytics import YOLO


class ObjectDetector:
    """Detector de objetos usando YOLOv8."""

    def __init__(self, model_name: str = "yolov8n.pt"):
        """
        Inicializa o detector.
        
        Modelos disponíveis (do mais leve ao mais preciso):
        - yolov8n.pt (Nano - mais rápido)
        - yolov8s.pt (Small)
        - yolov8m.pt (Medium)
        - yolov8l.pt (Large)
        - yolov8x.pt (Extra Large - mais preciso)
        """
        self.model = YOLO(model_name)
        print(f"Modelo carregado: {model_name}")

    def detect(
        self,
        img: np.ndarray,
        confidence: float = 0.5,
        classes: list[int] | None = None,
    ) -> tuple[np.ndarray, list[dict]]:
        """
        Detecta objetos em uma imagem.
        
        Args:
            img: Imagem de entrada (BGR)
            confidence: Limiar de confiança mínima
            classes: Lista de classes para detectar (None = todas)
        
        Returns:
            Imagem anotada e lista de detecções
        """
        results = self.model(img, conf=confidence, classes=classes, verbose=False)
        
        result = results[0]
        annotated = result.plot()  # Imagem com anotações
        
        detections = []
        for box in result.boxes:
            x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
            conf = float(box.conf[0])
            cls_id = int(box.cls[0])
            cls_name = result.names[cls_id]
            
            detections.append({
                "class": cls_name,
                "class_id": cls_id,
                "confidence": round(conf, 4),
                "bbox": (int(x1), int(y1), int(x2 - x1), int(y2 - y1)),
            })
        
        print(f"Objetos detectados: {len(detections)}")
        for det in detections:
            print(f"  - {det['class']}: {det['confidence']:.2%}")
        
        return annotated, detections

    def detect_video(
        self,
        video_path: str,
        output_path: str,
        confidence: float = 0.5,
    ) -> None:
        """Detecta objetos em um vídeo e salva o resultado."""
        cap = cv2.VideoCapture(video_path)
        
        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        fps = int(cap.get(cv2.CAP_PROP_FPS))
        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
        
        frame_count = 0
        while cap.isOpened():
            ret, frame = cap.read()
            if not ret:
                break
            
            annotated, _ = self.detect(frame, confidence=confidence)
            out.write(annotated)
            
            frame_count += 1
            if frame_count % 30 == 0:
                print(f"Processado: {frame_count}/{total_frames} frames")
        
        cap.release()
        out.release()
        print(f"Vídeo salvo em: {output_path}")

    def detect_webcam(self, confidence: float = 0.5) -> None:
        """Detecção em tempo real usando webcam."""
        cap = cv2.VideoCapture(0)
        
        print("Pressione 'q' para sair")
        
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            
            annotated, detections = self.detect(frame, confidence=confidence)
            
            # Mostrar FPS
            fps_text = f"Objetos: {len(detections)}"
            cv2.putText(annotated, fps_text, (10, 30),
                        cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
            
            cv2.imshow("YOLO Detection", annotated)
            
            if cv2.waitKey(1) & 0xFF == ord("q"):
                break
        
        cap.release()
        cv2.destroyAllWindows()


# Classes COCO (80 classes que YOLO detecta por padrão)
COCO_CLASSES = {
    0: "person", 1: "bicycle", 2: "car", 3: "motorcycle", 4: "airplane",
    5: "bus", 6: "train", 7: "truck", 8: "boat", 9: "traffic light",
    10: "fire hydrant", 11: "stop sign", 12: "parking meter", 13: "bench",
    14: "bird", 15: "cat", 16: "dog", 17: "horse", 18: "sheep", 19: "cow",
    20: "elephant", 21: "bear", 22: "zebra", 23: "giraffe", 24: "backpack",
    25: "umbrella", 26: "handbag", 27: "tie", 28: "suitcase", 29: "frisbee",
    # ... mais 50 classes
}

Exemplo de Uso Completo

"""Exemplo completo: detecção de objetos em imagem."""

from pathlib import Path


def main():
    # 1. Carregar imagem
    img = cv2.imread("images/rua.jpg")
    if img is None:
        print("Crie uma pasta 'images' e coloque imagens de teste nela.")
        return
    
    # 2. Inicializar detector
    detector = ObjectDetector("yolov8n.pt")
    
    # 3. Detectar todos os objetos
    annotated, detections = detector.detect(img, confidence=0.5)
    cv2.imwrite("output/detections.jpg", annotated)
    
    # 4. Detectar apenas pessoas e carros (classes 0 e 2)
    annotated_filtered, filtered = detector.detect(
        img, confidence=0.5, classes=[0, 2]
    )
    cv2.imwrite("output/people_cars.jpg", annotated_filtered)
    
    # 5. Contar objetos por classe
    class_counts = {}
    for det in detections:
        cls = det["class"]
        class_counts[cls] = class_counts.get(cls, 0) + 1
    
    print("\nContagem por classe:")
    for cls, count in sorted(class_counts.items(), key=lambda x: -x[1]):
        print(f"  {cls}: {count}")


if __name__ == "__main__":
    main()

Parte 6: Processamento de Vídeo

Captura e Processamento de Webcam

"""Processamento de vídeo em tempo real."""

import cv2
import numpy as np
import time


def process_webcam_with_effects(effect: str = "edges") -> None:
    """Processa vídeo da webcam com efeitos em tempo real."""
    cap = cv2.VideoCapture(0)
    
    if not cap.isOpened():
        print("Erro: não foi possível abrir a webcam.")
        return
    
    effects = {
        "edges": lambda f: cv2.Canny(cv2.cvtColor(f, cv2.COLOR_BGR2GRAY), 50, 150),
        "blur": lambda f: cv2.GaussianBlur(f, (21, 21), 0),
        "cartoon": lambda f: cartoon_effect(f),
        "negative": lambda f: cv2.bitwise_not(f),
        "gray": lambda f: cv2.cvtColor(f, cv2.COLOR_BGR2GRAY),
    }
    
    process_fn = effects.get(effect, effects["edges"])
    
    prev_time = time.time()
    
    print(f"Efeito: {effect} | Pressione 'q' para sair")
    
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        processed = process_fn(frame)
        
        # Calcular FPS
        curr_time = time.time()
        fps = 1 / (curr_time - prev_time)
        prev_time = curr_time
        
        if len(processed.shape) == 2:
            processed = cv2.cvtColor(processed, cv2.COLOR_GRAY2BGR)
        
        cv2.putText(processed, f"FPS: {fps:.1f}", (10, 30),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
        
        cv2.imshow("Webcam", processed)
        
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
    
    cap.release()
    cv2.destroyAllWindows()


def cartoon_effect(img: np.ndarray) -> np.ndarray:
    """Aplica efeito cartoon a uma imagem."""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = cv2.medianBlur(gray, 5)
    
    edges = cv2.adaptiveThreshold(
        gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
        cv2.THRESH_BINARY, 9, 9
    )
    
    color = cv2.bilateralFilter(img, 9, 300, 300)
    
    cartoon = cv2.bitwise_and(color, color, mask=edges)
    return cartoon


def detect_motion(threshold: int = 25, min_area: int = 500) -> None:
    """Detecta movimento usando diferença de frames."""
    cap = cv2.VideoCapture(0)
    
    ret, prev_frame = cap.read()
    prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
    prev_gray = cv2.GaussianBlur(prev_gray, (21, 21), 0)
    
    print("Detectando movimento... Pressione 'q' para sair")
    
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        gray = cv2.GaussianBlur(gray, (21, 21), 0)
        
        frame_delta = cv2.absdiff(prev_gray, gray)
        _, thresh = cv2.threshold(frame_delta, threshold, 255, cv2.THRESH_BINARY)
        thresh = cv2.dilate(thresh, None, iterations=2)
        
        contours, _ = cv2.findContours(
            thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
        )
        
        motion_detected = False
        for contour in contours:
            if cv2.contourArea(contour) > min_area:
                motion_detected = True
                x, y, w, h = cv2.boundingRect(contour)
                cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
        
        status = "MOVIMENTO!" if motion_detected else "Quieto"
        color = (0, 0, 255) if motion_detected else (0, 255, 0)
        cv2.putText(frame, status, (10, 30),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2)
        
        cv2.imshow("Motion Detection", frame)
        
        prev_gray = gray
        
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
    
    cap.release()
    cv2.destroyAllWindows()

Parte 7: Projeto Prático — Sistema de Contagem de Pessoas

"""
Projeto prático: Sistema de contagem de pessoas em imagens.
Combina detecção de faces (DNN) com detecção de pessoas (YOLO).
"""

import cv2
import numpy as np
from pathlib import Path
from dataclasses import dataclass


@dataclass
class PersonCount:
    """Resultado da contagem de pessoas."""

    total_people: int
    total_faces: int
    detections: list[dict]
    image_path: str


def count_people(image_path: str) -> PersonCount:
    """Conta pessoas em uma imagem usando YOLO."""
    from ultralytics import YOLO
    
    img = cv2.imread(image_path)
    if img is None:
        raise FileNotFoundError(f"Imagem não encontrada: {image_path}")
    
    # Detectar pessoas com YOLO
    model = YOLO("yolov8n.pt")
    results = model(img, classes=[0], conf=0.5, verbose=False)  # classe 0 = person
    
    people_count = len(results[0].boxes)
    
    # Detectar faces
    face_cascade = cv2.CascadeClassifier(
        cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
    )
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(30, 30))
    
    # Anotar imagem
    annotated = results[0].plot()
    for (x, y, w, h) in faces:
        cv2.rectangle(annotated, (x, y), (x + w, y + h), (255, 0, 0), 2)
    
    # Adicionar contagem no topo
    text = f"Pessoas: {people_count} | Faces: {len(faces)}"
    cv2.putText(annotated, text, (10, 40),
                cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 255), 3)
    
    output_path = f"output/counted_{Path(image_path).name}"
    Path("output").mkdir(exist_ok=True)
    cv2.imwrite(output_path, annotated)
    
    return PersonCount(
        total_people=people_count,
        total_faces=len(faces),
        detections=[],
        image_path=output_path,
    )

Conclusão

Neste tutorial, percorremos todo o espectro da visão computacional com OpenCV e Python:

  1. Operações básicas: leitura, manipulação, espaços de cores e desenho.
  2. Filtros: suavização, nitidez, morfologia e threshold.
  3. Detecção de bordas: Sobel, Laplacian, Canny e contornos.
  4. Detecção de faces: Haar Cascades (clássico) e DNN (moderno).
  5. Detecção de objetos: YOLOv8 para detecção multi-classe.
  6. Processamento de vídeo: webcam, efeitos em tempo real e detecção de movimento.
  7. Projeto prático: sistema de contagem de pessoas.

A visão computacional é um campo vasto e em constante evolução. Com as bases que construímos aqui, você está preparado para explorar aplicações mais avançadas.


Próximos Passos

  • Explorar segmentação semântica com SAM (Segment Anything Model).
  • Implementar rastreamento de objetos com DeepSORT.
  • Treinar modelos YOLO customizados para domínios específicos.
  • Explorar estimação de pose com MediaPipe.
  • Construir aplicações de realidade aumentada com OpenCV.
  • Integrar com frameworks de deep learning (PyTorch) para modelos customizados.

Acompanhe o Inteligência em Código para mais tutoriais sobre visão computacional e deep learning.