"Практическая работа №2. Детектирование объектов н...

Created on: November 18, 2025

Answered using GPT-5.1 Thinking by Chat01

Question

"Практическая работа №2. Детектирование объектов на изображениях с использованием библиотеки OpenCV
Задача. Разработать приложение для детектирования транспортных средств с использованием обученных нейронных сетей, которые доступны среди "зоопарка" моделей OpenCV. При выборе моделей необходимо обратить внимание на наличие интересующих классов транспортных средств среди детектируемых классов объектов. Архив с кадрами видео доступен по ссылке, разметка в текстовом формате доступна по ссылке.

Требования:

Язык разработки – Python, среда разработки – Spyder или любая другая.
При реализации необходимо использовать модуль DNN библиотеки OpenCV.
Разрабатываемая библиотека должна содержать иерархию классов для детектирования объектов с использованием различных моделей. Необходимо предусмотреть, что использование моделей предполагает применение разных методов предобработки изображений, а также методов постобработки выхода моделей.
Демонстрационное приложение должно загружать последовательность кадров видео, создавать детектор в соответствии с выбором модели, опционально отображать кадры видео, на которых отрисованы прямоугольниками разных цветов объекты разных классов (цвет соответствует классу объектов), и выводить показатели качества детектирования TPR и FDR. При отображении в левом верхнем углу каждого прямоугольника необходимо написать название класса и достоверность наличия объекта в этом прямоугольнике (точность для отображения вещественного числа – три знака после запятой), над прямоугольником необходимо указать наблюдаемый класс объектов. Путь до набора изображений и модель для детектирования объектов являются параметрами приложения.
Программное решение должно сопровождаться файлом README.md, в котором для каждой выбранной модели дается описание реализованных алгоритмов предварительной обработки изображений и обработки выхода сети (мини-отчет).
В шапке файла README.md необходимо указать название практической работы: "Практическая работа №2. Детектирование объектов на изображениях с использованием библиотеки OpenCV".
Результаты выполнения практической работы должны быть выложены в репозиторий курса. При этом директория практической работы содержит набор разработанных скриптов и файл README.md.
Оценивание:

Наличие возможности использования одной модели - 5 баллов.
Наибольшее количество моделей - 3 модели.
Наличие корректного приложения - 5 баллов.
Наличие README - 5 баллов.
Дополнительный балл за качество работы модели - 10 * TPR баллов.
Максимальный балл за практическую работу - 35 баллов." - требования к лабораторной этого года, "Практическая работа №2. Детектирование объектов на изображениях с использованием библиотеки OpenCV
Задача. Разработать приложение для детектирования транспортных средств с использованием обученных нейронных сетей, которые доступны среди "зоопарка" моделей OpenCV. При выборе моделей необходимо обратить внимание на наличие интересующих классов транспортных средств среди детектируемых классов объектов. Архив с кадрами видео доступен по ссылке, разметка в текстовом формате доступна по ссылке.

Требования:

Язык разработки – Python, среда разработки – Spyder или любая другая.
При реализации необходимо использовать модуль DNN библиотеки OpenCV.
Разрабатываемая библиотека должна содержать иерархию классов для детектирования объектов с использованием различных моделей. Необходимо предусмотреть, что использование моделей предполагает применение разных методов предобработки изображений, а также методов постобработки выхода моделей.
Демонстрационное приложение должно загружать последовательность кадров видео, создавать детектор в соответствии с выбором модели, опционально отображать кадры видео, на которых отрисованы прямоугольниками разных цветов объекты разных классов (цвет соответствует классу объектов), и выводить показатели качества детектирования TPR и FDR. При отображении в левом верхнем углу каждого прямоугольника необходимо написать название класса и достоверность наличия объекта в этом прямоугольнике (точность для отображения вещественного числа – три знака после запятой), над прямоугольником необходимо указать наблюдаемый класс объектов. Путь до набора изображений и модель для детектирования объектов являются параметрами приложения.
Программное решение должно сопровождаться файлом README.md, в котором для каждой выбранной модели дается описание реализованных алгоритмов предварительной обработки изображений и обработки выхода сети (мини-отчет).
В шапке файла README.md необходимо указать название практической работы: "Практическая работа №2. Детектирование объектов на изображениях с использованием библиотеки OpenCV".
Результаты выполнения практической работы должны быть выложены в репозиторий курса. При этом директория практической работы содержит набор разработанных скриптов и файл README.md.
Оценивание:

Наличие возможности использования одной модели - 5 баллов.
Наибольшее количество моделей - 3 модели.
Наличие корректного приложения - 5 баллов.
Наличие README - 5 баллов.
Дополнительный балл за качество работы модели - 10 * TPR баллов.
Максимальный балл за практическую работу - 35 баллов." - требования к лабораторной того года, "import cv2 as cv
import numpy as np
import argparse
import sys

import time

def argument_parser():
parser = argparse.ArgumentParser(description="Object Detection using YOLOv4 and OpenCV DNN")
parser.add_argument('-m', '--mode',
help="Mode of operation: 'image' or 'video'. Default is 'image'.",
type=str,
dest='mode',
default='image')
parser.add_argument('-i', '--input',
help="Path to an image or a video file.",
type=str,
dest='input_path',
required=True)
parser.add_argument('-cl', '--classes',
help="Path to the file containing class labels. Default is 'coco.names'.",
type=str,
dest='classes_path',
default='coco.names')
parser.add_argument('-c', '--config',
help="Path to the YOLOv4 config file. Default is 'yolov4.cfg'",
type=str,
dest='config_path',
default='yolov4.cfg')
parser.add_argument('-w', '--weights',
help="Path to the YOLOv4 weights file. Default is 'yolov4.weights'",
type=str,
dest='weights_path',
default='yolov4.weights')
parser.add_argument('-ct', '--conf_threshold',
help="Confidence threshold for detecting objects. Default is 0.5.",
type=float,
dest='conf_threshold',
default=0.5)
parser.add_argument('-nt', '--nms_threshold',
help="NMS threshold. Default is 0.4.",
type=float,
dest='nms_threshold',
default=0.4)
args = parser.parse_args()
return args

def Model(config_path, weights_path):
return cv.dnn.readNet(config_path, weights_path)

def GetImage(image_path):
# ImageProcessing(image_path, net, conf_threshold, nms_threshold, classes, colors)
image = cv.imread(image_path)
if image is None:
raise ValueError('Unable to load image')
return image

def GetCap(video_path):
cap = cv.VideoCapture(video_path)
if not cap.isOpened():
raise ValueError('Unable to open video')
return cap

def VideoProcessing(video_path, YOLO, conf_threshold, nms_threshold, classes, colors):
cap = GetCap(video_path)

text
while cap.isOpened(): ret, frame = cap.read() if not ret: break ProcessFrame(frame, YOLO, conf_threshold, nms_threshold, classes, colors) key = cv.waitKey(0) if key == ord('v'): break

def ImageProcessing(image_path, net, conf_threshold, nms_threshold, classes, colors):
image = GetImage(image_path)
ProcessFrame(image, net, conf_threshold, nms_threshold, classes, colors)
cv.waitKey(0)

def ProcessFrame(frame, YOLO, conf_threshold, nms_threshold, classes, colors): # This function is a kind of control: it calls the detection function and calls the render function
detections = Detect(frame, YOLO, conf_threshold, nms_threshold, classes)
result_image = Draw(frame, detections, colors)
cv.imshow("Detected Objects", result_image)
stats = {}
for det in detections:
class_name = det['class_name']
stats[class_name] = stats.get(class_name, 0) + 1

print("Image Statistics:", stats)

def Detect(image, YOLO, conf_threshold, nms_threshold, classes):
h, w = image.shape[:2]

text
blob = cv.dnn.blobFromImage(image, 1/255.0, (416, 416), (0, 0, 0), True, crop=False) # print(image.shape) # print(blob.shape) YOLO.setInput(blob) layers = YOLO.getLayerNames() # Get the name of all layers of the network. out_layers = [layers[i - 1] for i in YOLO.getUnconnectedOutLayers()] # Get the index of the output layers (327 353 379) # прямая связь (вывод) и получение выхода сети start = time.perf_counter() layer_outputs = YOLO.forward(out_layers) # for example [0.9295312 , 0.9318892 , 0.20397013, ..., 0. , 0. ,0. ] - the probability of detecting objects (coco.names) time_took = time.perf_counter() - start print(f"took: {time_took:.2f}s") boxes, probability_list, founded_classes = [], [], [] for output in layer_outputs: # перебираем каждое обнаружение объекта for detection in output: # извлекаем идентификатор класса (метку) и достоверность (как вероятность) обнаружения текущего объекта scores = detection[5:] class_id = np.argmax(scores) probability = scores[class_id] # отбросьте слабые прогнозы, убедившись, что обнаруженные # вероятность больше минимальной вероятности if probability > conf_threshold: # масштабируем координаты ограничивающего прямоугольника относительно # размер изображения, т.к "Юла" на самом деле # возвращает центральные координаты (x, y) ограничивающего # поля, за которым следуют ширина и высота поля box = detection[:4] * np.array([w, h, w, h]) (centerX, centerY, width, height) = box.astype("int") # используем центральные координаты (x, y) для получения вершины и # и левый угол ограничительной рамки x = int(centerX - (width / 2)) y = int(centerY - (height / 2)) # обновим список координат ограничивающего прямоугольника, достоверности, # и идентификаторы класса boxes.append([x, y, int(width), int(height)]) probability_list.append(float(probability)) founded_classes.append(class_id) indices = cv.dnn.NMSBoxes(boxes, probability_list, conf_threshold, nms_threshold) results = [] if len(indices) > 0: for i in indices.flatten(): results.append({ "class_id": founded_classes[i], "class_name": classes[founded_classes[i]], "confidence": probability_list[i], "box": boxes[i] }) print(results) return results

def Draw(image, detections, colors):
for det in detections:
x, y, w, h = det['box']
color = colors[det['class_id']]
label = f"{det['class_name']}: {det['confidence']:.3f}"
cv.rectangle(image, (x, y), (x + w, y + h), color, 2)

text
label_size = cv.getTextSize(label, cv.FONT_HERSHEY_SIMPLEX, 0.5, 2)[0] label_y = max(y - 10, 10) cv.rectangle(image, (x, label_y - label_size[1] - 5), (x + label_size[0], label_y), color, cv.FILLED) cv.putText(image, label, (x, label_y), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 2) return image

def main():
args = argument_parser()

text
# get data from command line with open(args.classes_path, 'r') as f: CLASSES = f.read().strip().split('\n') COLORS = np.random.uniform(0, 255, size=(len(CLASSES), 3)) YOLO = Model(args.config_path, args.weights_path) if args.mode == 'video': VideoProcessing(args.input_path, YOLO, args.conf_threshold, args.nms_threshold, CLASSES, COLORS) elif args.mode == 'image': ImageProcessing(args.input_path, YOLO, args.conf_threshold, args.nms_threshold, CLASSES, COLORS)

if name == 'main':
sys.exit(main() or 0)" - принятый код лабораторной того года, "import argparse
from app import VehicleDetectionApp

def parser():
parser = argparse.ArgumentParser(description="Vehicle Detection using OpenCV DNN")
parser.add_argument("--images", type=str, required=True,
help="Path to images directory")
parser.add_argument("--annotations", type=str, required=True,
help="Path to annotations file")
parser.add_argument("--model", type=str, required=True,
choices=["yolo", "mobilenet", "rcnn"],
help="Model type for detection")
parser.add_argument("--confidence", type=float, default=0.5,
help="Confidence threshold for detection")
parser.add_argument("--no-display", action="store_true",
help="Disable image display")
parser.add_argument("--show-ground-truth", action="store_true",
help="Display ground truth bounding boxes")

return parser.parse_args()

def main():
args = parser()

text
app = VehicleDetectionApp() app.run( images_path=args.images, annotation_path=args.annotations, model_type=args.model, confidence_threshold=args.confidence, display=not args.no_display, show_ground_truth=args.show_ground_truth )

if name == "main":
main()" - main.py из принятой лабораторной этого года, "from typing import List, Tuple
from base_struct import Detection

class DetectionEvaluator:
def init(self, target_class = "car"):
self.target_class = target_class
self.true_positives = 0
self.false_positives = 0
self.false_negatives = 0

text
def calculate_iou(self, box1, box2): x1_1, y1_1, x2_1, y2_1 = box1 x1_2, y1_2, x2_2, y2_2 = box2 xi1 = max(x1_1, x1_2) yi1 = max(y1_1, y1_2) xi2 = min(x2_1, x2_2) yi2 = min(y2_1, y2_2) intersection_area = max(0, xi2 - xi1) * max(0, yi2 - yi1) box1_area = (x2_1 - x1_1) * (y2_1 - y1_1) box2_area = (x2_2 - x1_2) * (y2_2 - y1_2) union_area = box1_area + box2_area - intersection_area return intersection_area / union_area if union_area > 0 else 0 def evaluate_frame(self, detections, ground_truth, iou_threshold = 0.5): frame_tp = 0 frame_fp = 0 frame_fn = 0 gt_used = [False] * len(ground_truth) detections.sort(key=lambda x: x.confidence, reverse=True) for detection in detections: if detection.class_name.lower() != self.target_class.lower(): continue best_iou = 0 best_gt_idx = -1 for i, gt_box in enumerate(ground_truth): if not gt_used[i]: iou = self.calculate_iou(detection.bbox, gt_box) if iou > best_iou: best_iou = iou best_gt_idx = i if best_iou >= iou_threshold and best_gt_idx != -1: frame_tp += 1 self.true_positives += 1 gt_used[best_gt_idx] = True else: frame_fp += 1 self.false_positives += 1 unused_gt = sum(1 for used in gt_used if not used) frame_fn += unused_gt self.false_negatives += unused_gt frame_tpr = frame_tp / (frame_tp + frame_fn) if (frame_tp + frame_fn) > 0 else 0 frame_fdr = frame_fp / (frame_tp + frame_fp) if (frame_tp + frame_fp) > 0 else 0 return frame_tpr, frame_fdr def get_metrics(self): tpr = self.true_positives / (self.true_positives + self.false_negatives) if (self.true_positives + self.false_negatives) > 0 else 0 fdr = self.false_positives / (self.true_positives + self.false_positives) if (self.true_positives + self.false_positives) > 0 else 0 return tpr, fdr def reset(self): self.true_positives = 0 self.false_positives = 0 self.false_negatives = 0" - evaluator.py из принятой работы этого года, "import numpy as np

from typing import List, Tuple
from base_struct import BaseDetector, Detection

class SSDMobileNetDetector(BaseDetector):
def postprocess(self, outputs, image_shape):
detections = []
img_height, img_width = image_shape

text
for detection in outputs[0, 0]: confidence = float(detection[2]) class_id = int(detection[1]) if class_id == 3 and confidence > self.config.confidence_threshold: x1 = int(detection[3] * img_width) y1 = int(detection[4] * img_height) x2 = int(detection[5] * img_width) y2 = int(detection[6] * img_height) if x2 > x1 and y2 > y1: detections.append(Detection( class_id=class_id, class_name="car", confidence=confidence, bbox=(x1, y1, x2, y2) )) return detections" - mobilenet_detector.py из принятой работы этого года, "import cv2

import numpy as np
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
from abc import ABC, abstractmethod

@dataclass
class Detection:
class_id: int
class_name: str
confidence: float
bbox: Tuple[int, int, int, int]

@dataclass
class ModelConfig:
name: str
model_path: str
config_path: str
classes: List[str]
input_size: Tuple[int, int]
scale_factor: float
swap_rb: bool
confidence_threshold: float
nms_threshold: float

class BaseDetector(ABC):
def init(self, config):
self.config = config
self.net = cv2.dnn.readNet(config.model_path, config.config_path)
self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)

text
def preprocess(self, image): blob = cv2.dnn.blobFromImage( image, self.config.scale_factor, self.config.input_size, swapRB=self.config.swap_rb, crop=False ) return blob @abstractmethod def postprocess(self, outputs, image_shape): pass def _apply_nms(self, detections): if not detections: return [] boxes = [] confidences = [] class_ids = [] for det in detections: x1, y1, x2, y2 = det.bbox boxes.append([x1, y1, x2 - x1, y2 - y1]) confidences.append(det.confidence) class_ids.append(det.class_id) indices = cv2.dnn.NMSBoxes( boxes, confidences, self.config.confidence_threshold, self.config.nms_threshold ) if len(indices) > 0: result = [] for i in indices.flatten(): result.append(detections[i]) return result return [] def detect(self, image): blob = self.preprocess(image) self.net.setInput(blob) outputs = self.net.forward() detections = self.postprocess(outputs, image.shape[:2]) return self._apply_nms(detections)" - base_struct.py из принятой работы этого года, "import cv2

import numpy as np
import os
import glob
import time
from typing import List, Tuple
from base_struct import Detection
from detector_factory import VehicleDetectorFactory
from evaluator import DetectionEvaluator
from annotation_loader import AnnotationLoader

class VehicleDetectionApp:
def init(self):
self.detection_colors = (0, 255, 0)
self.ground_truth_color = (255, 0, 255)

text
def draw_detections(self, image, detections): result = image.copy() for detection in detections: x1, y1, x2, y2 = detection.bbox class_name = detection.class_name.lower() color = self.detection_colors confidence = detection.confidence cv2.rectangle(result, (x1, y1), (x2, y2), color, 2) label = f"{class_name}: {confidence:.3f}" (text_width, text_height), baseline = cv2.getTextSize( label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1 ) cv2.rectangle( result, (x1, y1 - text_height - baseline - 5), (x1 + text_width, y1), color, -1 ) cv2.putText( result, label, (x1, y1 - baseline - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1 ) return result def draw_ground_truth(self, image, ground_truth): result = image.copy() for i, bbox in enumerate(ground_truth): x1, y1, x2, y2 = bbox cv2.rectangle(result, (x1, y1), (x2, y2), self.ground_truth_color, 2) label = "GT" (text_width, text_height), baseline = cv2.getTextSize( label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1 ) text_x = x2 - text_width text_y = y1 + text_height + baseline + 5 cv2.rectangle( result, (text_x, text_y - text_height - baseline - 5), (text_x + text_width, text_y), self.ground_truth_color, -1 ) cv2.putText( result, label, (text_x, text_y - baseline - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1 ) return result def display_metrics(self, image, tpr, fdr, frame_tpr = None, frame_fdr = None): result = image.copy() metrics_text = [ f"TPR: {tpr:.3f}", f"FDR: {fdr:.3f}" ] if frame_tpr is not None and frame_fdr is not None: metrics_text.extend([ f"Frame TPR: {frame_tpr:.3f}", f"Frame FDR: {frame_fdr:.3f}" ]) for i, text in enumerate(metrics_text): cv2.putText( result, text, (10, 30 + i * 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2 ) return result def run(self, images_path: str, annotation_path: str, model_type: str, confidence_threshold: float = 0.5, display: bool = True, show_ground_truth: bool = False): available_models = VehicleDetectorFactory.get_available_models() if model_type not in available_models: print(f"Доступные модели: {list(available_models.keys())}") return print(f"Выбрана модель: {available_models[model_type]['name']}") print(f"Классы транспортных средств: {available_models[model_type]['vehicle_classes']}") print(f"Отображение Ground Truth: {'ВКЛ' if show_ground_truth else 'ВЫКЛ'}") try: detector = VehicleDetectorFactory.create_detector(model_type, confidence_threshold) except Exception as e: print(f"Ошибка при создании детектора: {e}") print("Убедитесь, что модели загружены в папку 'models/'") return annotation_loader = AnnotationLoader(annotation_path) evaluator = DetectionEvaluator(target_class="car") image_files = sorted(glob.glob(os.path.join(images_path, "*.jpg"))) print(f"Найдено изображений: {len(image_files)}") total_time = 0 processed_frames = 0 for image_file in image_files: image_id = os.path.splitext(os.path.basename(image_file))[0] image = cv2.imread(image_file) if image is None: print(f"Не удалось загрузить изображение: {image_file}") continue ground_truth = annotation_loader.get_ground_truth(image_id) start_time = time.time() detections = detector.detect(image) end_time = time.time() processing_time = end_time - start_time total_time += processing_time processed_frames += 1 frame_tpr, frame_fdr = evaluator.evaluate_frame(detections, ground_truth) overall_tpr, overall_fdr = evaluator.get_metrics() if display: result_image = image.copy() if show_ground_truth: result_image = self.draw_ground_truth(result_image, ground_truth) result_image = self.draw_detections(result_image, detections) result_image = self.display_metrics(result_image, overall_tpr, overall_fdr, frame_tpr, frame_fdr) cv2.imshow("Vehicle Detection", result_image) key = cv2.waitKey(1) & 0xFF if key == ord('q') or key == 27: break print(f"Обработано: {image_id}.jpg | Время: {processing_time:.3f}s | " f"TPR: {overall_tpr:.3f} | FDR: {overall_fdr:.3f} | " f"Frame TPR: {frame_tpr:.3f} | Frame FDR: {frame_fdr:.3f}") final_tpr, final_fdr = evaluator.get_metrics() avg_time = total_time / processed_frames if processed_frames > 0 else 0 print("\n" + "="*50) print("ФИНАЛЬНЫЕ РЕЗУЛЬТАТЫ:") print(f"Модель: {available_models[model_type]['name']}") print(f"Обработано кадров: {processed_frames}") print(f"Среднее время обработки: {avg_time:.3f}s") print(f"TPR (True Positive Rate): {final_tpr:.3f}") print(f"FDR (False Discovery Rate): {final_fdr:.3f}") print(f"True Positives: {evaluator.true_positives}") print(f"False Positives: {evaluator.false_positives}") print(f"False Negatives: {evaluator.false_negatives}") if display: cv2.destroyAllWindows()" - app.py из принятой работы этого года, "import os

from typing import List, Tuple, Dict

class AnnotationLoader:
def init(self, annotation_path):
self.annotation_path = annotation_path
self.annotations = self._load_annotations()

text
def _load_annotations(self): annotations = {} if not os.path.exists(self.annotation_path): return annotations with open(self.annotation_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) >= 6: image_id = parts[0].zfill(6) class_name = parts[1] if class_name.lower() == "car": x1, y1, x2, y2 = map(int, parts[2:6]) if image_id not in annotations: annotations[image_id] = [] annotations[image_id].append((x1, y1, x2, y2)) return annotations def get_ground_truth(self, image_id): return self.annotations.get(image_id, [])" - annotation_loader.py из принятой работы этого года, "import numpy as np

from typing import List, Tuple
from base_struct import BaseDetector, Detection

class SSDMobileNetDetector(BaseDetector):
def postprocess(self, outputs, image_shape):
detections = []
img_height, img_width = image_shape

text
for detection in outputs[0, 0]: confidence = float(detection[2]) class_id = int(detection[1]) if class_id == 3 and confidence > self.config.confidence_threshold: x1 = int(detection[3] * img_width) y1 = int(detection[4] * img_height) x2 = int(detection[5] * img_width) y2 = int(detection[6] * img_height) if x2 > x1 and y2 > y1: detections.append(Detection( class_id=class_id, class_name="car", confidence=confidence, bbox=(x1, y1, x2, y2) )) return detections" - mobilenet_detector.py из принятой работы этого года, "import numpy as np

from typing import List, Tuple
from base_struct import BaseDetector, Detection

class FasterRCNNDetector(BaseDetector):
def postprocess(self, outputs, image_shape):
detections = []
img_height, img_width = image_shape

text
for detection in outputs[0, 0]: confidence = float(detection[2]) class_id = int(detection[1]) if class_id == 2 and confidence > self.config.confidence_threshold: x1 = int(detection[3] * img_width) y1 = int(detection[4] * img_height) x2 = int(detection[5] * img_width) y2 = int(detection[6] * img_height) if x2 > x1 and y2 > y1: detections.append(Detection( class_id=class_id, class_name="car", confidence=confidence, bbox=(x1, y1, x2, y2) )) return detections" - rcnn_detector.py из принятой работы этого года, "import numpy as np

from typing import List, Tuple
from base_struct import BaseDetector, Detection

class YOLODetector(BaseDetector):
def postprocess(self, outputs, image_shape):
detections = []
img_height, img_width = image_shape

text
for detection in outputs: try: object_confidence = detection[4] if object_confidence < 0.1: continue class_probs = detection[5:85] class_id = np.argmax(class_probs) class_confidence = class_probs[class_id] total_confidence = object_confidence * class_confidence if class_id == 2 and total_confidence > self.config.confidence_threshold: center_x = detection[0] * img_width center_y = detection[1] * img_height width = detection[2] * img_width height = detection[3] * img_height x1 = int(center_x - width / 2) y1 = int(center_y - height / 2) x2 = int(x1 + width) y2 = int(y1 + height) x1 = max(0, x1) y1 = max(0, y1) x2 = min(img_width - 1, x2) y2 = min(img_height - 1, y2) if x2 > x1 and y2 > y1: detections.append(Detection( class_id=class_id, class_name="car", confidence=float(total_confidence), bbox=(x1, y1, x2, y2) )) except Exception as e: print(f"ERROR processing detection: {e}") continue return detections" - yolo_detector.py из принятой работы этого года, в работе этого года есть также папки configs и models, а также два текстовых файла rcnn_detector.txt и yolo_detector.txt, README: "Практическая работа №2. Детектирование объектов на изображениях с использованием библиотеки OpenCV

Алгоритмы детектирования транспортных средств
Алгоритм YOLOv4
Процесс детектирования:

Предобработка изображения

Изменение размера до 416x416 пикселей
Нормализация значений пикселей:
x

x
255.0
Конвертация изображения в 4-мерный тензор (blob)
Прямое распространение через сеть

Извлечение признаков через CSPDarknet53
Предсказание bounding boxes и классов на разных масштабах
Постобработка

Для каждой детекции вычисляется общая уверенность:
p
t
o
t
a
l

p
o
b
j
e
c
t
×
max
(
p
c
l
a
s
s
)
Выбор класса детектированного объекта:
c
^

arg

max
k
p
k
Фильтрация детекций по порогу уверенности:
p
t
o
t
a
l

τ
c
o
n
f
Преобразование координат из формата (center_x, center_y, width, height) в (x1, y1, x2, y2):
x
1

x
c

w
2
,
y
1

y
c

h
2

x
2

x
1
+
w
,
y
2

y
1
+
h
Применение Non-Maximum Suppression (NMS) для устранения дублирующих bounding boxes
Масштабирование координат обратно к исходному размеру изображения
Алгоритм SSD MobileNet
Процесс детектирования:

Предобработка изображения

Изменение размера до 600x600 пикселей
Конвертация изображения в 4-мерный тензор (blob)
Извлечение признаков

Глубинные separable свертки для эффективного извлечения признаков
Многоуровневое детектирование на разных feature maps
Постобработка

Фильтрация по порогу уверенности:
p

τ
c
o
n
f
Применение NMS для устранения дубликатов
Масштабирование координат из нормализованных [0,1] в абсолютные:
x
p
i
x

x

W
,
y
p
i
x

y

H
где W и H - ширина и высота исходного изображения
Алгоритм Faster R-CNN
Процесс детектирования:

Предобработка изображения

Изменение размера до 300x300 пикселей
Конвертация изображения в 4-мерный тензор (blob)
Генерация регионов интереса

Region Proposal Network (RPN) генерирует candidate bounding boxes
Отбор наиболее перспективных регионов
Постобработка

Фильтрация по порогу уверенности:
p

τ
c
o
n
f
Применение NMS для устранения дубликатов
Масштабирование координат из нормализованных [0,1] в абсолютные
Методика оценки качества
Для оценки качества работы детекторов применяются показатели TPR (True Positive Rate) и FDR (False Discovery Rate). Метрики вычисляются на основе пересечения предсказанных рамок с разметкой (Ground Truth) по критерию IoU (Intersection over Union).

Intersection over Union (IoU)
I
o
U
(
A
,
B
)

|
A

B
|
|
A

B
|

|
A

B
|
|
A
|
+
|
B
|

|
A

B
|

где:

A

B

  • площадь пересечения predicted и ground truth bounding boxes
    A

    B
  • площадь объединения bounding boxes
    True Positive Rate (TPR) / Recall
    T
    P
    R
    =
    T
    P
    T
    P

F
N

Показывает, какая доля реальных объектов была правильно детектирована

False Detection Rate (FDR)
F
D
R

F
P
T
P
+
F
P

Показывает, какая доля детекций является ложными срабатываниями

Критерии классификации:
True Positive (TP): Детекция считается корректной если
I
o
U

0.5
и класс правильный
False Positive (FP): Детекция с
I
o
U
<
0.5
или неправильным классом
False Negative (FN): Объект из ground truth не был детектирован
Non-Maximum Suppression (NMS)
Алгоритм применяется для устранения дублирующих bounding boxes:

Детекции сортируются по уверенности:
d
e
t
e
c
t
i
o
n
s

s
o
r
t
(
d
e
t
e
c
t
i
o
n
s
,
c
o
n
f
i
d
e
n
c
e
)
Для каждой пары детекций вычисляется IoU
Если
I
o
U
(
d
e
t
i
,
d
e
t
j
)

τ
n
m
s
, то менее уверенная детекция удаляется
Программная реализация
Структура классов:
BaseDetector - базовый класс, содержащий общую логику для всех детекторов
YOLODetector - реализация алгоритма YOLOv4
SSDMobileNetDetector - реализация алгоритма SSD MobileNet
FasterRCNNDetector - реализация алгоритма Faster R-CNN
VehicleDetectorFactory - фабрика для создания детекторов
DetectionEvaluator - класс для оценки качества детектирования
AnnotationLoader - класс для загрузки аннотаций
VehicleDetectionApp - основной класс приложения
Основные модули:
base_struct.py - базовые классы и структуры данных (Detection, ModelConfig, BaseDetector)
detectors.py - реализации конкретных детекторов (YOLO, SSD MobileNet, Faster R-CNN)
factory.py - фабрика для создания детекторов
evaluator.py - класс для оценки качества детектирования
annotation_loader.py - класс для работы с аннотациями
app.py - основной класс приложения
main.py - основной скрипт для запуска приложения
Входные параметры приложения:
--images - путь к директории с изображениями (обязательный параметр)
--annotations - путь к файлу с аннотациями (обязательный параметр)
--model - алгоритм детектирования: yolo, mobilenet или rcnn (обязательный параметр)
--confidence - порог уверенности для детекций
τ
c
o
n
f
(по умолчанию: 0.5)
--no-display - отключить отображение результатов в реальном времени
--show-ground-truth - отображать ground truth bounding boxes
Выходные данные:
Детекции транспортных средств в реальном времени
Метрики качества: True Positive Rate (TPR) и False Discovery Rate (FDR)
Визуализация bounding boxes с уверенностью
Статистика по времени обработки
Методы классов
BaseDetector
Основные методы:

preprocess - предобработка изображения для нейронной сети
postprocess - абстрактный метод для постобработки выходов сети
_apply_nms - применение Non-Maximum Suppression для фильтрации дубликатов
detect - основной метод для детектирования объектов на изображении
YOLODetector
Основные методы:

postprocess - обработка выходов YOLO сети, преобразование координат, фильтрация по уверенности
SSDMobileNetDetector
Основные методы:

postprocess - обработка выходов SSD сети, извлечение bounding boxes и классов
FasterRCNNDetector
Основные методы:

postprocess - обработка выходов Faster R-CNN сети, работа с регионами интереса
VehicleDetectorFactory
Основные методы:

get_available_models - возвращает информацию о доступных моделях
create_detector - создает экземпляр детектора по указанному типу
DetectionEvaluator
Основные методы:

calculate_iou - вычисление Intersection over Union для двух bounding boxes
evaluate_frame - оценка качества детектирования для одного кадра
get_metrics - получение агрегированных метрик качества
AnnotationLoader
Основные методы:

_load_annotations - загрузка аннотаций из файла
get_ground_truth - получение ground truth bounding boxes для конкретного изображения
VehicleDetectionApp
Основные методы:

draw_detections - отрисовка bounding boxes детекций на изображении
draw_ground_truth - отрисовка ground truth bounding boxes
display_metrics - отображение метрик качества на изображении
run - основной метод запуска приложения
Метрики качества реализованных моделей
SSD Mobilenet

TPR = 0.813
FDR = 0.023
YOLOv4

TPR = 0.787
FDR = 0.033
Faster R-CNN

TPR = 0.703
FDR = 0.066
Выводы
В ходе выполнения работы:

Реализована система детекции транспортных средств на изображениях
Поддержаны три различные архитектуры нейронных сетей
Реализованы алгоритмы предобработки и постобработки для каждой модели
Выполнена оценка качества с использованием метрик IoU, TPR, FDR
Построена модульная, расширяемая архитектура приложения
Обеспечена корректная визуализация результатов и работа с аннотациями
Модель SSD MobileNet показала наилучший баланс между точностью (TPR) и количеством ложных срабатываний (FDR), что делает её наиболее подходящей для практического применения в задачах детектирования транспортных средств." - этого года, "Практическая работа №2. Детектирование объектов на изображениях с использованием библиотеки OpenCV
Описание
Лабораторная демонстрирует использование библиотеки OpenCV и модели YOLOv4 для детектирования объектов на изображениях и видео. Реализованы алгоритмы предварительной обработки изображений, обработки выходных данных сети и визуализации результатов. Скрипт поддерживает два режима работы: обработка одиночных изображений и покадровая обработка видео.

Алгоритмы
Предварительная обработка изображений
Изменение масштаба изображения: Используется функция cv.dnn.blobFromImage для нормализации и изменения размера изображения до 416x416(стандартный размер входа для YOLOv4).
Формирование входного blob: Изображение нормализуется в диапазоне [0, 1], каналы меняются местами (RGB → BGR), и изображение подается в модель.
Обработка выхода сети
Получение выходов модели. Выполняется прямой проход через сеть (net.forward), используя выходные слои YOLO. Для каждого выхода извлекаются вероятности классов, координаты объектов и уровень их уверенности.
Фильтрация по порогу уверенности. Объекты с вероятностью ниже заданного порога, который можно задать пользователю, отбрасываются.
Non-Maximum Suppression (NMS). Алгоритм NMS используется для удаления пересекающихся ограничивающих рамок с более низкой уверенностью, основываясь на пороге nms_threshold.
Визуализация
Отрисовка рамок. Каждому объекту назначается уникальный цвет на основе его класса (цвета определяются случайным образом). На изображение накладываются рамки, отображается имя класса и уровень уверенности.
Показ статистики. В режиме обработки изображений и видео отображается количество обнаруженных объектов каждого класса.
Структура проекта
lab2.py: Основной скрипт для выполнения детектирования.
для запуска YOLOv4-модели необходимо наличие следующих файлов:
models/yolov4.cfg: Конфигурационный файл.
models/yolov4.weights: Весовые коэффициенты модели(необходимо учитывать разрешение фреймов, на которых натренирована модель)
models/coco.names: Список классов объектов (COCO dataset)
файл видео или формата .jpg" - того года, мне нужно соответствовать требованиям этого года, проект должен быть выложен в репозитории на гитхаб в папке с моей работой, какие папки и какие файлы мне нужно создать в папке моей работы на гитхаб и каким кодом должны быть заполнены эти файлы, лучше ориентироваться на эти работы как принятые - но полного плагиата быть не должно, код должен быть лаконичным понятным читаемым и не перегружен, но выполняющим требования этого года, дай полный развёрнутый ответ какие папки и файлы я должен создать, также напиши README, не давай общих советов а приведи конкретный код, старайся избегать чересчур сложных решений, основа ориентироваться на предоставленные работы но без лишнего усложнения, логика должна быть понятна и главное удовлетворять требованиям этого года

Thought

Outlining project structure and code requirements

The user is asking for a clear directory structure, specific files, and code examples to meet this year's requirements. They want everything simple, clean, and understandable, without too much complexity. I need to suggest which files and folders to create, along with a well-commented README file. I'll focus on clear code and fulfilling the given requirements, avoiding excessive complexity and unnecessary features.

Planning detector model setup and app specifications

I need to use OpenCV’s DNN module to create a class hierarchy for detectors, with various models and processing steps. The app will support one to three models and display annotated frames with detection labels, confidence, and probabilities. There’s some ambiguity in the wording around "observed class" that I need to clarify—perhaps it’s asking for a simple detection label instead of the ground truth. The display should also include TPR and FDR metrics.

Structuring app, evaluating metrics, and creating configs

The app will calculate TPR and FDR using the annotation files, just like the provided evaluator.py, but we’ll adjust the structure while keeping the same core logic. I’ll make a simpler version with renamed classes and more comments. The README.md will explain the models and processing steps. Configuration files will help avoid including actual model weights, and I’ll guide users to download them. The config will reference relative paths to make everything adaptable for students.

Designing project structure and avoiding plagiarism

I need to ensure the project is original but based on similar logic, so I'll restructure the given code. The project folder could look like this: separate folders for detectors, metrics, data, configs, and models. I’ll create clear, simple files while keeping essential functionalities, focusing on the detector class hierarchy. For simplicity, I may integrate some utils into the main app. The focus should stay on detecting models.

Simplifying directory structure keeping it clear

The user wants to avoid overcomplicating things, so I’ll go with a simpler structure. I'll suggest directories like lab2_vehicle_detection with key files: README.md, app.py, detectors, models, configs, and classes. I'll make sure to rename and restructure the code to maintain originality, while still reflecting the accepted work style. The goal is to ensure it’s understandable and adaptable, without unnecessary complexity.

Structuring project to avoid overcomplication

I’m thinking about how to structure the project based on the user’s preferences for simplicity. The key components: several detectors (YOLO, SSD MobileNet, FasterRCNN), a model for tracking ‘car’ class, and a few configurations for paths, annotations, and optional color mapping. I'll ensure it handles images, handles class-specific colors, and computes correct metrics without including too much unnecessary complexity.

Simplifying YOLOv4 detection implementation

Since the user requires a class hierarchy for detectors, I plan to keep the DetectorBase class for general detection logic. For YOLO, while maintaining simplicity, I suggest using YOLOv4-tiny-ONNX or YOLOv3-416, which simplifies output handling. I'll flatten the detection results as necessary, ensuring the logic remains clean and efficient without overcomplicating the model structure.

Designing detector_base.py for readability and simplicity

For this assignment, I'll focus on readability and clear design rather than making the code run on my side. I'll create data classes for Detection and DetectorConfig, then use a simple cv2.dnn universal loader for models. For YOLO, I’ll adjust postprocess to handle multi-output scenarios, keeping things simple. The goal is to ensure the code is easily understood and flexible.

Implementing YOLO postprocessing and evaluation

In the postprocess function, I would reshape the outputs if needed and process each detection based on its confidence and class scores. I’ll calculate bounding box coordinates and clip them, while focusing on the 'car' class for evaluation. For metrics, I’ll implement TPR and FDR, based on matches and IoU. I’ll also restructure the code to improve clarity and ensure it follows the given instructions.

Handling frame display and labels for detections

To match the spec, I'll handle optional frame display and include a 'show_ground_truth' flag. For detection colors, I'll map class names to random colors instead of using a fixed color. For labels, we'll show the predicted class and confidence inside the rectangle, and ground-truth labels above the bounding box for matched detections. This approach should meet the specification while remaining manageable.

Implementing detection logic and customization

For the detection app, I will adjust the display of labels on detection boxes by adding text inside and above the boxes for predicted and ground-truth classes. We'll also implement an option to show True Positive and False Detection ratios. The app will include a CLI to handle image paths, model selection, and display settings. For YOLO specifically, I'll create a subclass to adjust the detect method and handle multiple output layers.

Finalizing detection and evaluation logic

The post-processing for detections will convert model outputs to bounding boxes, applying filtering by confidence score based on the set threshold. For different model types (YOLO, SSD, Faster R-CNN), I'll customize detection processing and handle annotation loading via classes. Lastly, I'll implement metrics evaluation for tracking model performance, focusing on True Positive and False Detection ratios.

Planning code implementation and README structure

I need to organize drawing outputs for detection, including class labels and confidence scores. Then, I'll integrate metrics like TPR and FDR, drawing them overlayed on the frame. My application will load annotations, process detections, and display results in real-time. For the README, I must provide clear instructions, model descriptions, and placeholders for metrics. I'll ensure a non-plagiarized, original format within the defined project structure to avoid copying the accepted code.

Thought for 5m 29s

Answer

Ниже — полностью конкретный вариант структуры проекта и кода, который:

  • соответствует требованиям этого года;
  • опирается на принятые работы (твои примеры);
  • не является дословным плагиатом;
  • достаточно простой и читаемый.

1. Структура папки работы на GitHub

Предлагаю такую структуру директории с твоей работой, например папка репозитория: lab2_vehicle_detection
(название можешь поменять, главное — содержимое).

text
lab2_vehicle_detection/ ├── README.md ├── main.py ├── app.py ├── detector_base.py ├── detector_factory.py ├── yolo_detector.py ├── ssd_mobilenet_detector.py ├── faster_rcnn_detector.py ├── evaluator.py ├── annotation_loader.py ├── configs/ │ ├── coco.names │ ├── ssd_mobilenet_v3_large_coco.pbtxt │ ├── faster_rcnn_inception_v2_coco.pbtxt │ └── yolov4.cfg ├── models/ │ ├── ssd_mobilenet_v3_large_coco_2020_01_14.pb │ ├── frozen_inference_graph_faster_rcnn.pb │ └── yolov4.weights └── data/ ├── images/ # сюда кладёшь кадры (.jpg) из архива └── annotations.txt # файл разметки из задания
  • configs/ и models/ — пути для OpenCV DNN.
    • Файлы .pb, .pbtxt, .cfg, .weights ты скачиваешь из официальных источников / ссылок курса и кладёшь сюда.
  • data/images и data/annotations.txt — путь к кадрам и разметке из задания.

Дальше — конкретное содержимое каждого текстового файла.


2. detector_base.py — базовые структуры и абстрактный детектор

python
# detector_base.py from dataclasses import dataclass from typing import List, Tuple, Sequence import cv2 as cv import numpy as np from abc import ABC, abstractmethod @dataclass class Detection: """Результат детектирования одного объекта.""" class_id: int class_name: str confidence: float bbox: Tuple[int, int, int, int] # (x1, y1, x2, y2) @dataclass class DetectorConfig: """Настройки конкретной модели.""" name: str model_path: str config_path: str classes: List[str] input_size: Tuple[int, int] scale: float swap_rb: bool mean: Tuple[float, float, float] conf_threshold: float nms_threshold: float class BaseDetector(ABC): """ Базовый класс детектора. Потомки меняют только постобработку (и при необходимости forward). """ def __init__(self, config: DetectorConfig): self.config = config self.net = cv.dnn.readNet(config.model_path, config.config_path) self.net.setPreferableBackend(cv.dnn.DNN_BACKEND_OPENCV) self.net.setPreferableTarget(cv.dnn.DNN_TARGET_CPU) def _make_blob(self, image): blob = cv.dnn.blobFromImage( image, scalefactor=self.config.scale, size=self.config.input_size, mean=self.config.mean, swapRB=self.config.swap_rb, crop=False ) return blob def _forward(self, blob: np.ndarray): """Стандартный forward. YOLO переопределит этот метод.""" self.net.setInput(blob) return self.net.forward() @abstractmethod def _postprocess(self, outputs: np.ndarray, image_shape: Tuple[int, int]) -> List[Detection]: """Постобработка выходов сети в список Detection.""" pass def _apply_nms(self, detections: List[Detection]) -> List[Detection]: """Non-Maximum Suppression по всем детекциям.""" if not detections: return [] boxes = [] confidences = [] for det in detections: x1, y1, x2, y2 = det.bbox boxes.append([x1, y1, x2 - x1, y2 - y1]) confidences.append(float(det.confidence)) indices = cv.dnn.NMSBoxes( boxes, confidences, self.config.conf_threshold, self.config.nms_threshold ) if len(indices) == 0: return [] indices = indices.flatten() return [detections[i] for i in indices] def detect(self, image) -> List[Detection]: """Полный цикл детектирования: препроцесс, forward, постпроцесс, NMS.""" blob = self._make_blob(image) outputs = self._forward(blob) raw_detections = self._postprocess(outputs, image.shape[:2]) return self._apply_nms(raw_detections)

3. Детекторы для трёх моделей

3.1 yolo_detector.py — YOLOv4 (Darknet)

python
# yolo_detector.py from typing import List, Tuple import numpy as np import cv2 as cv from detector_base import BaseDetector, Detection, DetectorConfig class YOLOv4Detector(BaseDetector): """Детектор на основе YOLOv4 (Darknet).""" def __init__(self, config: DetectorConfig): super().__init__(config) # Имена выходных слоёв YOLO layer_names = self.net.getLayerNames() self.output_layer_names = [layer_names[i - 1] for i in self.net.getUnconnectedOutLayers().flatten()] def _forward(self, blob): self.net.setInput(blob) layer_outputs = self.net.forward(self.output_layer_names) # Склеиваем все выходы в один массив N×85 return np.vstack(layer_outputs) def _postprocess(self, outputs: np.ndarray, image_shape: Tuple[int, int]) -> List[Detection]: img_h, img_w = image_shape detections: List[Detection] = [] # outputs: N x (4 + 1 + num_classes) (x, y, w, h, obj, class_scores...) for row in outputs: object_score = float(row[4]) if object_score < 0.1: continue class_scores = row[5:] class_id = int(np.argmax(class_scores)) class_score = float(class_scores[class_id]) confidence = object_score * class_score if confidence < self.config.conf_threshold: continue cx, cy, w, h = row[0], row[1], row[2], row[3] cx *= img_w cy *= img_h w *= img_w h *= img_h x1 = int(cx - w / 2) y1 = int(cy - h / 2) x2 = int(cx + w / 2) y2 = int(cy + h / 2) x1 = max(0, min(x1, img_w - 1)) y1 = max(0, min(y1, img_h - 1)) x2 = max(0, min(x2, img_w - 1)) y2 = max(0, min(y2, img_h - 1)) if x2 <= x1 or y2 <= y1: continue if 0 <= class_id < len(self.config.classes): class_name = self.config.classes[class_id] else: class_name = f"class_{class_id}" detections.append( Detection( class_id=class_id, class_name=class_name, confidence=confidence, bbox=(x1, y1, x2, y2) ) ) return detections

3.2 ssd_mobilenet_detector.py — SSD MobileNet

python
# ssd_mobilenet_detector.py from typing import List, Tuple import numpy as np from detector_base import BaseDetector, Detection, DetectorConfig class SSDMobileNetDetector(BaseDetector): """Детектор на основе SSD MobileNet (TensorFlow).""" def _postprocess(self, outputs, image_shape: Tuple[int, int]) -> List[Detection]: img_h, img_w = image_shape detections: List[Detection] = [] # Стандартный формат TF-детектора: [1, 1, N, 7] output_array = outputs[0, 0] for det in output_array: confidence = float(det[2]) if confidence < self.config.conf_threshold: continue class_id = int(det[1]) x1 = int(det[3] * img_w) y1 = int(det[4] * img_h) x2 = int(det[5] * img_w) y2 = int(det[6] * img_h) if x2 <= x1 or y2 <= y1: continue if 0 <= class_id < len(self.config.classes): class_name = self.config.classes[class_id] else: class_name = f"class_{class_id}" detections.append( Detection( class_id=class_id, class_name=class_name, confidence=confidence, bbox=(x1, y1, x2, y2) ) ) return detections

3.3 faster_rcnn_detector.py — Faster R-CNN

python
# faster_rcnn_detector.py from typing import List, Tuple import numpy as np from detector_base import BaseDetector, Detection, DetectorConfig class FasterRCNNDetector(BaseDetector): """Детектор на основе Faster R-CNN (TensorFlow).""" def _postprocess(self, outputs, image_shape: Tuple[int, int]) -> List[Detection]: img_h, img_w = image_shape detections: List[Detection] = [] # Формат такой же, как у SSD TF-детектора: [1, 1, N, 7] output_array = outputs[0, 0] for det in output_array: confidence = float(det[2]) if confidence < self.config.conf_threshold: continue class_id = int(det[1]) x1 = int(det[3] * img_w) y1 = int(det[4] * img_h) x2 = int(det[5] * img_w) y2 = int(det[6] * img_h) if x2 <= x1 or y2 <= y1: continue if 0 <= class_id < len(self.config.classes): class_name = self.config.classes[class_id] else: class_name = f"class_{class_id}" detections.append( Detection( class_id=class_id, class_name=class_name, confidence=confidence, bbox=(x1, y1, x2, y2) ) ) return detections

4. detector_factory.py — фабрика детекторов и конфиги моделей

python
# detector_factory.py from typing import Dict, Any, List import os from detector_base import DetectorConfig from yolo_detector import YOLOv4Detector from ssd_mobilenet_detector import SSDMobileNetDetector from faster_rcnn_detector import FasterRCNNDetector class VehicleDetectorFactory: """Фабрика для создания детекторов транспортных средств.""" @staticmethod def _load_classes(names_path: str) -> List[str]: with open(names_path, "r", encoding="utf-8") as f: return [line.strip() for line in f if line.strip()] @staticmethod def get_available_models() -> Dict[str, Dict[str, Any]]: """Справочная информация о моделях (для печати в приложении).""" return { "yolo": { "name": "YOLOv4 (Darknet)", "vehicle_classes": ["car", "bus", "truck"] }, "ssd": { "name": "SSD MobileNet v3", "vehicle_classes": ["car", "bus", "truck"] }, "rcnn": { "name": "Faster R-CNN", "vehicle_classes": ["car", "bus", "truck"] }, } @staticmethod def create_detector(model_key: str, confidence_threshold: float): """Создание детектора по ключу 'yolo' / 'ssd' / 'rcnn'.""" coco_names_path = os.path.join("configs", "coco.names") classes = VehicleDetectorFactory._load_classes(coco_names_path) if model_key == "yolo": config = DetectorConfig( name="YOLOv4 (Darknet)", model_path=os.path.join("models", "yolov4.weights"), config_path=os.path.join("configs", "yolov4.cfg"), classes=classes, input_size=(416, 416), scale=1.0 / 255.0, swap_rb=True, mean=(0.0, 0.0, 0.0), conf_threshold=confidence_threshold, nms_threshold=0.4, ) return YOLOv4Detector(config) if model_key == "ssd": config = DetectorConfig( name="SSD MobileNet v3", model_path=os.path.join("models", "ssd_mobilenet_v3_large_coco_2020_01_14.pb"), config_path=os.path.join("configs", "ssd_mobilenet_v3_large_coco.pbtxt"), classes=classes, input_size=(300, 300), scale=1.0 / 127.5, swap_rb=True, mean=(127.5, 127.5, 127.5), conf_threshold=confidence_threshold, nms_threshold=0.4, ) return SSDMobileNetDetector(config) if model_key == "rcnn": config = DetectorConfig( name="Faster R-CNN", model_path=os.path.join("models", "frozen_inference_graph_faster_rcnn.pb"), config_path=os.path.join("configs", "faster_rcnn_inception_v2_coco.pbtxt"), classes=classes, input_size=(300, 300), scale=1.0, swap_rb=True, mean=(0.0, 0.0, 0.0), conf_threshold=confidence_threshold, nms_threshold=0.4, ) return FasterRCNNDetector(config) raise ValueError(f"Неизвестная модель: {model_key}")

5. annotation_loader.py — загрузка разметки

python
# annotation_loader.py import os from typing import Dict, List, Tuple class AnnotationLoader: """ Загружает разметку из текстового файла. Формат строки: image_id class_name x1 y1 x2 y2 ... Используем только объекты класса 'car' (как в примере). """ def __init__(self, annotation_path: str, target_class: str = "car"): self.annotation_path = annotation_path self.target_class = target_class.lower() self._annotations: Dict[str, List[Tuple[int, int, int, int]]] = self._load() def _load(self) -> Dict[str, List[Tuple[int, int, int, int]]]: annotations: Dict[str, List[Tuple[int, int, int, int]]] = {} if not os.path.exists(self.annotation_path): print(f"Файл аннотаций не найден: {self.annotation_path}") return annotations with open(self.annotation_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) < 6: continue image_id_raw = parts[0] class_name = parts[1].lower() if class_name != self.target_class: continue x1, y1, x2, y2 = map(int, parts[2:6]) # На всякий случай нормализуем до 6-значного id image_id = image_id_raw.zfill(6) if image_id not in annotations: annotations[image_id] = [] annotations[image_id].append((x1, y1, x2, y2)) return annotations def get_ground_truth(self, image_id: str) -> List[Tuple[int, int, int, int]]: """Возвращает список GT-боксов для данного кадра.""" return self._annotations.get(image_id, [])

6. evaluator.py — вычисление TPR и FDR

python
# evaluator.py from typing import List, Tuple from detector_base import Detection class DetectionEvaluator: """ Подсчёт метрик TPR и FDR по одному целевому классу (car). """ def __init__(self, target_class: str = "car"): self.target_class = target_class.lower() self.true_positives = 0 self.false_positives = 0 self.false_negatives = 0 @staticmethod def _iou(box_a: Tuple[int, int, int, int], box_b: Tuple[int, int, int, int]) -> float: x1_a, y1_a, x2_a, y2_a = box_a x1_b, y1_b, x2_b, y2_b = box_b x_left = max(x1_a, x1_b) y_top = max(y1_a, y1_b) x_right = min(x2_a, x2_b) y_bottom = min(y2_a, y2_b) inter_w = max(0, x_right - x_left) inter_h = max(0, y_bottom - y_top) intersection = inter_w * inter_h area_a = (x2_a - x1_a) * (y2_a - y1_a) area_b = (x2_b - x1_b) * (y2_b - y1_b) union = area_a + area_b - intersection if union <= 0: return 0.0 return intersection / union def evaluate_frame(self, detections: List[Detection], gt_boxes: List[Tuple[int, int, int, int]], iou_threshold: float = 0.5) -> Tuple[float, float]: """ Обновляет глобальные счётчики и возвращает TPR/FDR для кадра. """ frame_tp = 0 frame_fp = 0 frame_fn = 0 gt_used = [False] * len(gt_boxes) # Сортируем по уверенности, чтобы более уверенные детекции матчились первыми candidate_detections = sorted( [d for d in detections if d.class_name.lower() == self.target_class], key=lambda d: d.confidence, reverse=True ) for det in candidate_detections: best_iou = 0.0 best_idx = -1 for idx, gt in enumerate(gt_boxes): if gt_used[idx]: continue iou = self._iou(det.bbox, gt) if iou > best_iou: best_iou = iou best_idx = idx if best_iou >= iou_threshold and best_idx != -1: frame_tp += 1 self.true_positives += 1 gt_used[best_idx] = True else: frame_fp += 1 self.false_positives += 1 # Все неиспользованные GT — это FN unused_gt = sum(1 for used in gt_used if not used) frame_fn += unused_gt self.false_negatives += unused_gt frame_tpr = frame_tp / (frame_tp + frame_fn) if (frame_tp + frame_fn) > 0 else 0.0 frame_fdr = frame_fp / (frame_tp + frame_fp) if (frame_tp + frame_fp) > 0 else 0.0 return frame_tpr, frame_fdr def get_metrics(self) -> Tuple[float, float]: """Глобальные TPR и FDR по всем обработанным кадрам.""" tpr = self.true_positives / (self.true_positives + self.false_negatives) \ if (self.true_positives + self.false_negatives) > 0 else 0.0 fdr = self.false_positives / (self.true_positives + self.false_positives) \ if (self.true_positives + self.false_positives) > 0 else 0.0 return tpr, fdr def reset(self): self.true_positives = 0 self.false_positives = 0 self.false_negatives = 0

7. app.py — основной класс приложения

python
# app.py import os import glob import time from typing import List, Tuple import cv2 as cv import numpy as np from detector_base import Detection from detector_factory import VehicleDetectorFactory from annotation_loader import AnnotationLoader from evaluator import DetectionEvaluator class VehicleDetectionApp: """ Демонстрационное приложение: - загрузка кадров; - запуск выбранной модели; - отображение детекций и Ground Truth; - вычисление TPR и FDR. """ def __init__(self): self.gt_color = (255, 0, 255) # фиолетовый для GT self.text_color = (255, 255, 255) self.class_colors = {} # class_name -> BGR def _get_color_for_class(self, class_name: str): if class_name not in self.class_colors: color = tuple(int(c) for c in np.random.randint(0, 255, size=3)) self.class_colors[class_name] = color return self.class_colors[class_name] def _draw_detections(self, image, detections: List[Detection]): result = image.copy() for det in detections: x1, y1, x2, y2 = det.bbox class_name = det.class_name color = self._get_color_for_class(class_name) conf = det.confidence # Прямоугольник cv.rectangle(result, (x1, y1), (x2, y2), color, 2) # Текст в левом верхнем углу прямоугольника: "class: conf" label_inside = f"{class_name}: {conf:.3f}" (tw, th), baseline = cv.getTextSize( label_inside, cv.FONT_HERSHEY_SIMPLEX, 0.5, 1 ) text_x_inside = x1 + 2 text_y_inside = y1 + th + 2 cv.rectangle( result, (x1, y1), (x1 + tw + 4, y1 + th + baseline + 4), color, thickness=cv.FILLED ) cv.putText( result, label_inside, (text_x_inside, text_y_inside), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1 ) # Над прямоугольником — наблюдаемый класс (как требует задание) observed_label = class_name (tw2, th2), base2 = cv.getTextSize( observed_label, cv.FONT_HERSHEY_SIMPLEX, 0.5, 1 ) text_x_above = x1 text_y_above = max(0, y1 - 5) cv.putText( result, observed_label, (text_x_above, text_y_above), cv.FONT_HERSHEY_SIMPLEX, 0.5, color, 1 ) return result def _draw_ground_truth(self, image, gt_boxes: List[Tuple[int, int, int, int]]): result = image.copy() for bbox in gt_boxes: x1, y1, x2, y2 = bbox cv.rectangle(result, (x1, y1), (x2, y2), self.gt_color, 2) label = "GT" (tw, th), baseline = cv.getTextSize( label, cv.FONT_HERSHEY_SIMPLEX, 0.5, 1 ) text_x = x1 text_y = max(0, y1 - 5) cv.rectangle( result, (text_x, text_y - th - baseline), (text_x + tw + 4, text_y + baseline), self.gt_color, thickness=cv.FILLED ) cv.putText( result, label, (text_x + 2, text_y), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1 ) return result def _draw_metrics(self, image, global_tpr: float, global_fdr: float, frame_tpr: float, frame_fdr: float): result = image.copy() lines = [ f"TPR: {global_tpr:.3f}", f"FDR: {global_fdr:.3f}", f"Frame TPR: {frame_tpr:.3f}", f"Frame FDR: {frame_fdr:.3f}", ] for i, text in enumerate(lines): cv.putText( result, text, (10, 30 + i * 25), cv.FONT_HERSHEY_SIMPLEX, 0.7, self.text_color, 2 ) return result def run(self, images_dir: str, annotations_path: str, model_key: str, confidence_threshold: float = 0.5, display: bool = True, show_ground_truth: bool = False): """ Главный метод запуска: - images_dir — путь к .jpg кадрам; - annotations_path — путь к файлу разметки; - model_key — 'yolo' / 'ssd' / 'rcnn'; - confidence_threshold — порог уверенности; - display — показывать окна с результатами; - show_ground_truth — рисовать ли GT-боксы. """ available = VehicleDetectorFactory.get_available_models() if model_key not in available: print("Неизвестная модель.") print("Доступные модели:", ", ".join(available.keys())) return model_info = available[model_key] print(f"Выбрана модель: {model_info['name']}") print(f"Классы транспортных средств: {model_info['vehicle_classes']}") print(f"Отображение GT: {'ВКЛ' if show_ground_truth else 'ВЫКЛ'}") # Создаём детектор try: detector = VehicleDetectorFactory.create_detector(model_key, confidence_threshold) except Exception as e: print(f"Ошибка при создании детектора: {e}") print("Проверьте наличие файлов в папках models/ и configs/") return # Загрузка разметки и инициализация оценщика annotation_loader = AnnotationLoader(annotations_path, target_class="car") evaluator = DetectionEvaluator(target_class="car") image_paths = sorted(glob.glob(os.path.join(images_dir, "*.jpg"))) print(f"Найдено изображений: {len(image_paths)}") total_time = 0.0 processed_frames = 0 for image_path in image_paths: image_id = os.path.splitext(os.path.basename(image_path))[0] image = cv.imread(image_path) if image is None: print(f"Не удалось прочитать изображение: {image_path}") continue gt_boxes = annotation_loader.get_ground_truth(image_id) start = time.time() detections = detector.detect(image) end = time.time() frame_time = end - start total_time += frame_time processed_frames += 1 frame_tpr, frame_fdr = evaluator.evaluate_frame(detections, gt_boxes) global_tpr, global_fdr = evaluator.get_metrics() if display: frame_to_show = image.copy() if show_ground_truth: frame_to_show = self._draw_ground_truth(frame_to_show, gt_boxes) frame_to_show = self._draw_detections(frame_to_show, detections) frame_to_show = self._draw_metrics( frame_to_show, global_tpr, global_fdr, frame_tpr, frame_fdr ) cv.imshow("Vehicle Detection", frame_to_show) key = cv.waitKey(1) & 0xFF if key == ord('q') or key == 27: break print( f"{image_id}.jpg | time: {frame_time:.3f}s | " f"TPR: {global_tpr:.3f} | FDR: {global_fdr:.3f} | " f"frame TPR: {frame_tpr:.3f} | frame FDR: {frame_fdr:.3f}" ) final_tpr, final_fdr = evaluator.get_metrics() avg_time = total_time / processed_frames if processed_frames > 0 else 0.0 print("\n" + "=" * 50) print("ИТОГОВЫЕ РЕЗУЛЬТАТЫ:") print(f"Модель: {model_info['name']}") print(f"Обработано кадров: {processed_frames}") print(f"Среднее время обработки: {avg_time:.3f}s") print(f"TPR: {final_tpr:.3f}") print(f"FDR: {final_fdr:.3f}") print(f"True Positives: {evaluator.true_positives}") print(f"False Positives: {evaluator.false_positives}") print(f"False Negatives: {evaluator.false_negatives}") if display: cv.destroyAllWindows()

8. main.py — точка входа (CLI)

python
# main.py import argparse from app import VehicleDetectionApp def build_arg_parser(): parser = argparse.ArgumentParser( description="Практическая работа №2. Детектирование транспортных средств с OpenCV DNN" ) parser.add_argument( "--images", "-i", type=str, required=True, help="Путь к директории с кадрами (.jpg)" ) parser.add_argument( "--annotations", "-a", type=str, required=True, help="Путь к файлу с разметкой (annotations.txt)" ) parser.add_argument( "--model", "-m", type=str, required=True, choices=["yolo", "ssd", "rcnn"], help="Модель детектирования: yolo | ssd | rcnn" ) parser.add_argument( "--conf", type=float, default=0.5, help="Порог уверенности (по умолчанию 0.5)" ) parser.add_argument( "--no-display", action="store_true", help="Не показывать окна с визуализацией" ) parser.add_argument( "--show-gt", action="store_true", help="Отрисовывать Ground Truth (разметку)" ) return parser def main(): args = build_arg_parser().parse_args() app = VehicleDetectionApp() app.run( images_dir=args.images, annotations_path=args.annotations, model_key=args.model, confidence_threshold=args.conf, display=not args.no_display, show_ground_truth=args.show_gt ) if __name__ == "__main__": main()

9. configs/coco.names — список классов COCO

Создай файл configs/coco.names со стандартными названиями классов COCO (для краткости оставлю полный список, чтобы не пришлось искать):

text
person bicycle car motorbike aeroplane bus train truck boat traffic light fire hydrant stop sign parking meter bench bird cat dog horse sheep cow elephant bear zebra giraffe backpack umbrella handbag tie suitcase frisbee skis snowboard sports ball kite baseball bat baseball glove skateboard surfboard tennis racket bottle wine glass cup fork knife spoon bowl banana apple sandwich orange broccoli carrot hot dog pizza donut cake chair sofa pottedplant bed diningtable toilet tvmonitor laptop mouse remote keyboard cell phone microwave oven toaster sink refrigerator book clock vase scissors teddy bear hair drier toothbrush

Файлы *.cfg, *.pbtxt, *.pb, *.weights ты просто скачиваешь и кладёшь по указанным путям — код выше их использует.


10. README.md

Создай README.md в корне проекта:

markdown
# Практическая работа №2. Детектирование объектов на изображениях с использованием библиотеки OpenCV ## Цель работы Реализовать приложение для детектирования транспортных средств на последовательности кадров с использованием нейронных сетей из модуля `cv.dnn` библиотеки OpenCV. Приложение должно: - поддерживать **несколько моделей** (YOLOv4, SSD MobileNet, Faster R-CNN); - иметь **иерархию классов детекторов** с разной пред- и постобработкой; - загружать кадры из директории, сравнивать детекции с разметкой и считать метрики **TPR** и **FDR**; - опционально отображать кадры с отрисованными прямоугольниками разных цветов для разных классов. --- ## Структура проекта ```text lab2_vehicle_detection/ ├── README.md ├── main.py # точка входа (CLI) ├── app.py # логика приложения, визуализация, метрики ├── detector_base.py # базовые структуры и абстрактный детектор ├── detector_factory.py # фабрика детекторов и конфиги моделей ├── yolo_detector.py # детектор YOLOv4 ├── ssd_mobilenet_detector.py # детектор SSD MobileNet ├── faster_rcnn_detector.py # детектор Faster R-CNN ├── evaluator.py # вычисление TPR и FDR ├── annotation_loader.py # загрузка аннотаций (Ground Truth) ├── configs/ │ ├── coco.names # классы COCO │ ├── ssd_mobilenet_v3_large_coco.pbtxt │ ├── faster_rcnn_inception_v2_coco.pbtxt │ └── yolov4.cfg ├── models/ │ ├── ssd_mobilenet_v3_large_coco_2020_01_14.pb │ ├── frozen_inference_graph_faster_rcnn.pb │ └── yolov4.weights └── data/ ├── images/ # кадры (.jpg) из архива задания └── annotations.txt # файл разметки

Файлы моделей (*.pb, *.pbtxt, *.cfg, *.weights) скачиваются из официальных репозиториев / ссылок курса и кладутся в папки models/ и configs/.


Алгоритмы детектирования

1. YOLOv4

Предобработка изображения

  • изменение размера до 416×416 пикселей;
  • нормализация пикселей: умножение на 1/255.0;
  • формирование 4D-тензора (blob) через cv.dnn.blobFromImage с параметрами:
    • swapRB=True,
    • mean=(0, 0, 0),
    • crop=False.

Прямое распространение

  • сеть загружается из yolov4.cfg и yolov4.weights;
  • для получения выходов используются имена выходных слоёв (через getUnconnectedOutLayers);
  • выполняется forward по этим слоям, выходы объединяются в массив детекций N × (4 + 1 + C).

Постобработка

  • каждая детекция содержит:
    • нормализованные координаты центра и размеров бокса (x, y, w, h),
    • оценку objectness,
    • вероятности по классам;
  • вычисляется итоговая уверенность: ptotal=pobjectmaxkpkp_{\text{total}} = p_{\text{object}} \cdot \max_k p_k
  • выбирается класс: c^=argmaxkpk\hat{c} = \arg\max_k p_k
  • отбрасываются детекции с p_total < threshold;
  • координаты переводятся к формату (x1, y1, x2, y2) и масштабируются к размеру исходного изображения;
  • применяется Non-Maximum Suppression (NMS) для удаления дубликатов.

2. SSD MobileNet

Предобработка

  • изменение размера до 300×300;
  • нормализация к диапазону [-1, 1]:
    • scale = 1/127.5, mean=(127.5, 127.5, 127.5), swapRB=True;
  • формирование blob через cv.dnn.blobFromImage.

Выход модели

  • формат outputs[0, 0, N, 7], где каждая запись: (\text{batch_id}, \text{class_id}, p, x_1, y_1, x_2, y_2) с координатами в нормализованном виде [0, 1].

Постобработка

  • отбираются детекции с p ≥ threshold;
  • координаты масштабируются: xpx=xW,ypx=yHx_{\text{px}} = x \cdot W,\quad y_{\text{px}} = y \cdot H
  • применяется NMS на стороне базового класса.

3. Faster R-CNN

Предобработка

  • изменение размера до 300×300 (для используемой TF-модели);
  • формирование blob без дополнительной нормализации (scale = 1.0);
  • swapRB=True.

Прямое распространение и постобработка

  • формат выходов такой же, как у SSD ([1, 1, N, 7]);
  • порог по уверенности и масштабирование координат аналогичны SSD;
  • NMS применяется в базовом классе.

Методика оценки качества

Для оценки используются метрики:

  • TPR (True Positive Rate / Recall): TPR=TPTP+FNTPR = \frac{TP}{TP + FN}
  • FDR (False Discovery Rate): FDR=FPTP+FPFDR = \frac{FP}{TP + FP}

Сопоставление предсказаний с разметкой выполняется по метрике Intersection over Union (IoU):

IoU(A,B)=ABAB=ABA+BABIoU(A, B) = \frac{|A \cap B|}{|A \cup B|} = \frac{|A \cap B|} {|A| + |B| - |A \cap B|}

Критерий:

  • True Positive (TP) — если IoU ≥ 0.5 и класс совпадает (car);
  • False Positive (FP) — детекция без подходящего GT (IoU < 0.5 или отсутствует GT);
  • False Negative (FN) — GT-бокс, не покрытый ни одной детекцией.

Алгоритм реализован в evaluator.py:

  • для каждого кадра детекции сортируются по убыванию уверенности;
  • каждая детекция матчится с наилучшим GT-боксом, если IoU ≥ 0.5;
  • обновляются глобальные счётчики TP, FP, FN;
  • вычисляются кадровые TPR/FDR и агрегированные TPR/FDR.

Визуализация

При отображении (если не указано --no-display):

  • каждая детекция рисуется прямоугольником:
    • цвет зависит от класса (разные классы — разные цвета);
  • в левом верхнем углу прямоугольника отображается:
    • "<класс>: <уверенность>" с точностью до трёх знаков после запятой;
  • над прямоугольником дополнительно подписывается наблюдаемый класс объекта;
  • Ground Truth-боксы (если --show-gt) рисуются фиолетовым цветом и помечаются подписью GT;
  • в левом верхнем углу кадра выводятся текущие значения:
    • TPR, FDR,
    • Frame TPR, Frame FDR.

Запуск приложения

Пример запуска из корня проекта:

bash
python main.py \ --images data/images \ --annotations data/annotations.txt \ --model yolo \ --conf 0.5 \ --show-gt

Параметры:

  • --images — путь к директории с кадрами (.jpg);
  • --annotations — путь к текстовому файлу разметки;
  • --model — одна из моделей: yolo, ssd, rcnn;
  • --conf — порог уверенности детекций;
  • --no-display — отключить показ окон OpenCV;
  • --show-gt — отрисовывать Ground Truth-боксы.

Метрики качества (пример)

После запуска выводятся итоговые значения:

  • TPR (True Positive Rate) — доля правильно найденных машин;
  • FDR (False Discovery Rate) — доля ложных срабатываний среди всех детекций.

Дополнительно: согласно критериям оценки лабораторной, за качество детектирования добавляется
10 * TPR баллов (по итоговому TPR, рассчитанному приложением).

В отчёте/защите можно привести полученные значения TPR и FDR отдельно для каждой модели
(SSD MobileNet, YOLOv4, Faster R-CNN) после запуска на полном наборе кадров.

text
--- Если хочешь, дальше можно будет подогнать имена папок/файлов под точные требования преподавателя (например, назвать корневую папку `Practicum2_Фамилия` и т.п.), но с таким содержимым ты будешь полностью соответствовать требованиям этого года: три модели, иерархия классов, OpenCV DNN, визуализация, TPR/FDR и README с описанием пред-/постобработки.

Share this Q&A