diff --git a/CMakeLists.txt b/CMakeLists.txt index dc272e3..678b9c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ # +-----------------------------------------+ # | License: MIT | # +-----------------------------------------+ -# | Copyright (c) 2023 | +# | Copyright (c) 2024 | # | Author: Gleb Trufanov (aka Glebchansky) | # +-----------------------------------------+ @@ -9,68 +9,54 @@ cmake_minimum_required(VERSION 3.21) project(ISS-Driver-Drowsiness-Detector) +include(cmake/CheckSystem.cmake) + set(MAIN_TARGET DDDetector) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOUIC_SEARCH_PATHS ${CMAKE_SOURCE_DIR}/src/ui) -if (CMAKE_SIZEOF_VOID_P EQUAL 8) - if (WIN32) - message(STATUS "Windows operating system detected") - - if (NOT MSVC) - message(FATAL_ERROR "The project is designed to work with the MSVC compiler when compiling on a Windows operating system") - endif () - - message(STATUS "Disabling the console at program start-up") - add_link_options(/SUBSYSTEM:windows /ENTRY:mainCRTStartup) # To hide the console window at program start-up on Windows - elseif (UNIX AND NOT APPLE) - message(STATUS "Linux kernel operating system detected") - else () - message(FATAL_ERROR "At this point, the project is only designed to run on Windows or Linux operating systems") - endif () -else () - message(FATAL_ERROR "The project is designed to work with an operating system that supports 64-bit extensions") +check_system() + +set(EXECUTABLE_DESTINATION_PATH ${CMAKE_BINARY_DIR}) + +if (UNIX) + set(EXECUTABLE_DESTINATION_PATH ${CMAKE_BINARY_DIR}/bin) endif () -set(RESOURCE_CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/resource) -set(RESOURCE_CMAKE_BINARY_DIR ${CMAKE_BINARY_DIR}/resource) +message(STATUS "Set the output directory for the executable to " ${EXECUTABLE_DESTINATION_PATH}) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${EXECUTABLE_DESTINATION_PATH}) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${EXECUTABLE_DESTINATION_PATH}) -message(STATUS "Set the output directory for the executable to " ${CMAKE_BINARY_DIR}) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}) +set(RESOURCE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/resource) +set(RESOURCE_BINARY_DIR ${CMAKE_BINARY_DIR}/resource) # Project sources -set(TS_FILES ${RESOURCE_CMAKE_SOURCE_DIR}/translation/${PROJECT_NAME}_ru_RU.ts) -set(RC_FILES ${RESOURCE_CMAKE_SOURCE_DIR}/resource.qrc ${RESOURCE_CMAKE_SOURCE_DIR}/executable_icon_resource.rc) +set(TS_FILES ${RESOURCE_SOURCE_DIR}/translation/${PROJECT_NAME}_ru_RU.ts) +set(RC_FILES ${RESOURCE_SOURCE_DIR}/resource.qrc ${RESOURCE_SOURCE_DIR}/executable_icon_resource.rc) set(PROJECT_SOURCES ${RC_FILES} + ${CMAKE_SOURCE_DIR}/src/ui/mainwindow.ui ${CMAKE_SOURCE_DIR}/src/main.cpp - ${CMAKE_SOURCE_DIR}/src/MainWindow.h ${CMAKE_SOURCE_DIR}/src/MainWindow.cpp - ${CMAKE_SOURCE_DIR}/src/Camera.h ${CMAKE_SOURCE_DIR}/src/Camera.cpp - ${CMAKE_SOURCE_DIR}/src/ImageRecognizer.h ${CMAKE_SOURCE_DIR}/src/ImageRecognizer.cpp - ${CMAKE_SOURCE_DIR}/src/utils/Utils.h ${CMAKE_SOURCE_DIR}/src/utils/Utils.cpp - ${CMAKE_SOURCE_DIR}/src/utils/RecognitionFrameCounter.h - ${CMAKE_SOURCE_DIR}/src/utils/TextBrowserLogger.h ${CMAKE_SOURCE_DIR}/src/utils/TextBrowserLogger.cpp - ${CMAKE_SOURCE_DIR}/src/ui/mainwindow.ui ) ##################################################################################### # Find packages -find_package(Qt6 COMPONENTS REQUIRED +find_package(Qt6 6.3 REQUIRED COMPONENTS LinguistTools Widgets Multimedia @@ -79,18 +65,14 @@ find_package(Qt6 COMPONENTS REQUIRED find_package(OpenCV 4.5.4 REQUIRED) # 4.5.4 is the minimum version ##################################################################################### -# Uncomment this to create a QM file based on the TS file. -# A deprecated but working way to update .ts files based on new translations in the source code -# qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) - add_executable(${MAIN_TARGET} ${PROJECT_SOURCES}) # Copying the neural network model and warning sound to the folder with the executable file -# It only works when running cmake command, not during actual build time -file(COPY ${RESOURCE_CMAKE_SOURCE_DIR}/neural_network_model DESTINATION ${RESOURCE_CMAKE_BINARY_DIR}) -file(COPY ${RESOURCE_CMAKE_SOURCE_DIR}/sound DESTINATION ${RESOURCE_CMAKE_BINARY_DIR}) +# It only works during CMake configuration (build generation), not during the actual build +file(COPY ${RESOURCE_SOURCE_DIR}/neural_network_model DESTINATION ${RESOURCE_BINARY_DIR}) +file(COPY ${RESOURCE_SOURCE_DIR}/sound DESTINATION ${RESOURCE_BINARY_DIR}) -set_source_files_properties(${TS_FILES} PROPERTIES OUTPUT_LOCATION ${RESOURCE_CMAKE_BINARY_DIR}/translation) +set_source_files_properties(${TS_FILES} PROPERTIES OUTPUT_LOCATION ${RESOURCE_BINARY_DIR}/translation) qt6_add_translations(${MAIN_TARGET} TS_FILES ${TS_FILES}) target_include_directories(${MAIN_TARGET} PRIVATE @@ -99,6 +81,19 @@ target_include_directories(${MAIN_TARGET} PRIVATE ${OpenCV_INCLUDE_DIRS} ) +if (UNIX) + set(RPATH_LIBRARY_DIRECTORIES Qt6 opencv cuda) + + foreach (RPATH_DIRECTORY ${RPATH_LIBRARY_DIRECTORIES}) + list(APPEND RPATH_LIST "$ORIGIN/../lib64/${RPATH_DIRECTORY}") + endforeach () + + list(JOIN RPATH_LIST ":" RPATH_LIST) + + # Lots of problems with escaping $ORIGIN in CMake (target_link_options especially), but thanks https://stackoverflow.com/a/75790542 + set_target_properties(${MAIN_TARGET} PROPERTIES LINK_FLAGS "-Wl,--enable-new-dtags,-rpath,'${RPATH_LIST}'") +endif () + target_link_libraries(${MAIN_TARGET} PRIVATE Qt6::Widgets Qt6::Multimedia diff --git a/LICENSE.md b/LICENSE.md index d6987c1..e7d4b11 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2023 Gleb Trufanov (aka Glebchansky) +Copyright (c) 2024 Gleb Trufanov (aka Glebchansky) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 49c3ed1..fcbebab 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,14 @@ using facial video surveillance. - 🎮 Interactive interaction with the prototype: - Start/stop the recognition system - Interaction with a video camera - - Eye and gesture recognition ([V gesture ✌](#v-gesture), [Fist gesture ✊](#fist-gesture), [Palm gesture ✋](#palm-gesture)) + - Eye and gesture recognition ([V gesture ✌️](#v-gesture), [Fist gesture ✊](#fist-gesture), [Palm gesture ✋](#palm-gesture)) - Warning of a potential emergency situation using warning sound - 🚀 Recognition (drowsiness, gestures, etc.) takes approximately one second - ⚙️ Multi-language user interface - ⚙️ Cross-platform (Windows/Linux) - ⚙️ Multithreaded application -## Dependencies +## Dependencies (Ninja) TODO - [C++17](https://en.cppreference.com/w/cpp/17): - 64-bit MSVC since version 19.15 on Windows - 64-bit GCC since version 11.2 on Linux diff --git a/build.py b/build.py index 001bd60..5b92969 100644 --- a/build.py +++ b/build.py @@ -1,7 +1,7 @@ # +-----------------------------------------+ # | License: MIT | # +-----------------------------------------+ -# | Copyright (c) 2023 | +# | Copyright (c) 2024 | # | Author: Gleb Trufanov (aka Glebchansky) | # +-----------------------------------------+ @@ -12,27 +12,30 @@ import subprocess argParser = argparse.ArgumentParser(prog="Driver drowsiness detector") +argParser.add_argument("--qt-cmake-prefix-path", required=True, help="Qt CMake prefix path") +argParser.add_argument("--opencv-dir", required=True, help="Directory containing a CMake configuration file for OpenCV") +argParser.add_argument("-j", "--jobs", default=4, help="Number of threads used when building") + +args = argParser.parse_args() -qtCmakePrefixPathDefaultValue = "C:\\Qt\\6.5.2\\msvc2019_64\\lib\\cmake" # Hardcoded default value -opencvDirDefaultValue = "C:\\opencv-4.8.0\\build\\install" # Hardcoded default value cmakeConfigureArgs = ["cmake"] +cmakeBuildArgs = ["cmake", "--build", "."] if platform.system() == "Linux": + needShell = False + # On Windows, the generator is not explicitly specified so that the required Visual Studio version of the # generator is detected automatically - cmakeConfigureArgs.append("-G Ninja") # TODO: Build with Ninja in Linux - - qtCmakePrefixPath = "qt_pass" # Hardcoded default value # TODO: Workability in Linux - opencvDir = "opencv_pass" # Hardcoded default value # TODO: Workability in Linux -elif platform.system() != "Windows": + cmakeConfigureArgs.append("-G=Ninja") + cmakeConfigureArgs.append("-DCMAKE_BUILD_TYPE=Release") +elif platform.system() == "Windows": + needShell = True + + cmakeConfigureArgs.append("-DCMAKE_CONFIGURATION_TYPES=Release") + cmakeBuildArgs.append("--config=Release") +else: raise Exception(f"Unexpected operating system. Got {platform.system()}, but expected Windows or Linux") -argParser.add_argument("--qt-cmake-prefix-path", default=qtCmakePrefixPathDefaultValue, help="Qt CMake prefix path") -argParser.add_argument("--opencv-dir", default=opencvDirDefaultValue, help="Directory containing a CMake " - "configuration file for OpenCV") -argParser.add_argument("-j", "--jobs", default=4, help="Number of threads used when building") -args = argParser.parse_args() - shutil.rmtree("build", ignore_errors=True) os.mkdir("build") os.chdir("build") @@ -41,5 +44,7 @@ cmakeConfigureArgs.append(f"-DOpenCV_DIR={args.opencv_dir}") cmakeConfigureArgs.append("..") -subprocess.run(cmakeConfigureArgs, shell=True, check=True) # CMake configure -subprocess.run(["cmake", "--build", ".", "--config", "Release", "-j", str(args.jobs)], shell=True, check=True) # Build +cmakeBuildArgs.append(f"-j={str(args.jobs)}") + +subprocess.run(cmakeConfigureArgs, shell=needShell, check=True) # CMake configure +subprocess.run(cmakeBuildArgs, shell=needShell, check=True) # Build diff --git a/cmake/CheckSystem.cmake b/cmake/CheckSystem.cmake new file mode 100644 index 0000000..4649e38 --- /dev/null +++ b/cmake/CheckSystem.cmake @@ -0,0 +1,24 @@ +function(check_system) + if (CMAKE_SIZEOF_VOID_P EQUAL 8) + if (WIN32) + message(STATUS "Windows operating system detected") + + if (NOT MSVC) + message(FATAL_ERROR "The project is designed to work with the MSVC compiler when compiling on Windows operating system") + endif () + + message(STATUS "Disabling the console at program start-up") + add_link_options(/SUBSYSTEM:windows /ENTRY:mainCRTStartup) # To hide the console window at program start-up on Windows + elseif (UNIX AND NOT APPLE) + message(STATUS "Linux kernel operating system detected") + + if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + message(FATAL_ERROR "The project is designed to work with the GNU compiler (GCC) when compiling on Linux operating system") + endif () + else () + message(FATAL_ERROR "At this point, the project is only designed to run on Windows or Linux operating systems") + endif () + else () + message(FATAL_ERROR "The project is designed to work with an operating system that supports 64-bit extensions") + endif () +endfunction() \ No newline at end of file diff --git a/resource/resource.qrc b/resource/resource.qrc index 46ab028..f2ae27f 100644 --- a/resource/resource.qrc +++ b/resource/resource.qrc @@ -2,6 +2,6 @@ images/Flag_of_Russia.svg images/Flag_of_the_United_Kingdom.svg - images/Main_Window_Icon.png + images/Main_Window_Icon.png diff --git a/resource/translation/ISS-Driver-Drowsiness-Detector_ru_RU.ts b/resource/translation/ISS-Driver-Drowsiness-Detector_ru_RU.ts index 9f7da26..017ad88 100644 --- a/resource/translation/ISS-Driver-Drowsiness-Detector_ru_RU.ts +++ b/resource/translation/ISS-Driver-Drowsiness-Detector_ru_RU.ts @@ -25,8 +25,7 @@ - - + Run Запуск @@ -36,22 +35,22 @@ Доступные камеры - + System information Системная информация - + GPU: ГП: - + CPU: ЦП: - + Operating System: Операционная система: @@ -71,29 +70,28 @@ Очистить логгер - + Logs will appear here... Логи появятся здесь... - + Interface language Язык интерфейса - + Русский - + English - Error when initialising the recogniser - Ошибка при инициализации распознавателя + Ошибка при инициализации распознавателя Error when initialising the warning sound @@ -104,83 +102,88 @@ Не найден файл предупредительного звукового сигнала по адресу " - + The camera " Камера " - + " is selected " выбрана - + No available cameras Нет доступных камер - - + + Stop Стоп - + There is a problem with the camera Возникла проблема с камерой - + Camera failure Сбой камеры - + The camera is running Камера запущена - + The camera is stopped Камера остановлена - + The Fist gesture is recognized Распознан жест "Кулак" - + Restart the recognition system Перезапуск системы распознавания - + + Error when initializing the recogniser + + + + The Palm gesture is recognized Распознан жест "Ладонь" - + Waking up the drowsiness recognition system Ввод системы распознавания сонливости в режим оповещения - + The V gesture is recognized Распознан "V-жест" - + Putting the drowsiness recognition system into sleep mode Ввод системы распознавания сонливости в спящий режим - + Drowsiness is recognized Распознана сонливость - + Drowsiness alert with a warning sound Оповещение о сонливости при помощи предупредительного звукового сигнала @@ -196,27 +199,27 @@ Стоп - + Attentive Eye Внимательный глаз - + Drowsy Eye Соннный глаз - + Fist Gesture Жест "Кулак" - + Palm Gesture Жест "Ладонь" - + V Gesture "V-жест" diff --git a/src/Camera.cpp b/src/Camera.cpp index 4606f3d..c503a13 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ @@ -29,12 +29,25 @@ bool Camera::IsAvailable() const { } void Camera::SetCameraDevice(int cameraDeviceIndex) { - // Very fine-tuning, perhaps not all systems will be suitable for this combination (with MJPEG) to get - // high FPS when capturing frames from the camera +#ifdef _WIN64 // The Camera class is Qt independent, so no Q_OS_* type macros are used here _isAvailable = _videoCapture.open(cameraDeviceIndex, cv::CAP_DSHOW); - // If possible, this resolution will be used - _videoCapture.set(cv::CAP_PROP_FRAME_WIDTH, _preferredResolution.width); - _videoCapture.set(cv::CAP_PROP_FRAME_HEIGHT, _preferredResolution.height); - _videoCapture.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M', 'J', 'P', 'G')); + if (!_isAvailable) + _isAvailable = _videoCapture.open(cameraDeviceIndex, cv::CAP_MSMF); +#else + _isAvailable = _videoCapture.open(cameraDeviceIndex, cv::CAP_V4L2); +#endif + + if (_isAvailable) { + // If possible, this resolution will be used + _videoCapture.set(cv::CAP_PROP_FRAME_WIDTH, _preferredResolution.width); + _videoCapture.set(cv::CAP_PROP_FRAME_HEIGHT, _preferredResolution.height); + + auto const cameraBackendName = _videoCapture.getBackendName(); + + // Very fine-tuning, perhaps not all systems will be suitable for this combination (DSHOW/V4L2 with MJPEG) to get + // high FPS when capturing frames from the camera + if (cameraBackendName == "DSHOW" || cameraBackendName == "V4L2") + _videoCapture.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M', 'J', 'P', 'G')); + } } diff --git a/src/Camera.h b/src/Camera.h index ac0ec17..780122c 100644 --- a/src/Camera.h +++ b/src/Camera.h @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ diff --git a/src/ImageRecognizer.cpp b/src/ImageRecognizer.cpp index 1e365eb..c0775fb 100644 --- a/src/ImageRecognizer.cpp +++ b/src/ImageRecognizer.cpp @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ diff --git a/src/ImageRecognizer.h b/src/ImageRecognizer.h index 8d0e4fe..05bb665 100644 --- a/src/ImageRecognizer.h +++ b/src/ImageRecognizer.h @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 51b13e6..4c5d80e 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -22,9 +23,7 @@ using namespace std; using namespace cv; -namespace { - -constexpr int CAMERA_CONTENT_HINT_LABEL_PAGE = 0; +constexpr int CAMERA_CONTENT_HINT_LABEL_PAGE = 0; constexpr int VIDEO_FRAME_PAGE = 1; constexpr int RESTART_RECOGNITION_SYSTEM_PAGE = 2; @@ -37,6 +36,16 @@ constexpr QColor CRAIOLA_PERIWINKLE = {197, 208, 230}; constexpr QColor DEEP_CARMINE_PINK = {239, 48, 56}; constexpr QColor WISTERIA = {201, 160, 20}; +#ifdef Q_OS_WIN64 +constexpr QLatin1StringView NEURAL_NETWORK_MODEL_PATH{"/resource/neural_network_model/neural_network_model.onnx"}; +constexpr QLatin1StringView WARNING_SOUND_PATH{"/resource/sound/warning_sound.wav"}; +constexpr QLatin1StringView TRANSLATION_PATH{"/resource/translation/ISS-Driver-Drowsiness-Detector_ru_RU.qm"}; +#else +constexpr QLatin1StringView NEURAL_NETWORK_MODEL_PATH{"/../resource/neural_network_model/neural_network_model.onnx"}; +constexpr QLatin1StringView WARNING_SOUND_PATH{"/../resource/sound/warning_sound.wav"}; +constexpr QLatin1StringView TRANSLATION_PATH{"/../resource/translation/ISS-Driver-Drowsiness-Detector_ru_RU.qm"}; +#endif + string ToClassName(RecognitionType recognitionType) { switch (recognitionType) { case AttentiveEye: return QObject::tr("Attentive Eye").toStdString(); @@ -62,7 +71,7 @@ Scalar ToRecognitionColor(RecognitionType recognitionType) { void DrawLabel(Mat& image, const string& text, int left, int top, const Scalar& lineRectangleColor) { int baseLine; - Size labelSize = getTextSize(text, FONT_HERSHEY_COMPLEX, 0.5, 1, &baseLine); + const auto labelSize = getTextSize(text, FONT_HERSHEY_COMPLEX, 0.5, 1, &baseLine); top = max(top, labelSize.height); Point cornerLineTopRight(left + 15, top - 20); @@ -80,29 +89,9 @@ void DrawLabel(Mat& image, const string& text, int left, int top, const Scalar& putText(image, text, cornerLineTopRight, FONT_HERSHEY_COMPLEX, 0.5, Scalar(255, 255, 255), 1, LINE_AA); // BGR format } -} // namespace - -MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow), - _imageRecognizer(QDir::currentPath().append("/resource/neural_network_model/neural_network_model.onnx").toStdString()) +MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), _ui(new Ui::MainWindow), + _imageRecognizer(QCoreApplication::applicationDirPath().append(NEURAL_NETWORK_MODEL_PATH).toStdString()) { - auto warningSoundPath = QDir::currentPath().append("/resource/sound/warning_sound.wav"); - - if (!QFileInfo::exists(warningSoundPath)) { - QMessageBox::critical(nullptr, "Error when initialising the warning sound", - QString("No warning sound file found at \"").append(warningSoundPath).append("\"")); - _hasErrors = true; - return; - } - - if (!_imageRecognizer.Error().empty()) { - QMessageBox::critical(nullptr, tr("Error when initialising the recogniser"), tr(_imageRecognizer.Error().c_str())); - _hasErrors = true; - return; - } - - _warningSound.setSource(QUrl::fromLocalFile(warningSoundPath)); - - ui->setupUi(this); Init(); } @@ -112,19 +101,19 @@ bool MainWindow::Errors() const { void MainWindow::changeEvent(QEvent* event) { if (event->type() == QEvent::LanguageChange) { - ui->retranslateUi(this); + _ui->retranslateUi(this); - // Explicitly setting text is needed because calling "ui->retranslateUi(this)" resets the text to the one + // Explicitly setting text is needed because calling "_ui->retranslateUi(this)" resets the text to the one // originally set for the button in the GUI (Qt Designer). This is because retranslate only calls // QCoreApplication::translate for the GUI caption (in this case the "Run" caption) - if (_isCameraRun) ui->runStop->setText(tr("Stop")); + if (_isCameraRun) _ui->runStop->setText(tr("Stop")); } - QWidget::changeEvent(event); + QMainWindow::changeEvent(event); } void MainWindow::OnAvailableCamerasActivated(int index) { - auto selectedCameraName = ui->availableCameras->itemText(index); + auto selectedCameraName = _ui->availableCameras->itemText(index); if (selectedCameraName == _currentCameraName) // To avoid re-setting the same camera device return; @@ -139,14 +128,14 @@ void MainWindow::OnAvailableCamerasActivated(int index) { isNeedToResumeVideoStream = true; } - ui->videoFrame->clear(); + _ui->videoFrame->clear(); _currentCameraName = std::move(selectedCameraName); _camera.SetCameraDevice(index); const auto newCameraResolution = _camera.GetResolution(); - ui->cameraContentStackedWidget->setFixedSize(newCameraResolution.width, newCameraResolution.height); + _ui->cameraContentStackedWidget->setFixedSize(newCameraResolution.width, newCameraResolution.height); - TextBrowserLogger::Log(ui->logger, tr("The camera \"").append(_currentCameraName).append(tr("\" is selected")), CRAIOLA_PERIWINKLE); + TextBrowserLogger::Log(_ui->logger, tr("The camera \"").append(_currentCameraName).append(tr("\" is selected")), CRAIOLA_PERIWINKLE); if (isNeedToResumeVideoStream) ResumeVideoStreamThread(); @@ -159,26 +148,26 @@ void MainWindow::OnRunStopClicked() { } _isCameraRun = !_isCameraRun; - ui->runStop->setText(_isCameraRun ? tr("Stop") : tr("Run")); + _ui->runStop->setText(_isCameraRun ? tr("Stop") : tr("Run")); _isCameraRun ? StartCamera() : StopCamera(); } void MainWindow::OnClearLoggerClicked() { - TextBrowserLogger::Clear(ui->logger); + TextBrowserLogger::Clear(_ui->logger); } void MainWindow::OnVideoInputsChanged() { - const QList cameras = _mediaDevices.videoInputs(); + const QList cameras = QMediaDevices::videoInputs(); QList existingCameras; // There may be a situation where there are cameras with the same names (descriptions) - ui->availableCameras->clear(); + _ui->availableCameras->clear(); for (const auto& camera : cameras) { const auto camerasCount = existingCameras.count(camera.description()); const QString cameraName = camera.description().append(camerasCount > 0 ? QString::number(camerasCount) : ""); - ui->availableCameras->addItem(cameraName); + _ui->availableCameras->addItem(cameraName); existingCameras.emplace_back(camera.description()); } @@ -188,7 +177,7 @@ void MainWindow::OnVideoInputsChanged() { const auto errorMessage = tr("There is a problem with the camera ").append(_currentCameraName); OnRunStopClicked(); - TextBrowserLogger::Log(ui->logger, errorMessage, DEEP_CARMINE_PINK); + TextBrowserLogger::Log(_ui->logger, errorMessage, DEEP_CARMINE_PINK); QMessageBox::warning(nullptr, tr("Camera failure"), errorMessage); } @@ -198,58 +187,86 @@ void MainWindow::OnVideoInputsChanged() { else if (!_camera.IsAvailable() || !isCurrentCameraDeviceAvailable) { _camera.SetCameraDevice(0); // If there were no cameras available before or the current camera device has been deactivated const auto cameraResolution = _camera.GetResolution(); - ui->cameraContentStackedWidget->setFixedSize(cameraResolution.width, cameraResolution.height); + _ui->cameraContentStackedWidget->setFixedSize(cameraResolution.width, cameraResolution.height); _currentCameraName = existingCameras[0]; - ui->availableCameras->setCurrentIndex(0); + _ui->availableCameras->setCurrentIndex(0); } else { - ui->availableCameras->setCurrentText(_currentCameraName); + _ui->availableCameras->setCurrentText(_currentCameraName); } } void MainWindow::OnRecognitionSystemRestarted() { - ui->cameraContentStackedWidget->setCurrentIndex(RESTART_RECOGNITION_SYSTEM_PAGE); - ui->cameraContentStackedWidget->repaint(); + TextBrowserLogger::Log(_ui->logger, tr("The Fist gesture is recognized"), WISTERIA); + TextBrowserLogger::Log(_ui->logger, tr("Restart the recognition system"), WISTERIA); + + _ui->cameraContentStackedWidget->setCurrentIndex(RESTART_RECOGNITION_SYSTEM_PAGE); + _ui->cameraContentStackedWidget->repaint(); - _frameCounterWithRecognition.Reset(FistGesture); DestroyVideoStreamThread(); _imageRecognizer.Restart(); - QThread::msleep(RESTART_SLEEP_TIME_MS); // A pause for a sense of duration in restarting the recognition system + QThread::msleep(RESTART_SLEEP_TIME_MS); // Pause to simulate the restart duration of the recognition system StartCamera(); } void MainWindow::Init() { + if (!_imageRecognizer.Error().empty()) { + QMessageBox::critical(nullptr, tr("Error when initializing the recogniser"), tr(_imageRecognizer.Error().c_str())); + _hasErrors = true; + return; + } + + const auto warningSoundPath = QCoreApplication::applicationDirPath().append(WARNING_SOUND_PATH); + + if (!QFileInfo::exists(warningSoundPath)) { + QMessageBox::critical(nullptr, "Error when initializing the warning sound", + QString("No warning sound file found at \"").append(warningSoundPath).append("\"")); + _hasErrors = true; + return; + } + + _warningSound.setAudioDevice(QMediaDevices::defaultAudioOutput()); + _warningSound.setSource(QUrl::fromLocalFile(warningSoundPath)); + + _ui->setupUi(this); + SetSystemInformation(); - ui->russianLanguage->setIcon(QIcon(":/icons/images/Flag_of_Russia.svg")); - ui->englishLanguage->setIcon(QIcon(":/icons/images/Flag_of_the_United_Kingdom.svg")); + _ui->russianLanguage->setIcon(QIcon(":/icons/images/Flag_of_Russia.svg")); + _ui->englishLanguage->setIcon(QIcon(":/icons/images/Flag_of_the_United_Kingdom.svg")); OnVideoInputsChanged(); // For fill combobox on init InitConnects(); - if (ui->availableCameras->count()) - _currentCameraName = ui->availableCameras->itemText(0); - - showMaximized(); + if (_ui->availableCameras->count()) + _currentCameraName = _ui->availableCameras->itemText(0); - const auto translationFilePath = QDir::currentPath().append("/resource/translation/ISS-Driver-Drowsiness-Detector_ru_RU.qm"); + const auto translationFilePath = QCoreApplication::applicationDirPath().append(TRANSLATION_PATH); const auto translationFilePathErrorMessage = QString("Error when loading the translation file \"%1\". " "The Russian translation will not be available during the work of the application.").arg(translationFilePath); if (!_uiRussianTranslator.load(translationFilePath)) { - ui->russianLanguage->setDisabled(true); + _ui->russianLanguage->setDisabled(true); QMessageBox::warning(nullptr, "Error when loading the translation file", translationFilePathErrorMessage); } + + // A workaround for the next very strange problem: + // Because the cameraContentStackedWidget is set to a fixed size in OnVideoInputsChanged, which is called above, + // this breaks QMainWindow's showMaximized and prevents the window from expanding fully (even the maximize window + // size button is removed). This is somehow fixed when the whole application is translated for the first time. + QApplication::installTranslator(&_uiRussianTranslator); + _ui->retranslateUi(this); + QApplication::removeTranslator(&_uiRussianTranslator); } void MainWindow::InitConnects() { // Connect for update available cameras if the list of cameras in the system has changed connect(&_mediaDevices, &QMediaDevices::videoInputsChanged, this, &MainWindow::OnVideoInputsChanged); - for (const auto* retranslateUiButton : {ui->russianLanguage, ui->englishLanguage}) { + for (const auto* retranslateUiButton : {_ui->russianLanguage, _ui->englishLanguage}) { connect(retranslateUiButton, &QAction::triggered, this, [&, retranslateUiButton]() { - if (retranslateUiButton == ui->russianLanguage) { + if (retranslateUiButton == _ui->russianLanguage) { QApplication::installTranslator(&_uiRussianTranslator); } else { @@ -258,40 +275,66 @@ void MainWindow::InitConnects() { }); } - connect(ui->availableCameras, &QComboBox::activated, this, &MainWindow::OnAvailableCamerasActivated); - connect(ui->runStop, &QPushButton::clicked, this, &MainWindow::OnRunStopClicked); - connect(ui->clearLogger, &QPushButton::clicked, this, &MainWindow::OnClearLoggerClicked); + connect(_ui->availableCameras, &QComboBox::activated, this, &MainWindow::OnAvailableCamerasActivated); + connect(_ui->runStop, &QPushButton::clicked, this, &MainWindow::OnRunStopClicked); + connect(_ui->clearLogger, &QPushButton::clicked, this, &MainWindow::OnClearLoggerClicked); + + // The connections below are used for safe interaction of the secondary thread with GUI elements via the main thread + connect(this, &MainWindow::RecognitionSystemRestarted, this, &MainWindow::OnRecognitionSystemRestarted); connect(this, &MainWindow::PlayWarningSignal, this, [this]() { _warningSound.play(); }); + + connect(this, &MainWindow::LogAsync, this, [this](const QString& message, const QColor& color) { + TextBrowserLogger::Log(_ui->logger, message, color); + }); + + connect(this, &MainWindow::UpdateVideoFrame, this, [this](const QPixmap& videoFrame) { + _ui->videoFrame->setPixmap(videoFrame); + }); } void MainWindow::SetSystemInformation() { QProcess systemProcess; - QString cpuName, gpuName; + QString cpuName = "Cannot be detected"; + QString gpuName = "Cannot be detected"; // If there are multiple active GPUs (SLI), they will concatenate into a single text (at least on Linux :) ) #ifdef Q_OS_WIN64 systemProcess.startCommand("wmic cpu get name"); - systemProcess.waitForFinished(); - cpuName = systemProcess.readAllStandardOutput(); - cpuName.remove(0, cpuName.indexOf('\n') + 1); - cpuName = cpuName.trimmed(); + + if (systemProcess.waitForFinished()) { + cpuName = systemProcess.readAllStandardOutput(); + cpuName = cpuName.remove(0, cpuName.indexOf('\n') + 1).trimmed(); + } systemProcess.startCommand("wmic PATH Win32_videocontroller get VideoProcessor"); - systemProcess.waitForFinished(); - gpuName = systemProcess.readAllStandardOutput(); - gpuName.remove(0, gpuName.indexOf('\n') + 1); - gpuName = gpuName.trimmed(); -#endif -#ifdef Q_OS_LINUX - systemProcess.start("\"cat /proc/cpuinfo | grep 'model name' | uniq\""); - systemProcess.waitForFinished(); - cpuName = systemProcess.readAllStandardOutput(); - // TODO System Information in Linux + + if (systemProcess.waitForFinished()) { + gpuName = systemProcess.readAllStandardOutput(); + gpuName = gpuName.remove(0, gpuName.indexOf('\n') + 1).trimmed(); + } +#else + static constexpr QUtf8StringView getCpuCommand{R"(lscpu | grep "Имя модели\|Model name" | awk -F ':' '{print $2}')"}; + + static constexpr QLatin1StringView getGpusCommand{ + R"( + lspci -vnnn | + perl -lne 'print if /^\d+\:.+(\[\S+\:\S+\])/' | + grep VGA | + awk -F ':' '{print $3,":",$4}' | + cut -d '(' -f 1 | + awk '{gsub("[[:blank:]]+:[[:blank:]]+", ":"); print}' + )"}; + + systemProcess.start("sh", QStringList() << "-c" << getCpuCommand.toString()); + if (systemProcess.waitForFinished()) cpuName = systemProcess.readAllStandardOutput().trimmed(); + + systemProcess.start("sh", QStringList() << "-c" << getGpusCommand); + if (systemProcess.waitForFinished()) gpuName = systemProcess.readAllStandardOutput().trimmed(); #endif - ui->operatingSystem->setText(QSysInfo::prettyProductName()); - ui->cpuName->setText(cpuName); - ui->gpuName->setText(gpuName); + _ui->operatingSystem->setText(QSysInfo::prettyProductName()); + _ui->cpuName->setText(cpuName); + _ui->gpuName->setText(gpuName); } void MainWindow::StartCamera() { @@ -304,20 +347,20 @@ void MainWindow::StartCamera() { ResumeVideoStreamThread(); } - TextBrowserLogger::Log(ui->logger, tr("The camera is running"), PALE_YELLOW); - ui->cameraContentStackedWidget->setCurrentIndex(VIDEO_FRAME_PAGE); + TextBrowserLogger::Log(_ui->logger, tr("The camera is running"), PALE_YELLOW); + _ui->cameraContentStackedWidget->setCurrentIndex(VIDEO_FRAME_PAGE); } void MainWindow::StopCamera() { - ui->videoFrame->clear(); - ui->cameraContentStackedWidget->setCurrentIndex(CAMERA_CONTENT_HINT_LABEL_PAGE); + _ui->videoFrame->clear(); + _ui->cameraContentStackedWidget->setCurrentIndex(CAMERA_CONTENT_HINT_LABEL_PAGE); SuspendVideoStreamThread(50); - TextBrowserLogger::Log(ui->logger, tr("The camera is stopped"), PALE_YELLOW); + TextBrowserLogger::Log(_ui->logger, tr("The camera is stopped"), PALE_YELLOW); } void MainWindow::VideoStream() { while (_isVideoStreamThreadRun) { - QMutexLocker locker(&_mutex); + QMutexLocker locker(&_mutex); while (!_isCameraRun) _videoStreamStopper.wait(&_mutex); @@ -332,7 +375,7 @@ void MainWindow::VideoStream() { const auto recognitions = _imageRecognizer.Recognize(flippedVideoFrame); HandleRecognitions(recognitions, flippedVideoFrame); - ui->videoFrame->setPixmap(QPixmap::fromImage(ToQImage(flippedVideoFrame))); + emit UpdateVideoFrame(QPixmap::fromImage(ToQImage(flippedVideoFrame))); } // When repeatedly stopping and starting the camera using the GUI without this explicit unlock call, the thread may stall @@ -341,7 +384,7 @@ void MainWindow::VideoStream() { } } -void MainWindow::HandleRecognitions(const std::vector& recognitions, cv::Mat& videoFrame) { +void MainWindow::HandleRecognitions(const vector& recognitions, cv::Mat& videoFrame) { static char confidenceBuffer[10]; int16_t drowsyEyeCount = 0; @@ -354,8 +397,9 @@ void MainWindow::HandleRecognitions(const std::vector& recognit }; for (const auto& recognitionInfo : recognitions) { - if (ui->showDetections->isChecked()) { + if (_ui->showDetections->isChecked()) { const auto recognitionColor = ToRecognitionColor(recognitionInfo.recognitionType); + snprintf(confidenceBuffer, sizeof(confidenceBuffer), " %.2f %%", recognitionInfo.confidence * 100); rectangle(videoFrame, recognitionInfo.boundingBox, recognitionColor, 2); // Bounding Box @@ -364,7 +408,7 @@ void MainWindow::HandleRecognitions(const std::vector& recognit } // There is no specific handling for the attentive eye - if (!ui->debugMode->isChecked() && recognitionInfo.recognitionType != AttentiveEye) { + if (!_ui->debugMode->isChecked() && recognitionInfo.recognitionType != AttentiveEye) { if (recognitionInfo.recognitionType == DrowsyEye) { // The frame counter with drowsy eye recognition is increased only if 2 drowsy eyes are recognized if (!_isSleepMode && ++drowsyEyeCount == 2) { @@ -401,31 +445,31 @@ void MainWindow::HandleRecognitions(const std::vector& recognit } void MainWindow::RestartRecognitionSystem() { - TextBrowserLogger::Log(ui->logger, tr("The Fist gesture is recognized"), WISTERIA); - TextBrowserLogger::Log(ui->logger, tr("Restart the recognition system"), WISTERIA); - + // Restart is performed in the main thread, so resetting the counter is necessary in this thread + // so that the next iteration of the VideoStream loop does not have a second restart at once + _frameCounterWithRecognition.Reset(FistGesture); emit RecognitionSystemRestarted(); } void MainWindow::WakeUpDrowsinessRecognitionSystem() { - TextBrowserLogger::Log(ui->logger, tr("The Palm gesture is recognized"), WISTERIA); - TextBrowserLogger::Log(ui->logger, tr("Waking up the drowsiness recognition system"), WISTERIA); + emit LogAsync(tr("The Palm gesture is recognized"), WISTERIA); + emit LogAsync(tr("Waking up the drowsiness recognition system"), WISTERIA); _isSleepMode = false; _frameCounterWithRecognition.Reset(PalmGesture); } void MainWindow::PutDrowsinessRecognitionSystemIntoSleepMode() { - TextBrowserLogger::Log(ui->logger, tr("The V gesture is recognized"), WISTERIA); - TextBrowserLogger::Log(ui->logger, tr("Putting the drowsiness recognition system into sleep mode"), WISTERIA); + emit LogAsync(tr("The V gesture is recognized"), WISTERIA); + emit LogAsync(tr("Putting the drowsiness recognition system into sleep mode"), WISTERIA); _isSleepMode = true; _frameCounterWithRecognition.Reset(VGesture); } void MainWindow::DrowsinessAlert() { - TextBrowserLogger::Log(ui->logger, tr("Drowsiness is recognized"), WISTERIA); - TextBrowserLogger::Log(ui->logger, tr("Drowsiness alert with a warning sound"), WISTERIA); + emit LogAsync(tr("Drowsiness is recognized"), WISTERIA); + emit LogAsync(tr("Drowsiness alert with a warning sound"), WISTERIA); emit PlayWarningSignal(); @@ -463,5 +507,5 @@ void MainWindow::DestroyVideoStreamThread() { MainWindow::~MainWindow() { DestroyVideoStreamThread(); - delete ui; + delete _ui; } diff --git a/src/MainWindow.h b/src/MainWindow.h index 011c87b..5471b63 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ @@ -32,8 +32,10 @@ class MainWindow : public QMainWindow { bool Errors() const; signals: + void LogAsync(const QString& message, const QColor& color); // Signal for the main thread to insert text into QTextBrowser void RecognitionSystemRestarted(); void PlayWarningSignal(); + void UpdateVideoFrame(const QPixmap& videoFrame); protected: void changeEvent(QEvent* event) override; @@ -46,7 +48,7 @@ private slots: void OnRecognitionSystemRestarted(); private: - Ui::MainWindow* ui; + Ui::MainWindow* _ui; bool _hasErrors = false; QTranslator _uiRussianTranslator; @@ -83,6 +85,8 @@ private slots: void StartCamera(); void StopCamera(); + + ///// Executed not in the main thread void VideoStream(); void HandleRecognitions(const std::vector& recognitions, cv::Mat& videoFrame); @@ -90,6 +94,7 @@ private slots: void WakeUpDrowsinessRecognitionSystem(); void PutDrowsinessRecognitionSystemIntoSleepMode(); void DrowsinessAlert(); + ////////// void ResumeVideoStreamThread(); void SuspendVideoStreamThread(uint16_t msDelay); diff --git a/src/main.cpp b/src/main.cpp index 094e79a..de7c59a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ @@ -9,9 +9,11 @@ #include int main(int argc, char** argv) { +#ifdef Q_OS_WIN64 // https://github.com/opencv/opencv/issues/17687#issuecomment-872291073 - // _putenv_s("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0"); // So there is no delay when opening a camera with MSMF backend - + _putenv_s("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0"); // So there is no delay when opening a camera with MSMF backend +#endif + // TODO: Readme QApplication application(argc, argv); QTranslator translator; MainWindow mainWindow; @@ -19,6 +21,6 @@ int main(int argc, char** argv) { if (mainWindow.Errors()) return 1; - mainWindow.show(); + mainWindow.showMaximized(); return QApplication::exec(); } diff --git a/src/ui/mainwindow.ui b/src/ui/mainwindow.ui index c0f9bb7..b9b58d9 100644 --- a/src/ui/mainwindow.ui +++ b/src/ui/mainwindow.ui @@ -71,7 +71,7 @@ QScrollBar::handle:vertical:pressed { background-color: #8b8787; } -/* BTN TOP - SCROLLBAR */ +/* BUTTON TOP - SCROLLBAR */ QScrollBar::sub-line:vertical { border: none; background-color: #4e4e4e; @@ -88,7 +88,7 @@ QScrollBar::sub-line:vertical:pressed { background-color: #9797a5; } -/* BTN BOTTOM - SCROLLBAR */ +/* BUTTON BOTTOM - SCROLLBAR */ QScrollBar::add-line:vertical { border: none; background-color: #4e4e4e; @@ -172,7 +172,7 @@ QLabel { 0 - + 0 @@ -556,8 +556,8 @@ p, li { white-space: pre-wrap; } hr { height: 1px; border-width: 0; } li.unchecked::marker { content: "\2610"; } li.checked::marker { content: "\2612"; } -</style></head><body style=" font-family:'Segoe UI'; font-size:16pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> +</style></head><body style=" font-family:'Ubuntu'; font-size:16pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Segoe UI';"><br /></p></body></html> Logs will appear here... @@ -763,8 +763,8 @@ li.checked::marker { content: "\2612"; } 0 0 - 805 - 31 + 4095 + 27 diff --git a/src/utils/RecognitionFrameCounter.h b/src/utils/RecognitionFrameCounter.h index b6a7eb2..c77ea44 100644 --- a/src/utils/RecognitionFrameCounter.h +++ b/src/utils/RecognitionFrameCounter.h @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ diff --git a/src/utils/TextBrowserLogger.cpp b/src/utils/TextBrowserLogger.cpp index 5a6d58e..e2bc917 100644 --- a/src/utils/TextBrowserLogger.cpp +++ b/src/utils/TextBrowserLogger.cpp @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ diff --git a/src/utils/TextBrowserLogger.h b/src/utils/TextBrowserLogger.h index b6f6633..0670b03 100644 --- a/src/utils/TextBrowserLogger.h +++ b/src/utils/TextBrowserLogger.h @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ diff --git a/src/utils/Utils.cpp b/src/utils/Utils.cpp index 4b58c62..a5375a5 100644 --- a/src/utils/Utils.cpp +++ b/src/utils/Utils.cpp @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+ diff --git a/src/utils/Utils.h b/src/utils/Utils.h index 80b5df7..ef7e187 100644 --- a/src/utils/Utils.h +++ b/src/utils/Utils.h @@ -1,7 +1,7 @@ // +-----------------------------------------+ // | License: MIT | // +-----------------------------------------+ -// | Copyright (c) 2023 | +// | Copyright (c) 2024 | // | Author: Gleb Trufanov (aka Glebchansky) | // +-----------------------------------------+