diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..eb251e0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max_line_length=300 +in-place = true +ignore=W503, # ignore old opposite of W504 + E266, ##### diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..403fb56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +*.log +.vscode \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29..0000000 diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..1a35c29 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,3 @@ +[settings] +multi_line_output=5 +overwrite_in_place=true \ No newline at end of file diff --git a/.pep8 b/.pep8 new file mode 100644 index 0000000..3dac59f --- /dev/null +++ b/.pep8 @@ -0,0 +1,3 @@ +[pycodestyle] +max_line_length=300 +in-place = true \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e1ff19c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: check-yaml + - id: check-added-large-files + - id: check-ast + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-json + - id: check-shebang-scripts-are-executable + - id: check-merge-conflict + - id: check-xml + - id: mixed-line-ending + - id: requirements-txt-fixer +- repo: https://github.com/PyCQA/flake8 + rev: '4.0.1' + hooks: + - id: flake8 +- repo: https://github.com/pycqa/isort + rev: '5.10.1' + hooks: + - id: isort + name: isort +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: 'v0.931' # Use the sha / tag you want to point at +# hooks: +# - id: mypy diff --git a/README.md b/README.md index f0a03f3..8023932 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ ## About The Project -This project is a result of data protection laws that require identifiable information to be censored in media that is posted to the internet. Dashcam videos in particular tend to be fairly cumbersome to manually edit, so this tool aims to automate the task. +This project is a result of data protection laws that require identifiable information to be censored in media that is posted to the internet. Dashcam videos in particular tend to be fairly cumbersome to manually edit, so this tool aims to automate the task. The goal is to release a simple to use application with simple settings and acceptable performance that does not require any knowledge about image processing, neural networks or even programming as a whole on the end user's part. diff --git a/dashcamcleaner/cli.py b/dashcamcleaner/cli.py old mode 100644 new mode 100755 index b0df41e..370ca1d --- a/dashcamcleaner/cli.py +++ b/dashcamcleaner/cli.py @@ -1,18 +1,17 @@ -import inspect -import os -import sys -from glob import glob +#!/usr/bin/env python3 + +import signal from argparse import ArgumentParser -from tqdm import tqdm from src.blurrer import VideoBlurrer +# makes it possible to interrupt while running in other thread +signal.signal(signal.SIGINT, signal.SIG_DFL) + + class CLI(): def __init__(self, opt): - """ - Constructor - """ self.opt = opt self.blurrer = None @@ -48,19 +47,25 @@ def start_blurring(self): else: print("Blurring resulted in errors.") + def parse_arguments(): - parser = ArgumentParser() - parser.add_argument("input", help="input video file path", type=str) - parser.add_argument("output", help="output video file path", type=str) - parser.add_argument("weights", help="weights file name", type=str) - parser.add_argument("inference_size", help="vertical inference size, e.g. 1080 or fHD", type=int) - parser.add_argument("threshold", help="detection threshold", type=float) - parser.add_argument("blur_size", help="granularitay of the blurring filter", type=int) - parser.add_argument("frame_memory", help="blur objects in the last x frames too", type=int) - parser.add_argument("roi_multi", help="increase/decrease area that will be blurred - 1 means no change", type=float) - parser.add_argument("quality", help="quality of the resulting video", type=int) + parser = ArgumentParser(description=" This tool allows you to automatically censor faces and number plates on dashcam footage.") + + required_named = parser.add_argument_group('required named arguments') + + required_named.add_argument("-i", "--input", metavar="INPUT_PATH", required=True, help="input video file path", type=str) + required_named.add_argument("-o", "--output", metavar="OUTPUT_NAME", required=True, help="output video file path", type=str) + required_named.add_argument("-w", "--weights", metavar="WEIGHTS_FILE_NAME", required=True, help="", type=str) + required_named.add_argument("--threshold", required=True, help="detection threshold", type=float) + required_named.add_argument("--blur_size", required=True, help="granularity of the blurring filter", type=int) + required_named.add_argument("--frame_memory", required=True, help="blur objects in the last x frames too", type=int) + + parser.add_argument("--inference_size", help="vertical inference size, e.g. 1080 or fHD", type=int, default=1080) + parser.add_argument("--roi_multi", required=False, help="increase/decrease area that will be blurred - 1 means no change", type=float, default=1.0) + parser.add_argument("-q", "--quality", metavar="[1, 10]", required=False, help="quality of the resulting video. in range [1, 10] from 1 - bad to 10 - best, default: 10", type=int, choices=range(1, 11), default=10) return parser.parse_args() + if __name__ == "__main__": opt = parse_arguments() cli = CLI(opt) diff --git a/dashcamcleaner/main.py b/dashcamcleaner/main.py old mode 100644 new mode 100755 index f6aaea3..e869a2c --- a/dashcamcleaner/main.py +++ b/dashcamcleaner/main.py @@ -1,15 +1,21 @@ +#!/usr/bin/env python3 import inspect import os +import signal import sys from glob import glob from PySide6.QtCore import QSettings -from PySide6.QtWidgets import QApplication, QMainWindow, QFileDialog -from PySide6.QtWidgets import QSpinBox, QDoubleSpinBox, QLineEdit, QRadioButton, QMessageBox, QComboBox - +from PySide6.QtWidgets import ( + QApplication, QComboBox, QDoubleSpinBox, QFileDialog, QLineEdit, + QMainWindow, QMessageBox, QRadioButton, QSpinBox +) from src.blurrer import VideoBlurrer from src.ui_mainwindow import Ui_MainWindow +# makes it possible to interrupt while running in other thread +signal.signal(signal.SIGINT, signal.SIG_DFL) + class MainWindow(QMainWindow): diff --git a/dashcamcleaner/src/blurrer.py b/dashcamcleaner/src/blurrer.py index 0b35e03..f030bcc 100644 --- a/dashcamcleaner/src/blurrer.py +++ b/dashcamcleaner/src/blurrer.py @@ -1,5 +1,6 @@ import os import subprocess +from shutil import which from timeit import default_timer as timer import cv2 @@ -8,7 +9,6 @@ import torch from PySide6.QtCore import QThread, Signal from src.box import Box -from shutil import which class VideoBlurrer(QThread): @@ -25,7 +25,7 @@ def __init__(self, weights_name, parameters=None): super(VideoBlurrer, self).__init__() self.parameters = parameters self.detections = [] - weights_path = os.path.join("weights", f"{weights_name}.pt") + weights_path = os.path.join("weights", f"{weights_name}.pt".replace(".pt.pt", ".pt")) self.detector = setup_detector(weights_path) self.result = {"success": False, "elapsed_time": 0} print("Worker created") @@ -133,9 +133,9 @@ def run(self): # open video file cap = cv2.VideoCapture(input_path) - # get the height and width of each frame - width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + # get the height and width of each frame for future debug outputs on frame + # width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + # height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) length = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) fps = cap.get(cv2.CAP_PROP_FPS) @@ -201,9 +201,9 @@ def run(self): # delete temporary output that had no audio track try: os.remove(temp_output) - except: + except Exception as e: self.alert.emit( - "Could not delete temporary, muted video. Maybe another process (like a cloud service or antivirus) is using it already." + f"Could not delete temporary, muted video. Maybe another process (like a cloud service or antivirus) is using it already. \n{str(e)}" ) # store success and elapsed time diff --git a/dashcamcleaner/src/box.py b/dashcamcleaner/src/box.py index 9946971..44ce127 100644 --- a/dashcamcleaner/src/box.py +++ b/dashcamcleaner/src/box.py @@ -1,4 +1,4 @@ -from math import sqrt, floor +from math import floor, sqrt class Box: diff --git a/dashcamcleaner/src/generate_training_data.py b/dashcamcleaner/src/generate_training_data.py index 6cc6804..e161799 100644 --- a/dashcamcleaner/src/generate_training_data.py +++ b/dashcamcleaner/src/generate_training_data.py @@ -1,21 +1,21 @@ import os +import random import sys +from argparse import ArgumentParser +from glob import glob +from math import floor, sqrt import cv2 - -# hack to add Anonymizer submodule to PYTHONPATH -sys.path.append(os.path.join(os.path.dirname(__file__), "anonymizer")) +import pandas as pd from anonymizer.anonymization.anonymizer import Anonymizer from anonymizer.detection.detector import Detector from anonymizer.detection.weights import download_weights, get_weights_path -from argparse import ArgumentParser from anonymizer.obfuscation.obfuscator import Obfuscator -from math import sqrt, floor -from glob import glob -from tqdm import tqdm -import pandas as pd from pascal_voc_writer import Writer -import random +from tqdm import tqdm + +# hack to add Anonymizer submodule to PYTHONPATH +sys.path.append(os.path.join(os.path.dirname(__file__), "anonymizer")) def setup_anonymizer(weights_path: str, obfuscation_parameters: str): @@ -95,7 +95,6 @@ def batch_processing(self, input_folder, image_folder, label_folder, train_split for index, vid in enumerate(tqdm(randomized_videos[train_videos:], desc="Processing validation video file")): self.labeled_data_from_video(vid, index, label_format, "val") - def labeled_data_from_video(self, video_path: str, vid_num: int, label_format, folder_suffix, roi_multi=1.2): """ Extract frames and labels from a video @@ -119,7 +118,7 @@ def labeled_data_from_video(self, video_path: str, vid_num: int, label_format, f "plate": 0.2 } - if cap.isOpened() == False: + if cap.isOpened() is False: print('error file not found') return @@ -128,7 +127,7 @@ def labeled_data_from_video(self, video_path: str, vid_num: int, label_format, f while cap.isOpened(): # returns each frame ret, frame = cap.read() - if ret == True: + if ret is True: # skip frames to avoid too similar frames if counter % (self.skip_frames) == 0: _, new_detections = self.anonymizer.anonymize_image(frame, detection_thresholds) diff --git a/dashcamcleaner/src/ui_mainwindow.py b/dashcamcleaner/src/ui_mainwindow.py index da44826..482ae0e 100644 --- a/dashcamcleaner/src/ui_mainwindow.py +++ b/dashcamcleaner/src/ui_mainwindow.py @@ -8,17 +8,13 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QComboBox, QDoubleSpinBox, QFrame, - QHBoxLayout, QLabel, QLineEdit, QMainWindow, - QProgressBar, QPushButton, QSizePolicy, QSpacerItem, - QSpinBox, QVBoxLayout, QWidget) +from PySide6.QtCore import QCoreApplication, QMetaObject +from PySide6.QtWidgets import ( + QComboBox, QDoubleSpinBox, QFrame, QHBoxLayout, QLabel, QLineEdit, + QProgressBar, QPushButton, QSizePolicy, QSpacerItem, QSpinBox, QVBoxLayout, + QWidget +) + class Ui_MainWindow(object): def setupUi(self, MainWindow): @@ -43,7 +39,6 @@ def setupUi(self, MainWindow): self.horizontalLayout.addWidget(self.button_source) - self.verticalLayout.addLayout(self.horizontalLayout) self.horizontalLayout_2 = QHBoxLayout() @@ -60,7 +55,6 @@ def setupUi(self, MainWindow): self.horizontalLayout_2.addWidget(self.button_target) - self.verticalLayout.addLayout(self.horizontalLayout_2) self.line = QFrame(self.centralwidget) @@ -120,7 +114,6 @@ def setupUi(self, MainWindow): self.horizontalLayout_3.addWidget(self.double_spin_roimulti) - self.verticalLayout.addLayout(self.horizontalLayout_3) self.line_2 = QFrame(self.centralwidget) @@ -171,7 +164,6 @@ def setupUi(self, MainWindow): self.horizontalLayout_4.addItem(self.horizontalSpacer) - self.verticalLayout.addLayout(self.horizontalLayout_4) self.line_3 = QFrame(self.centralwidget) @@ -201,7 +193,6 @@ def setupUi(self, MainWindow): self.horizontalLayout_5.addWidget(self.button_abort) - self.verticalLayout.addLayout(self.horizontalLayout_5) MainWindow.setCentralWidget(self.centralwidget) @@ -230,4 +221,3 @@ def retranslateUi(self, MainWindow): self.button_start.setText(QCoreApplication.translate("MainWindow", u"Start", None)) self.button_abort.setText(QCoreApplication.translate("MainWindow", u"Abort", None)) # retranslateUi - diff --git a/requirements.txt b/requirements.txt index 8e2cda0..31c705e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,17 @@ +imageio>=2.9.0 +imageio-ffmpeg>=0.4.5 matplotlib>=3.2.2 numpy>=1.18.5 opencv-python>=4.1.2 -Pillow +pandas>=1.2.3 +Pillow +pyside6>=6.2.2.1 PyYAML>=5.3.1 +requests>=2.25.1 scipy>=1.4.1 +seaborn>=0.11.1 +tensorboard>=2.4.1 torch==1.8.1 torchaudio==0.8.1 -imageio>=2.9.0 -imageio-ffmpeg>=0.4.5 torchvision>=0.8.1 tqdm>=4.41.0 -pyside6>=6.2.2.1 -requests>=2.25.1 -pandas>=1.2.3 -seaborn>=0.11.1 -tensorboard>=2.4.1 diff --git a/requirements_gpu.txt b/requirements_gpu.txt index 17d571c..4ba17bc 100644 --- a/requirements_gpu.txt +++ b/requirements_gpu.txt @@ -1,19 +1,19 @@ + +--find-links https://download.pytorch.org/whl/torch_stable.html +imageio>=2.9.0 +imageio-ffmpeg>=0.4.5 matplotlib>=3.2.2 numpy>=1.18.5 opencv-python>=4.1.2 +pandas>=1.2.3 Pillow +pyside6>=6.2.2.1 PyYAML>=5.3.1 +requests>=2.25.1 scipy>=1.4.1 +seaborn>=0.11.1 +tensorboard>=2.4.1 torch==1.8.1+cu111 torchaudio==0.8.1 torchvision>=0.8.1 -imageio>=2.9.0 -imageio-ffmpeg>=0.4.5 tqdm>=4.41.0 -pyside6>=6.2.2.1 -requests>=2.25.1 -pandas>=1.2.3 -seaborn>=0.11.1 -tensorboard>=2.4.1 - ---find-links https://download.pytorch.org/whl/torch_stable.html