Skip to main content

Компьютерное зрение — Как роботы видят мир

Представь, что роботу дали фотографию и сказали: “Что здесь нарисовано?” Для нас это просто — мы видим кошку на диване. Для робота это массив из миллионов чисел (пикселей). Компьютерное зрение — это перевод с “пиксельного” языка на “человеческий”.

Что такое изображение для компьютера?

Простое изображение 3x3 пикселя в градациях серого:

# Каждый пиксель — число от 0 (чёрный) до 255 (белый)
image = [
    [255, 128, 0],
    [128, 0, 128],
    [0, 255, 128]
]

# Визуализация:
# █▓░  (255, 128, 0)
# ▓░▓  (128, 0, 128)
# ░█▓  (0, 255, 128)

Цветное изображение — три таких матрицы: Red, Green, Blue (RGB).


OpenCV — Главный инструмент

OpenCV — как швейцарский нож для работы с изображениями. Установить просто:

pip install opencv-python

Базовые операции за 5 минут:

import cv2
import numpy as np

# 1. Загрузка изображения
image = cv2.imread('robot_view.jpg')  # Читаем файл
# Если файла нет, создадим тестовое изображение
if image is None:
    # Создаём изображение 400x300 синего цвета
    image = np.zeros((300, 400, 3), dtype=np.uint8)
    image[:, :] = (255, 0, 0)  # Синий (BGR формат!)

# 2. Показываем
cv2.imshow('Что видит робот', image)
cv2.waitKey(0)  # Ждём нажатия любой клавиши
cv2.destroyAllWindows()

# 3. Информация об изображении
print(f"Размер: {image.shape}")  # (высота, ширина, каналы)
print(f"Тип данных: {image.dtype}")
print(f"Всего пикселей: {image.size}")

Практические задачи

1. Найти красный мяч

Самый частый сценарий: робот ищет цветной объект.

def find_red_object(image):
    # 1. Конвертируем в HSV (проще работать с цветами)
    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
    
    # 2. Диапазон красного цвета в HSV
    lower_red1 = np.array([0, 100, 100])     # Красный с одной стороны
    upper_red1 = np.array([10, 255, 255])
    lower_red2 = np.array([160, 100, 100])   # Красный с другой стороны
    upper_red2 = np.array([180, 255, 255])
    
    # 3. Создаём маски
    mask1 = cv2.inRange(hsv, lower_red1, upper_red1)
    mask2 = cv2.inRange(hsv, lower_red2, upper_red2)
    red_mask = cv2.bitwise_or(mask1, mask2)
    
    # 4. Находим контуры
    contours, _ = cv2.findContours(red_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    if contours:
        # Берём самый большой контур
        largest = max(contours, key=cv2.contourArea)
        
        # Находим центр и радиус
        ((x, y), radius) = cv2.minEnclosingCircle(largest)
        
        if radius > 10:  # Игнорируем мелкие шумы
            # Рисуем кружок вокруг объекта
            cv2.circle(image, (int(x), int(y)), int(radius), (0, 255, 0), 2)
            cv2.putText(image, "Red ball", (int(x), int(y)-10), 
                       cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
            
            return image, (int(x), int(y), int(radius))
    
    return image, None

# Использование с веб-камерой
cap = cv2.VideoCapture(0)  # 0 - первая камера

while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    # Ищем красный объект
    processed, ball = find_red_object(frame)
    
    if ball:
        x, y, r = ball
        print(f"Мяч в ({x}, {y}), радиус {r} пикселей")
    
    cv2.imshow('Поиск красного мяча', processed)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

2. Распознать QR-код

QR-коды — простой способ передачи информации роботу.

import cv2
from pyzbar import pyzbar

def read_qr_codes(image):
    # Ищем все QR и штрих-коды
    barcodes = pyzbar.decode(image)
    
    results = []
    for barcode in barcodes:
        # Координаты углов
        x, y, w, h = barcode.rect
        
        # Декодируем данные
        barcode_data = barcode.data.decode("utf-8")
        barcode_type = barcode.type
        
        # Рисуем рамку
        cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)
        
        # Подписываем
        text = f"{barcode_type}: {barcode_data}"
        cv2.putText(image, text, (x, y - 10), 
                   cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
        
        results.append({
            'type': barcode_type,
            'data': barcode_data,
            'location': (x, y, w, h)
        })
    
    return image, results

# Пример использования
image = cv2.imread('qrcode.jpg')
processed, codes = read_qr_codes(image)

for code in codes:
    print(f"Найден {code['type']}: {code['data']}")
    if code['data'].startswith('http'):
        print(f"Это ссылка! Робот может перейти по ней")

cv2.imshow('QR-коды', processed)
cv2.waitKey(0)
cv2.destroyAllWindows()

3. Распознать лица

def detect_faces(image):
    # Загружаем предобученный классификатор
    face_cascade = cv2.CascadeClassifier(
        cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
    )
    
    # Конвертируем в ч/б (работает быстрее)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # Ищем лица
    faces = face_cascade.detectMultiScale(
        gray,
        scaleFactor=1.1,      # Насколько уменьшаем изображение на каждом шаге
        minNeighbors=5,       # Сколько соседей должно быть у прямоугольника
        minSize=(30, 30)      # Минимальный размер лица
    )
    
    # Рисуем прямоугольники вокруг лиц
    for (x, y, w, h) in faces:
        cv2.rectangle(image, (x, y), (x + w, y + h), (255, 0, 0), 2)
        cv2.putText(image, "Face", (x, y-10), 
                   cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
    
    return image, len(faces)

# Использование с веб-камерой
cap = cv2.VideoCapture(0)

while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    processed, count = detect_faces(frame)
    print(f"Найдено лиц: {count}")
    
    cv2.imshow('Распознавание лиц', processed)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

Нейросети для компьютерного зрения

OpenCV хорош для простых задач, но для сложных нужны нейросети.

YOLO (You Only Look Once) — Детекция объектов в реальном времени

import cv2
import numpy as np

# Минимальный пример с предобученной YOLO
def detect_objects_yolo(image):
    # Загружаем веса и конфигурацию
    net = cv2.dnn.readNet("yolov3.weights", "yolov3.cfg")
    
    # Загружаем классы (80 объектов из COCO датасета)
    with open("coco.names", "r") as f:
        classes = [line.strip() for line in f.readlines()]
    
    # Подготавливаем изображение
    height, width = image.shape[:2]
    blob = cv2.dnn.blobFromImage(image, 1/255, (416, 416), swapRB=True, crop=False)
    
    # Пропускаем через сеть
    net.setInput(blob)
    outputs = net.forward(net.getUnconnectedOutLayersNames())
    
    # Обрабатываем результаты
    boxes = []
    confidences = []
    class_ids = []
    
    for output in outputs:
        for detection in output:
            scores = detection[5:]
            class_id = np.argmax(scores)
            confidence = scores[class_id]
            
            if confidence > 0.5:  # Порог уверенности
                # Координаты объекта
                center_x = int(detection[0] * width)
                center_y = int(detection[1] * height)
                w = int(detection[2] * width)
                h = int(detection[3] * height)
                
                # Верхний левый угол
                x = int(center_x - w / 2)
                y = int(center_y - h / 2)
                
                boxes.append([x, y, w, h])
                confidences.append(float(confidence))
                class_ids.append(class_id)
    
    # Подавление немаксимумов (убираем дублирующиеся боксы)
    indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
    
    # Рисуем результаты
    if len(indexes) > 0:
        for i in indexes.flatten():
            x, y, w, h = boxes[i]
            label = str(classes[class_ids[i]])
            confidence = confidences[i]
            
            # Разные цвета для разных классов
            color = (0, 255, 0)  # Зелёный по умолчанию
            if label == "person":
                color = (255, 0, 0)  # Синий для людей
            elif label == "car":
                color = (0, 0, 255)  # Красный для машин
            
            cv2.rectangle(image, (x, y), (x + w, y + h), color, 2)
            cv2.putText(image, f"{label} {confidence:.2f}", 
                       (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
    
    return image

# Простой пример без скачивания файлов
def simple_object_detection(image):
    # Имитируем работу нейросети
    # В реальности здесь была бы настоящая нейросеть
    
    # Просто рисуем тестовые объекты
    objects = [
        {"label": "cat", "confidence": 0.92, "box": (50, 50, 100, 100)},
        {"label": "dog", "confidence": 0.87, "box": (200, 80, 120, 120)},
        {"label": "person", "confidence": 0.95, "box": (350, 60, 80, 150)}
    ]
    
    for obj in objects:
        label = obj["label"]
        confidence = obj["confidence"]
        x, y, w, h = obj["box"]
        
        color = (0, 255, 0)
        cv2.rectangle(image, (x, y), (x + w, y + h), color, 2)
        cv2.putText(image, f"{label} {confidence:.2f}", 
                   (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
    
    return image

Стереозрение: Получение глубины

Как два глаза помогают нам оценивать расстояние, так две камеры помогают роботу.

import cv2
import numpy as np

def estimate_depth(left_image, right_image):
    """
    Оценивает глубину по двум изображениям.
    Чем больше смещение (диспаритет), тем ближе объект.
    """
    
    # Конвертируем в ч/б
    gray_left = cv2.cvtColor(left_image, cv2.COLOR_BGR2GRAY)
    gray_right = cv2.cvtColor(right_image, cv2.COLOR_BGR2GRAY)
    
    # Создаём стереоматчер
    stereo = cv2.StereoBM_create(numDisparities=16, blockSize=15)
    
    # Вычисляем диспаритет (разницу)
    disparity = stereo.compute(gray_left, gray_right)
    
    # Нормализуем для отображения
    disparity_normalized = cv2.normalize(disparity, None, 0, 255, cv2.NORM_MINMAX)
    disparity_normalized = np.uint8(disparity_normalized)
    
    # Применяем цветовую карту для наглядности
    disparity_colored = cv2.applyColorMap(disparity_normalized, cv2.COLORMAP_JET)
    
    return disparity_colored

# Если нет двух камер, создаём тестовые изображения
def create_test_stereo_images():
    # Левое изображение: белый квадрат слева
    left = np.zeros((300, 400, 3), dtype=np.uint8)
    left[100:200, 50:150] = (255, 255, 255)  # Белый квадрат
    
    # Правое изображение: тот же квадрат смещён вправо
    right = np.zeros((300, 400, 3), dtype=np.uint8)
    right[100:200, 100:200] = (255, 255, 255)  # Квадрат смещён
    
    return left, right

# Тестируем
left_img, right_img = create_test_stereo_images()
depth_map = estimate_depth(left_img, right_img)

# Показываем результат
cv2.imshow('Left camera', left_img)
cv2.imshow('Right camera', right_img)
cv2.imshow('Depth map (ближе = ярче)', depth_map)
cv2.waitKey(0)
cv2.destroyAllWindows()

Проект: Робот, который следует за человеком

Соберём всё вместе:

import cv2
import numpy as np

class PersonFollower:
    def __init__(self):
        # Загружаем классификатор для лиц и тел
        self.face_cascade = cv2.CascadeClassifier(
            cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
        )
        
        # Параметры ПИД-регулятора для плавного движения
        self.kp = 0.1  # Пропорциональный коэффициент
        self.ki = 0.01 # Интегральный
        self.kd = 0.05 # Дифференциальный
        
        self.prev_error = 0
        self.integral = 0
        
    def find_person(self, image):
        """Находит человека в кадре"""
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        
        # Ищем лица
        faces = self.face_cascade.detectMultiScale(gray, 1.1, 5)
        
        if len(faces) > 0:
            # Берём самое большое лицо (предполагаем, что это ближайший человек)
            (x, y, w, h) = max(faces, key=lambda f: f[2] * f[3])
            
            # Центр лица
            center_x = x + w // 2
            center_y = y + h // 2
            
            # Рисуем
            cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)
            cv2.circle(image, (center_x, center_y), 5, (0, 0, 255), -1)
            
            return image, (center_x, center_y, w * h)
        
        return image, None
    
    def calculate_control(self, person_center, frame_width):
        """Рассчитывает управляющий сигнал для робота"""
        if person_center is None:
            return 0  # Не двигаемся
        
        px, py, size = person_center
        
        # Ошибка: насколько лицо смещено от центра кадра
        error = (px - frame_width // 2) / (frame_width // 2)
        
        # ПИД-регулятор
        self.integral += error
        derivative = error - self.prev_error
        
        control = (self.kp * error + 
                  self.ki * self.integral + 
                  self.kd * derivative)
        
        self.prev_error = error
        
        # Ограничиваем выход
        control = max(-1, min(1, control))
        
        return control
    
    def get_motor_commands(self, control):
        """Переводит управляющий сигнал в команды моторам"""
        if abs(control) < 0.1:  # Если почти по центру
            return ("forward", 50)  # Едем вперёд
        
        if control > 0:
            return ("right", int(50 + control * 50))
        else:
            return ("left", int(50 - control * 50))
    
    def run(self):
        """Главный цикл"""
        cap = cv2.VideoCapture(0)
        
        print("Робот-проводник запущен!")
        print("Управление:")
        print("  q - выход")
        print("  s - остановить/запустить")
        
        running = True
        
        while True:
            if not running:
                cv2.waitKey(100)
                continue
                
            ret, frame = cap.read()
            if not ret:
                break
            
            # Находим человека
            processed, person = self.find_person(frame.copy())
            
            # Рассчитываем управление
            if person:
                control = self.calculate_control(person, frame.shape[1])
                command, speed = self.get_motor_commands(control)
                
                # Выводим команду
                cv2.putText(processed, f"Command: {command} {speed}", 
                           (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
                print(f"Команда: {command} со скоростью {speed}")
            
            cv2.imshow('Person Follower', processed)
            
            key = cv2.waitKey(1) & 0xFF
            if key == ord('q'):
                break
            elif key == ord('s'):
                running = not running
                print(f"Робот {'остановлен' if not running else 'запущен'}")
        
        cap.release()
        cv2.destroyAllWindows()
        print("Робот выключен")

# Запуск
if __name__ == "__main__":
    follower = PersonFollower()
    follower.run()

Что попробовать самому?

Проекты для начинающих:

  1. Детектор улыбок — находит улыбающиеся лица
  2. Счётчик людей — считает, сколько людей прошло перед камерой
  3. Распознаватель жестов — понимает “стоп”, “иди сюда”
  4. Сканер документов — выравнивает и обрезает фотографию документа

Для продвинутых:

  1. AR-маркеры — робот следует за ArUco-маркерами
  2. SLAM с камерой — построение карты по видео
  3. Трекинг объектов — слежение за движущимся объектом
  4. Распознавание действий — “человек идёт”, “сидит”, “поднимает руку”

Инструменты:

  • OpenCV — основа основ
  • Dlib — для распознавания лиц и поз
  • MediaPipe (от Google) — для распознавания жестов и поз
  • YOLO/Darknet — для детекции объектов
  • TensorFlow/PyTorch — для своих нейросетей

Советы по работе с компьютерным зрением:

  1. Начинай с ч/б — проще, быстрее, часто достаточно
  2. Используй готовые модели — не учи с нуля, если не нужно
  3. Оптимизируй — роботы работают в реальном времени
  4. Тестируй при разном освещении — свет меняет всё
  5. Не доверяй слепо — всегда добавляй проверки и fallback’и

Помни: Компьютерное зрение — это не про идеальные условия в лаборатории, а про работу в нашем грязном, неидеальном, но очень интересном мире!