Гид по технологиям

Создание приложения камеры на Python с OpenCV и tkinter

5 min read Python Обновлено 22 Apr 2026
Приложение камеры на Python с OpenCV и tkinter
Приложение камеры на Python с OpenCV и tkinter

Человек программирует на ноутбуке, рядом видна книга по Python

Введение

Создание приложения камеры — отличный практический проект, который сочетает разработку GUI, обработку изображений и мультипоточность. Такой проект помогает прокачать навыки работы с библиотеками, отладкой и проектированием пользовательских интерфейсов.

Важно: код работает с реальной камерой; убедитесь, что у вас есть доступ к веб-камере и установлены требуемые пакеты.

Подготовка окружения

Создайте виртуальное окружение для изоляции зависимостей. В терминале выполните:

pip install opencv-python pillow

Эта команда установит OpenCV (компьютерное зрение) и Pillow (работа с изображениями).

Примечание: используйте virtualenv или venv, чтобы не конфликтовать с системными пакетами.

Импорт необходимых библиотек

Импортируем модули Python и сторонние пакеты:

import tkinter as tk
import cv2
from PIL import Image, ImageTk
import os
import threading
import time

tkinter — для GUI; cv2 (OpenCV) — для захвата и обработки видео; Pillow — для конвертации и миниатюр; os, threading и time — для файловой работы, параллелизма и меток времени.

Создание каталога галереи и глобальных переменных

Убедитесь, что каталог для сохранения снимков и видео существует:

if not os.path.exists("gallery"):
    os.makedirs("gallery")

Инициализируем глобальные списки и флаги:

# Инициализация глобальных списков для миниатюр
image_thumbnails = []
video_thumbnails = []  # Список миниатюр для видео
update_camera = True  # Флаг обновления видеопотока

Флаг update_camera управляет обновлением превью с камеры, чтобы при проигрывании видео основной поток не конфликтовал с плеером.

Захват изображения из видеопотока

Функция для снятия кадра с текущего видеопотока и сохранения его в галерее:

def capture_image():
    ret, frame = cap.read()

    if ret:
        # Генерируем уникальное имя файла с отметкой времени
        timestamp = time.strftime("%Y%m%d%H%M%S")
        image_path = os.path.join("gallery", f"captured_image_{timestamp}.jpg")
        cv2.imwrite(image_path, frame)
        show_image(image_path)

Кадр сохраняется в формате JPEG, затем мы открываем его в приложении через show_image.

Запуск и остановка записи видео

Функция для запуска записи. Она создаёт VideoWriter, отключает кнопку «Запись» и стартует отдельный поток записи.

def start_recording():
    global video_writer, recording_start_time, recording_stopped, update_camera

    if not video_writer:
        timestamp = time.strftime("%Y%m%d%H%M%S")
        video_path = os.path.join("gallery", f"recorded_video_{timestamp}.mp4")

        # Используем кодек mp4v (можно попробовать другие кодеки)
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')

        # Настройка частоты кадров и разрешения при необходимости
        video_writer = cv2.VideoWriter(video_path, fourcc, 20.0,
                                       (640, 480))

        recording_start_time = time.time()
        recording_stopped = False
        record_button.config(state=tk.DISABLED)
        stop_button.config(state=tk.NORMAL)

        # Запуск отдельного потока для записи и отображения таймера
        recording_thread = threading.Thread(target=record_and_display)
        recording_thread.start()

Функция для остановки записи и освобождения ресурсов:

def stop_recording():
    global video_writer, recording_stopped

    if video_writer:
        video_writer.release()
        video_writer = None
        recording_stopped = True
        record_button.config(state=tk.NORMAL)
        stop_button.config(state=tk.DISABLED)

Обратите внимание: после release() полезно присвоить video_writer значение None, чтобы другие части кода корректно проверяли состояние записи.

Запись и отображение видео в реальном времени

Функция record_and_display читает кадры, добавляет текст с прошедшим временем и записывает кадры в файл.

def record_and_display():
    global recording_stopped, update_camera

    while video_writer and not recording_stopped:
        ret, frame = cap.read()

        if ret:
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

            # Вычисляем прошедшее время и добавляем в кадр
            elapsed_time = time.time() - recording_start_time
            timestamp = f"Time Elapsed: {int(elapsed_time)}s"

            cv2.putText(frame, timestamp, (10, 30), cv2.FONT_HERSHEY_SIMPLEX,
                        0.5, (255, 255, 255), 2)

            img = Image.fromarray(frame)
            photo = ImageTk.PhotoImage(image=img)
            camera_feed.config(image=photo)
            camera_feed.image = photo

            video_writer.write(frame)
            time.sleep(0.05)

    camera_feed.after(10, update_camera_feed)

Замечание: метка времени добавляется в RGB-кадр перед конвертацией в объект Image. Если требуется локализация текста таймера — замените строку timestamp.

Отображение сохранённых изображений и видео

Функция для отображения изображения в widget камеры:

def show_image(image_path):
    image = Image.open(image_path)
    photo = ImageTk.PhotoImage(image=image)
    camera_feed.config(image=photo)
    camera_feed.image = photo

Функция для открытия отдельного окна-плеера и приостановки обновления основного потока:

def play_video(video_path):
    def close_video_player():
        video_player.destroy()
        global update_camera
        update_camera = True

    global update_camera
    update_camera = False

    video_player = tk.Toplevel(root)
    video_player.title("Просмотр видео")

    video_cap = cv2.VideoCapture(video_path)

    def update_video_frame():
        ret, frame = video_cap.read()

        if ret:
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            img = Image.fromarray(frame)
            photo = ImageTk.PhotoImage(image=img)
            video_label.config(image=photo)
            video_label.image = photo

            # Получаем реальную частоту кадров видео
            frame_rate = video_cap.get(cv2.CAP_PROP_FPS)
            delay = int(1000 / frame_rate) if frame_rate and frame_rate > 0 else 33

            video_player.after(delay, update_video_frame)
        else:
            video_player.destroy()

    video_label = tk.Label(video_player)
    video_label.pack()

    update_video_frame()

    video_player.protocol("WM_DELETE_WINDOW", close_video_player)

Если скорость видео не определена, используется запасной delay ~33 мс (около 30 FPS).

Создание миниатюр видео и открытие галереи

Генерация миниатюры для видео (первый кадр):

def create_video_thumbnail(video_path):
    video_cap = cv2.VideoCapture(video_path)
    ret, frame = video_cap.read()

    if ret:
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        thumbnail = Image.fromarray(frame).resize((100, 100))
        thumbnail_photo = ImageTk.PhotoImage(image=thumbnail)
        return thumbnail_photo, os.path.basename(video_path)

    return None, None

Функция для открытия окна галереи с миниатюрами изображений и видео:

def play_video_from_thumbnail(video_path):
    play_video(video_path)


def open_gallery():
    global update_camera
    update_camera = False

    gallery_window = tk.Toplevel(root)
    gallery_window.title("Галерея")

    def back_to_camera():
        gallery_window.destroy()
        global update_camera

        # Возобновляем обновление видеопотока
        update_camera = True

    back_button = tk.Button(gallery_window, text="Назад к камере",
                            command=back_to_camera)

    back_button.pack()

    gallery_dir = "gallery"
    image_files = [f for f in os.listdir(gallery_dir) if f.endswith(".jpg")]
    video_files = [f for f in os.listdir(gallery_dir) if f.endswith(".mp4")]

    # Очищаем существующие списки миниатюр
    del image_thumbnails[:]
    del video_thumbnails[:]

    for image_file in image_files:
        image_path = os.path.join(gallery_dir, image_file)
        thumbnail = Image.open(image_path).resize((100, 100))
        thumbnail_photo = ImageTk.PhotoImage(image=thumbnail)
        image_name = os.path.basename(image_file)

        def show_image_in_gallery(img_path, img_name):
            image_window = tk.Toplevel(gallery_window)
            image_window.title("Изображение")
            img = Image.open(img_path)
            img_photo = ImageTk.PhotoImage(img)
            img_label = tk.Label(image_window, image=img_photo)
            img_label.image = img_photo
            img_label.pack()
            img_label_name = tk.Label(image_window, text=img_name)
            img_label_name.pack()

        thumbnail_label = tk.Label(gallery_window, image=thumbnail_photo)
        thumbnail_label.image = thumbnail_photo

        thumbnail_label.bind("", lambda event,
                                 img_path=image_path,
                                 img_name=image_name:
            show_image_in_gallery(img_path, img_name))

        thumbnail_label.pack()
        image_thumbnails.append(thumbnail_photo)

        # Отображаем имя файла под миниатюрой
        image_name_label = tk.Label(gallery_window, text=image_name)
        image_name_label.pack()

    for video_file in video_files:
        video_path = os.path.join(gallery_dir, video_file)

        # Создаём миниатюру видео и получаем имя файла
        thumbnail_photo, video_name = create_video_thumbnail(video_path)

        if thumbnail_photo:
            video_thumbnail_button = tk.Button(
                gallery_window,
                image=thumbnail_photo,
                command=lambda path=video_path: play_video_from_thumbnail(path)
            )

            video_thumbnail_button.pack()

            # Сохраняем объекты PhotoImage, чтобы они не удалялись GC
            video_thumbnails.append(thumbnail_photo)

            # Отображаем имя видео под миниатюрой
            video_name_label = tk.Label(gallery_window, text=video_name)
            video_name_label.pack()

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

Создание основного интерфейса

Инициализация окна приложения и переменных:

root = tk.Tk()
root.title("Приложение камеры")

video_writer = None
recording_start_time = 0  # Время начала записи
recording_stopped = False  # Флаг остановки записи

Создание кнопок управления (локализованные подписи):

capture_button = tk.Button(root, text="Снимок", command=capture_image)
record_button = tk.Button(root, text="Запись", command=start_recording)
stop_button = tk.Button(root, text="Остановить запись", command=stop_recording)
gallery_button = tk.Button(root, text="Галерея", command=open_gallery)
quit_button = tk.Button(root, text="Выход", command=root.quit)

capture_button.grid(row=0, column=0, padx=10, pady=10)
record_button.grid(row=0, column=1, padx=10, pady=10)
stop_button.grid(row=0, column=2, padx=10, pady=10)
gallery_button.grid(row=0, column=3, padx=10, pady=10)
quit_button.grid(row=0, column=4, padx=10, pady=10)

camera_feed = tk.Label(root)
camera_feed.grid(row=1, column=0, columnspan=5)
cap = cv2.VideoCapture(0)

Функция обновления видеопотока в GUI:

def update_camera_feed():
    if update_camera:
        if not video_writer:
            ret, frame = cap.read()

            if ret:
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                img = Image.fromarray(frame)
                photo = ImageTk.PhotoImage(image=img)
                camera_feed.config(image=photo)
                camera_feed.image = photo

    root.after(10, update_camera_feed)

update_camera_feed()

root.mainloop()

root.mainloop() запускает цикл обработки событий tkinter и отвечает за реакцию интерфейса на действия пользователя.

Тестирование и критерии приёмки

Критерии приёмки (минимальные тесты):

  • Камера успешно инициализируется (cap.open() возвращает True).
  • Нажатие “Снимок” сохраняет файл в папку gallery и отображает его.
  • Нажатие “Запись” создает mp4-файл, а “Остановить запись” корректно завершает файл.
  • Галерея отображает миниатюры изображений и видео, клики открывают содержимое.
  • Приложение не зависает при одновременных операциях записи и открытия галереи.

Тестовые сценарии (acceptance):

  1. Захват одного снимка: нажать Снимок, проверить файл gallery и просмотр.
  2. Короткая запись 5–10 секунд: проверить воспроизведение и целостность файла.
  3. Переход в Галерею во время записи: приложение должно приостановить обновление главного превью и корректно отобразить миниатюры.
  4. Непредвидное закрытие плеера: при закрытии окно должно вернуть приложение в режим обновления камеры.

Чек-листы по ролям

Разработчик:

  • Проверить совместимость OpenCV и Python (версия).
  • Покрыть критические функции простыми unit-тестами (вынести логику сохранения файлов в отдельные функции).
  • Обработать исключения при работе с файловой системой и камерой.

QA-инженер:

  • Прогнать тесты при разной нагрузке (много файлов в папке gallery).
  • Проверить поведение при отключении камеры во время записи.

DevOps:

  • Обеспечить инструкции по установке зависимостей и запуску в README.
  • Настроить CI, который запускает базовые проверки lint и unit-тесты.

Альтернативные подходы

  • Использовать PyQt или wxPython для более насыщенного и современного UI.
  • Для записи и трансляции в реальном времени применить GStreamer или FFmpeg (через subprocess) для более гибкой кодировки.
  • Для веб-интерфейса — использовать Flask/Streamlit и передавать кадры через MJPEG/WebRTC.

Когда это решение не подходит

  • Нужна высокая производительность и низкая задержка для промышленного видеонаблюдения — лучше выбирать специализированные решения на C++ или медиасерверы.
  • Требуется кроссплатформенная мобильная поддержка — этот GUI ориентирован на настольные приложения.

Безопасность и приватность

  • Храните медиа-файлы в защищённой директории и контролируйте доступ к ней.
  • Если приложение будет использоваться в корпоративной среде, шифруйте чувствительные файлы и логируйте доступ.
  • При публикации удостоверьтесь, что пользователи знают о включённой камере и получают согласие на запись.

Быстрый cheat-sheet: рекомендованные параметры

  • Кодек: mp4v (для кроссплатформенной совместимости). Возможные замены — XVID, H264 (если установлены кодеки).
  • Частота кадров при записи: 20–30 FPS.
  • Миниатюры: 100×100 — быстрый компромисс между скоростью и читаемостью.
  • Разрешение захвата: 640×480 по умолчанию; повышайте при необходимости, но учитывайте производительность.

Decision flow (простая схема выбора режима)

flowchart TD
    A[Запустить приложение] --> B{Пользователь хочет}
    B --> |Сделать снимок| C[Нажать 'Снимок']
    B --> |Записать видео| D[Нажать 'Запись']
    D --> E{Запись идёт?}
    E --> |Да| F[Нажать 'Остановить запись']
    E --> |Нет| D
    B --> |Посмотреть галерею| G[Нажать 'Галерея']
    G --> H[Просмотр миниатюр -> Открыть]

Глоссарий (1 строка)

  • OpenCV: библиотека для компьютерного зрения.
  • Pillow: форк PIL, библиотека для работы с изображениями в Python.
  • tkinter: стандартная библиотека GUI для Python.

Краткая методология внедрения (SOP)

  1. Создать виртуальное окружение и установить зависимости.
  2. Запустить приложение локально и убедиться в доступности камеры.
  3. Прогнать базовые сценарии тестирования по чек-листу.
  4. Добавить логирование и обработку ошибок.
  5. Подготовить README и инструкции по развёртыванию.

Риски и смягчающие меры

  • Риск: камера недоступна или используется другой программой. Мягчение: проверять доступность cap.isOpened() и показывать пользователю сообщение.
  • Риск: переполнение папки gallery. Мягчение: добавьте ротацию файлов или лимит на размер/число файлов.

Итог

Проект камеры на Python объединяет несколько ключевых навыков: работу с GUI, обработку изображений и управление потоками. Он легко расширяем: можно добавить фильтры, детекцию объектов, загрузку в облако или стриминг. Следуйте критериям приёмки и чек-листам, чтобы довести приложение до стабильного рабочего состояния.

Важно: экспериментируйте с кодеками и разрешениями, чтобы найти оптимальный баланс между качеством и производительностью для вашей задачи.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Запуск Командной строки от имени администратора
Windows

Запуск Командной строки от имени администратора

Отключить напоминания Facebook Memories и скрыть их
Социальные сети

Отключить напоминания Facebook Memories и скрыть их

Как управлять cookie в Chrome, Firefox и Edge
Приватность

Как управлять cookie в Chrome, Firefox и Edge

Как управлять расширениями в Chrome, Edge, Vivaldi
Браузеры

Как управлять расширениями в Chrome, Edge, Vivaldi

Добавить пользовательские эмодзи в Discord
Discord

Добавить пользовательские эмодзи в Discord

Вентилируемая подставка для ноутбука — 3 DIY-плана
DIY

Вентилируемая подставка для ноутбука — 3 DIY-плана