Refactor/component based architecture
Created by: Serebro1
В данном pull request я попытался разделить обязанности визуализатора отображать данные в определённом виде и всей работы нашего приложения. В результате я выделил 2 отдельных класса: Pipeline Components и Detection Pipeline
Основные изменения:
Pipeline Components
Данный класс является некоторым хранилищем всех используемых компонентов, за исключением AccuracyCalculator, так как данный модуль используется отдельно для вычисления метрик с использованием готового файла с детекциями и готовой разметкой. С помощью этого класса можно сформировать разные конфигурации для запуска детектора, например, с использованием UI библиотеки OPENCV или консольного вывода. Код:
@dataclass
class PipelineComponents:
"""Container for all pipeline components"""
reader: FrameDataReader
detector: Detector
visualizer: BaseVisualizer
writer: Writer = None
gt_reader: dr.DataReader = None
Detection Pipeline
Данный класс является ядром программы. В него передаётся объект класса PipelineComponents, заранее сформированная конфигурация работы нашего детектора. Код:
class DetectionPipeline:
def __init__(self, components: PipelineComponents):
"""Если в компонентах не передана визуальная часть,
детектор или датаридер видео/изображений, то выкинется исключение"""
self.components = components
self.gtboxes = None
def run(self):
try:
with self.components.reader as reader:
self.components.visualizer.initialize(reader.get_total_images())
if self.components.gt_reader:
self.gtboxes = self.components.gt_reader.read()
for frame_idx, frame in enumerate(reader):
self._process_frame(frame_idx, frame)
self.components.visualizer.update_progress()
if self._should_exit():
break
except Exception as e:
self._handle_error(e)
finally:
self._finalize()
Главный метод данного класса run() выполняет следующее:
- Использование датаридера видео и изображения реализуется контекстным менеджером, который в конструкции with проинициализирует ресурсы и в конце освободит их (важно для датаридера видео).
- Инициализация визуальной части, передаётся число кадров, для отслеживания процесса работы.
- Если есть разметка, то считываем её и сохраняем в классе для дальнейшего использования
- Запускается цикл прохода по кадрам. 4.1. Обработка изображения. Детектор выдаёт набор срабатываний, который при наличии компонента write запишет результат в файл и происходит визуализация изображения, в зависимости от того, какой визуализатор используем. 4.2. В визуализаторе обновляются данные о прогрессе и выводит статус выполнения (прогресс бар или обычный вывод в консоль) 4.3. Если была нажата клавиша прерывания, то цикл прервётся.
- Если произошло исключение, происходит очищение файла с набором записанных в процессе детекций и исключение перекидывается дальше.
- В конце работы цикла или при исключении происходит освобождение всех ресурсов и подтверждение об окончании цикла.
Main()
Таким образом примерно следующий main, с использованием новых классов, можно просто импортировать различные конфигурации и использовать в одном main. Полный код представлен в файле new_main.py
def cli_argument_parser():
parser = argparse.ArgumentParser()
"""аргументы, ожидаемые в консоли"""
args = parser.parse_args()
return args
def config_visual_main(args: argparse.Namespace):
return PipelineComponents(
reader = FrameDataReader.create(args.mode, (args.video_path or args.images_path)),
detector = Detector.create( "fake" ),
visualizer = vis.GUIVisualizer(),
writer = Writer.create(args.write_path) if args.write_path else None,
gt_reader = dr.CsvGTReader(args.groundtruth_path) if args.groundtruth_path else None)
def config_cli_main(args: argparse.Namespace):
return PipelineComponents(
reader = FrameDataReader.create(args.mode, (args.video_path or args.images_path)),
detector = Detector.create( "fake" ),
visualizer = vis.CLIVisualizer(),
writer = Writer.create(args.write_path) if args.write_path else None,
gt_reader = dr.CsvGTReader(args.groundtruth_path) if args.groundtruth_path else None)
def main():
try:
args = cli_argument_parser()
components = config_cli_main(args)
pipeline = DetectionPipeline(components)
pipeline.run()
if args.groundtruth_path and args.write_path is not None:
accur_calc = AccuracyCalculator()
accur_calc.load_detections(args.write_path)
accur_calc.load_groundtruths(args.groundtruth_path)
print (f"TPR: {accur_calc.calc_tpr()}\n"
f"FDR: {accur_calc.calc_fdr()}\n"
f"MAP: {accur_calc.calc_map()}")
except Exception as e:
print(e)
if __name__ == '__main__':
main()
В данном main мы вводим аргументы, передаём конфигуратору, который создаст компоненты и вернёт их. Далее передаём их конвейеру и запускаем. Если мы передали в аргументы путь к разметке и путь к файлу с детекциями, то считаем и выводим метрики.
Остальные изменения:
Класс FrameDataReader Добавлены функции enter и exit для использования контекстного менеджера и get_total_frames для получения числа кадров видео и изображений в папке.
Модуль Visualizer Создан абстрактный класс BaseVisualizer и наследники GUIVisualizer и CLIVisualizer
class BaseVisualizer(ABC):
@abstractmethod
def initialize(self, total_frames: int):
"""Инициализация и вывод статуса начала работы """
@abstractmethod
def update_progress(self):
"""Обновление данных и вывод статуса о текущем процессе выполнения """
@abstractmethod
def visualize_frame(self, frame: numpy.ndarray,
detections: list, ground_truth: list = None):
"""Визуализация данных"""
@abstractmethod
def check_exit(self):
"""Проверка была ли нажата кнопка для прерывания основного цикла"""
@abstractmethod
def finalize(self):
"""Очищение ресурсов и статус окончания работы"""