"Практическая работа №2. Детектирование объектов н...
Criado em: 18 de novembro de 2025
Respondido usando GPT-5.1 Thinking por Chat01
Criado em: 18 de novembro de 2025
Respondido usando GPT-5.1 Thinking por Chat01
"Практическая работа №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)
textwhile 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]
textblob = 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)
textlabel_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()
textapp = 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
textdef 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
textfor 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)
textdef 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)
textdef 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()
textdef _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
textfor 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
textfor 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
textfor 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
Процесс детектирования:
Предобработка изображения
x
255.0
Конвертация изображения в 4-мерный тензор (blob)
Прямое распространение через сеть
Извлечение признаков через CSPDarknet53
Предсказание bounding boxes и классов на разных масштабах
Постобработка
y
c
−
h
2
y
1
+
h
Применение Non-Maximum Suppression (NMS) для устранения дублирующих bounding boxes
Масштабирование координат обратно к исходному размеру изображения
Алгоритм SSD MobileNet
Процесс детектирования:
Предобработка изображения
Изменение размера до 600x600 пикселей
Конвертация изображения в 4-мерный тензор (blob)
Извлечение признаков
Глубинные separable свертки для эффективного извлечения признаков
Многоуровневое детектирование на разных feature maps
Постобработка
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).
|
A
∩
B
|
|
A
|
+
|
B
|
−
|
A
∩
B
|
где:
A
∩
B
F
N
Показывает, какая доля реальных объектов была правильно детектирована
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, не давай общих советов а приведи конкретный код, старайся избегать чересчур сложных решений, основа ориентироваться на предоставленные работы но без лишнего усложнения, логика должна быть понятна и главное удовлетворять требованиям этого года
Ниже — полностью конкретный вариант структуры проекта и кода, который:
Предлагаю такую структуру директории с твоей работой, например папка репозитория: lab2_vehicle_detection
(название можешь поменять, главное — содержимое).
textlab2_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 — путь к кадрам и разметке из задания.Дальше — конкретное содержимое каждого текстового файла.
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)
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
ssd_mobilenet_detector.py — SSD MobileNetpython# 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
faster_rcnn_detector.py — Faster R-CNNpython# 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
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}")
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, [])
evaluator.py — вычисление TPR и FDRpython# 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
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()
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()
configs/coco.names — список классов COCOСоздай файл configs/coco.names со стандартными названиями классов COCO (для краткости оставлю полный список, чтобы не пришлось искать):
textperson 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 ты просто скачиваешь и кладёшь по указанным путям — код выше их использует.
Создай 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/.
Предобработка изображения
416×416 пикселей;1/255.0;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,p_total < threshold;(x1, y1, x2, y2) и масштабируются к размеру исходного изображения;Предобработка
300×300;[-1, 1]:
scale = 1/127.5, mean=(127.5, 127.5, 127.5), swapRB=True;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;Предобработка
300×300 (для используемой TF-модели);swapRB=True.Прямое распространение и постобработка
[1, 1, N, 7]);Для оценки используются метрики:
Сопоставление предсказаний с разметкой выполняется по метрике Intersection over Union (IoU):
Критерий:
IoU ≥ 0.5 и класс совпадает (car);Алгоритм реализован в evaluator.py:
IoU ≥ 0.5;TP, FP, FN;При отображении (если не указано --no-display):
"<класс>: <уверенность>" с точностью до трёх знаков после запятой;--show-gt) рисуются фиолетовым цветом и помечаются подписью GT;TPR, FDR,Frame TPR, Frame FDR.Пример запуска из корня проекта:
bashpython 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 с описанием пред-/постобработки.