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

Введение
Создание приложения камеры — отличный практический проект, который сочетает разработку 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 timetkinter — для 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):
- Захват одного снимка: нажать Снимок, проверить файл gallery и просмотр.
- Короткая запись 5–10 секунд: проверить воспроизведение и целостность файла.
- Переход в Галерею во время записи: приложение должно приостановить обновление главного превью и корректно отобразить миниатюры.
- Непредвидное закрытие плеера: при закрытии окно должно вернуть приложение в режим обновления камеры.
Чек-листы по ролям
Разработчик:
- Проверить совместимость 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)
- Создать виртуальное окружение и установить зависимости.
- Запустить приложение локально и убедиться в доступности камеры.
- Прогнать базовые сценарии тестирования по чек-листу.
- Добавить логирование и обработку ошибок.
- Подготовить README и инструкции по развёртыванию.
Риски и смягчающие меры
- Риск: камера недоступна или используется другой программой. Мягчение: проверять доступность cap.isOpened() и показывать пользователю сообщение.
- Риск: переполнение папки gallery. Мягчение: добавьте ротацию файлов или лимит на размер/число файлов.
Итог
Проект камеры на Python объединяет несколько ключевых навыков: работу с GUI, обработку изображений и управление потоками. Он легко расширяем: можно добавить фильтры, детекцию объектов, загрузку в облако или стриминг. Следуйте критериям приёмки и чек-листам, чтобы довести приложение до стабильного рабочего состояния.
Важно: экспериментируйте с кодеками и разрешениями, чтобы найти оптимальный баланс между качеством и производительностью для вашей задачи.
Похожие материалы
Запуск Командной строки от имени администратора
Отключить напоминания Facebook Memories и скрыть их
Как управлять cookie в Chrome, Firefox и Edge
Как управлять расширениями в Chrome, Edge, Vivaldi
Добавить пользовательские эмодзи в Discord