diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..21ddc37 --- /dev/null +++ b/.clangd @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=https://json.schemastore.org/clangd.json + +CompileFlags: + CompilationDatabase: "build/" + + Add: + - "-std=c++17" + - "-IC:/libraries/opencv/build/include" + - "-IC:/libraries/nlohmann" diff --git a/.github/workflows/cpp-build.yml b/.github/workflows/cpp-build.yml deleted file mode 100644 index cf0d05d..0000000 --- a/.github/workflows/cpp-build.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: C++ Build - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: windows-latest - - steps: - - uses: actions/checkout@v3 - - - name: Install OpenCV - run: | - choco install opencv - - - name: Configure with CMake - run: cmake -B build - - - name: Build - run: cmake --build build diff --git a/.github/workflows/ubuntu-build.yml b/.github/workflows/ubuntu-build.yml new file mode 100644 index 0000000..125abd4 --- /dev/null +++ b/.github/workflows/ubuntu-build.yml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Ubuntu C++ Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install OpenCV + run: | + sudo apt-get update + sudo apt-get install -y --fix-missing libopencv-dev ninja-build + + - name: Configure with CMake + run: cmake -B build -G Ninja + + - name: Build + run: cmake --build build diff --git a/.github/workflows/window-build.yml b/.github/workflows/window-build.yml new file mode 100644 index 0000000..181003d --- /dev/null +++ b/.github/workflows/window-build.yml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Window C++ build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup MSVC + uses: ilammy/msvc-dev-cmd@v1 + + - name: Install Ninja + run: choco install ninja + + - name: Install OpenCV + run: choco install opencv -y + + - name: Find OpenCV path + run: dir C:\tools\opencv + + - name: Configure with CMake (Ninja + MSVC) + run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DOpenCV_DIR="C:/tools/opencv/build" + + - name: Build + run: cmake --build build diff --git a/.gitignore b/.gitignore index c0d6fd4..b2b4a75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode +.kateproject __pycache__/* input-* !input/example*.jpg diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..449c0f6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.10) +project(CropSense-Face-Detection) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# ---- OpenCV setup ---- +if(WIN32) + if(NOT DEFINED OpenCV_DIR) + set(OpenCV_DIR "C:/libraries/opencv/build") + message(STATUS "Using default OpenCV_DIR: ${OpenCV_DIR}") + endif() + if(NOT EXISTS "${OpenCV_DIR}/OpenCVConfig.cmake") + message(FATAL_ERROR "OpenCVConfig.cmake not found in ${OpenCV_DIR}") + endif() + find_package(OpenCV QUIET PATHS "${OpenCV_DIR}") +else() + find_package(OpenCV REQUIRED) +endif() + +if(NOT OpenCV_FOUND) + message(FATAL_ERROR + "OpenCV not found!\n" + "Check:\n" + "- OpenCV_DIR=${OpenCV_DIR}\n" + "- Make sure x64/vc16/lib exists\n" + ) +endif() + +# ---- Executable ---- +add_executable(main + src/main.cpp + src/user_input.cpp + src/image_processing.cpp +) + +# ---- JSON (nlohmann) ---- +include(FetchContent) +FetchContent_Declare(json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz +) +FetchContent_MakeAvailable(json) + +# ---- Linking ---- +target_link_libraries(main + PRIVATE + ${OpenCV_LIBS} + nlohmann_json::nlohmann_json +) + +# ---- Debug info ---- +message(STATUS "OpenCV version: ${OpenCV_VERSION}") +message(STATUS "OpenCV include dirs: ${OpenCV_INCLUDE_DIRS}") +message(STATUS "OpenCV libraries: ${OpenCV_LIBS}") diff --git a/src/config.json b/src/config.json new file mode 100644 index 0000000..ea10dec --- /dev/null +++ b/src/config.json @@ -0,0 +1,23 @@ +{ + "confidence_level" : 0.5, + + + "output_res" : 1080, + "preview_output_res" : 256, + "preview_debug_max_res" : 640, + + + "min_face_res" : 96, + "min_upperbody_res" : 64, + "min_fullbody_res" : 32, + + + "input_folder" : "input", + "output_upperbody_folder": "output/upperbody_cropped", + "debug_upperbody_folder": "output/upperbody_debug", + "output_face_folder": "output/face_cropped", + "debug_face_folder": "output/face_debug", + "output_fullbody_folder": "output/fullbody_cropped", + "debug_fullbody_folder": "output/fullbody_debug", + "error_folder": "output/error_images" +} diff --git a/src/crop_config.h b/src/crop_config.h new file mode 100644 index 0000000..b50a920 --- /dev/null +++ b/src/crop_config.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +enum class CropType { + UpperBody = 1, + Face = 2, + FullBody = 3 +}; + +struct CropConfig +{ + std::string error_folder; + std::string debug_folder; + std::string output_folder; + float top_margin; + float bottom_margin; + CropType crop_type; +}; diff --git a/src/image_processing.cpp b/src/image_processing.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/image_processing.h b/src/image_processing.h new file mode 100644 index 0000000..39fc9b5 --- /dev/null +++ b/src/image_processing.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include "crop_config.h" +#include "user_input.h" + +struct ImageProcessConfig { + int output_res; + int preview_output_res; + int preview_debug_max_res; + float confidence_level; + int min_face_res; + int min_upperbody_res; + int min_fullbody_res; +}; + +void images_error( + const std::string& image_path, + const std::string& error_folder +); +int process_image( + const std::string& image_path, + const CropConfig& crop, + const ImageProcessConfig& process_config, + const PreviewConfig& preview_config +); diff --git a/src/image_processing.py b/src/image_processing.py deleted file mode 100644 index 97ed1f4..0000000 --- a/src/image_processing.py +++ /dev/null @@ -1,435 +0,0 @@ -import cv2 -import os -import imghdr -import win32com.client -from tqdm import tqdm -import numpy as np -import variable - -def images_error(image_path, error_folder): - shell = win32com.client.Dispatch("WScript.Shell") - filename_shortcut = os.path.basename(image_path) - shortcut_path = os.path.join(error_folder, filename_shortcut + ".lnk") - shortcut = shell.CreateShortcut(shortcut_path) - shortcut.TargetPath = os.path.abspath(image_path) - shortcut.Save() - - - -def process_image(image_path, - error_folder, - output_folder, - debug_output, - output_res, - preview_output_res, - preview_debug_max_res, - show_preview, - croptype, - top_margin_value, - bottom_margin_value,): - error_count = 0 - endX = 0 - endY = 0 - startX = 0 - startY = 0 - i = 0 - confidence = 0 - image = "" - output_image_path = "" - filename = "" - error_msg = "" - - net = cv2.dnn.readNetFromCaffe("deploy.prototxt.txt", "res10_300x300_ssd_iter_140000.caffemodel") - is_error = False - filename, extension = os.path.splitext(os.path.basename(image_path)) - image = cv2.imread(image_path) - image_format = imghdr.what(image_path) - supported_formats = ["jpg", "jpeg", "png", "webp"] - if image_format is None or image_format not in supported_formats: - print(f"\rInvalid image format or unsupported format, skipping {filename}{extension}") - error_count += 1 - else: - if image.shape[0] > 300 or image.shape[1] > 300: - blob = cv2.dnn.blobFromImage(cv2.resize(image, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0)) - net.setInput(blob) - detections = net.forward() - - for i in range(detections.shape[2]): - confidence = detections[0, 0, i, 2] - box = detections[0, 0, i, 3:7] * np.array([image.shape[1], image.shape[0], image.shape[1], image.shape[0]]) - (startX, startY, endX, endY) = box.astype(int) - - width = endX - startX - height = endY - startY - if confidence < variable.confidence_level: - print(f"\rConfidence level too low ({int(confidence * 100)}%), skipping face_{i} on {filename}{extension}") - error_msg = "CONFIDENCE LEVEL TOO LOW" - images_error(image_path, error_folder) - is_error = True - error_count += 1 - draw_rectangle(endX, - startX, - endY, - startY, - top_margin_value, - bottom_margin_value, - image, - output_res, - output_folder, - output_image_path, - filename, - image_path, - debug_output, - show_preview, - preview_output_res, - preview_debug_max_res, - is_error, - i, - confidence, - error_msg) - break - - if confidence > variable.confidence_level: - if croptype == 3: - if width < variable.min_fullbody_res or height < variable.min_fullbody_res: - print(f"\rFace resolution is too small for fullbody crop, skipping face_{i} on {filename}{extension}") - error_msg = "FACE RESOLUTION IS TOO SMALL" - images_error(image_path, error_folder) - is_error = True - error_count += 1 - draw_rectangle(endX, - startX, - endY, - startY, - top_margin_value, - bottom_margin_value, - image, - output_res, - output_folder, - output_image_path, - filename, - image_path, - debug_output, - show_preview, - preview_output_res, - preview_debug_max_res, - is_error, - i, - confidence, - error_msg) - break - elif croptype == 2: - if width < variable.min_face_res or height < variable.min_face_res: - print(f"\rFace resolution is too small for face crop, skipping face_{i} on {filename}{extension}") - error_msg = "FACE RESOLUTION IS TOO SMALL" - images_error(image_path, error_folder) - is_error = True - error_count += 1 - draw_rectangle(endX, - startX, - endY, - startY, - top_margin_value, - bottom_margin_value, - image, - output_res, - output_folder, - output_image_path, - filename, - image_path, - debug_output, - show_preview, - preview_output_res, - preview_debug_max_res, - is_error, - i, - confidence, - error_msg) - break - elif croptype == 1: - if width < variable.min_upperbody_res or height < variable.min_upperbody_res: - print(f"\rFace resolution is too small for upperbody crop, skipping face_{i} on {filename}{extension}") - error_msg = "FACE RESOLUTION IS TOO SMALL" - images_error(image_path, error_folder) - is_error = True - error_count += 1 - draw_rectangle(endX, - startX, - endY, - startY, - top_margin_value, - bottom_margin_value, - image, - output_res, - output_folder, - output_image_path, - filename, - image_path, - debug_output, - show_preview, - preview_output_res, - preview_debug_max_res, - is_error, - i, - confidence, - error_msg) - break - break - for i in range(detections.shape[2]): - if is_error == True: - break - confidence = detections[0, 0, i, 2] - - # Filter out weak detections - if confidence > variable.confidence_level: - box = detections[0, 0, i, 3:7] * np.array([image.shape[1], image.shape[0], image.shape[1], image.shape[0]]) - (startX, startY, endX, endY) = box.astype(int) - - width = endX - startX - height = endY - startY - is_error = draw_rectangle(endX, - startX, - endY, - startY, - top_margin_value, - bottom_margin_value, - image, - output_res, - output_folder, - output_image_path, - filename, - image_path, - debug_output, - show_preview, - preview_output_res, - preview_debug_max_res, - is_error, - i, - confidence, - error_msg) - else: - break - else: - print(f"\rThe resolution is too low for face detection, skipping {filename}{extension}") - images_error(image_path, error_folder) - error_count += 1 - return error_count - -def draw_rectangle(endX, - startX, - endY, - startY, - top_margin_value, - bottom_margin_value, - image, - output_res, - output_folder, - output_image_path, - filename, - image_path, - debug_output, - show_preview, - preview_output_res, - preview_debug_max_res, - is_error, - i, - confidence, - error_msg): - # Calculate the size of the square region to be cropped - square_size = min(endX - startX, endY - startY) - - # Calculate the coordinates for the square region - square_center_x = (endX + startX) // 2 - square_center_y = (endY + startY) // 2 - square_upper_left_x = square_center_x - square_size // 2 - square_upper_left_y = square_center_y - square_size // 2 - square_lower_right_x = square_upper_left_x + square_size - square_lower_right_y = square_upper_left_y + square_size - - width_square = square_lower_right_x - square_upper_left_x - height_square = square_lower_right_y - square_upper_left_y - - # Calculate the margin based on the height of the bounding box - top_margin_percent = int(square_size * top_margin_value) - bottom_margin_percent = int(square_size * bottom_margin_value) - left_margin_percent = int(square_size * bottom_margin_value) - right_margin_percent = int(square_size * bottom_margin_value) - - # Calculate the coordinates of the upper body region - upper_left_x = max(square_upper_left_x - left_margin_percent, 0) - upper_left_y = max(square_upper_left_y - top_margin_percent, 0) # Align to the top of the face - - # Calculate the lower right coordinates based on the maximum image dimensions - lower_right_x = min(square_lower_right_x + right_margin_percent, image.shape[1]) - lower_right_y = min(square_lower_right_y + bottom_margin_percent, image.shape[0]) - - # Adjust the upper_left_x and lower_right_x based on the width of the cropped region - width_square_margin = lower_right_x - upper_left_x - if width_square_margin > image.shape[1]: - shift_amount = width_square_margin - image.shape[1] - upper_left_x = max(upper_left_x - shift_amount, 0) - lower_right_x = image.shape[1] - - # Adjust the upper_left_y based on the height of the cropped region - height_square_margin = lower_right_y - upper_left_y - if height_square_margin > image.shape[0]: - shift_amount = height_square_margin - image.shape[0] - upper_left_y = max(upper_left_y - shift_amount, 0) - lower_right_y = image.shape[0] - - # Calculate the new square size based on the adjusted coordinates - square_margin_size = min(lower_right_x - upper_left_x, lower_right_y - upper_left_y) - - # Calculate the coordinates for the square region - square_margin_center_x = (lower_right_x + upper_left_x) // 2 - square_margin_upper_left_x = square_margin_center_x - square_margin_size // 2 - square_margin_upper_left_y = upper_left_y # Align to the top of the face - square_margin_lower_right_x = square_margin_upper_left_x + square_margin_size - square_margin_lower_right_y = square_margin_upper_left_y + square_margin_size - - # Calculate the center point of the second box - second_box_center_x = (startX + endX) // 2 - - # Calculate the shift amount for aligning the square_region horizontally - shift_amount = second_box_center_x - ((square_margin_upper_left_x + square_margin_lower_right_x) // 2) - - # Adjust the square_margin_upper_left_x and square_margin_lower_right_x based on the shift amount - square_margin_upper_left_x += shift_amount - square_margin_lower_right_x += shift_amount - - # Crop the image to the square region with margin - square_margin_upper_left_x = max(square_margin_upper_left_x, 0) - square_margin_lower_right_x = min(square_margin_lower_right_x, image.shape[1]) - - # Adjust square_margin_size based on the final coordinates - square_margin_size = square_margin_lower_right_x - square_margin_upper_left_x - - square_region = image[square_margin_upper_left_y:square_margin_upper_left_y + square_margin_size, square_margin_upper_left_x:square_margin_upper_left_x + square_margin_size] - - # Save the cropped and resized image - output_image_path = os.path.join(output_folder, f"{filename}_face_{i}.png") # type: ignore - resized_image = "" - - if square_region.size == 0: - is_error = True - error_msg = ("NO FACE DETECTED") - else: - if is_error == False: - resized_image = cv2.resize(square_region, (output_res, output_res)) - cv2.imwrite(output_image_path, resized_image) - - # Calculate the thickness of the rectangle based on the image resolution - resolution_thickness_ratio = image.shape[1] // 128 - thickness = max(resolution_thickness_ratio, 5) - - debug_image = image.copy() - - cv2.rectangle(debug_image, (startX, startY), (endX, endY), (0, 0, 255), thickness) #face rectangle - cv2.rectangle(debug_image, (square_upper_left_x, square_upper_left_y), (square_lower_right_x, square_lower_right_y), (0, 255, 0), thickness) #crop rectagle - cv2.rectangle(debug_image, (square_margin_upper_left_x, square_margin_upper_left_y), - (square_margin_upper_left_x + square_margin_size, square_margin_upper_left_y + square_margin_size), - (255,165,0), thickness) - font_scale = min(image.shape[1], image.shape[0]) / 1000 - font_thickness = max(1, int(min(image.shape[1], image.shape[0]) / 500)) - - # FIRST TEXT LABEL - resolution_text = f"{image.shape[1]}x{image.shape[0]} face_{i}_{width_square}px ({int(confidence * 100)}%)" - background_color = (255, 255, 0) - text_color = (0, 0, 0) - text_size, _ = cv2.getTextSize(resolution_text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, font_thickness) - - background_width = text_size[0] + 10 - background_height = text_size[1] + 10 - background = np.zeros((background_height, background_width, 3), dtype=np.uint8) - background[:, :] = background_color - - # Resize the background if its width is greater than the available width in debug_image - if background_width > debug_image.shape[1]: - ratio = debug_image.shape[1] / background_width - background_width = debug_image.shape[1] - background_height = int(background_height * ratio) - background = cv2.resize(background, (background_width, background_height)) - - cv2.putText(background, resolution_text, (10, text_size[1] + 5), cv2.FONT_HERSHEY_SIMPLEX, font_scale, text_color, font_thickness) - debug_image[0:background_height, 0:background_width] = background - - # SECOND TEXT LABEL - if is_error == True: - second_background_color = (0, 0, 255) - second_text_color = (0, 0, 0) - second_text = f"ERROR {error_msg}" - else: - second_background_color = (0, 255, 0) - second_text_color = (0, 0, 0) - second_text = "OK PASSED" - - second_font_scale = font_scale - second_font_thickness = font_thickness - second_text_size, _ = cv2.getTextSize(second_text, cv2.FONT_HERSHEY_SIMPLEX, second_font_scale, second_font_thickness) - - second_background_width = second_text_size[0] + 10 - second_background_height = second_text_size[1] + 10 - - second_background = np.zeros((second_background_height, second_background_width, 3), dtype=np.uint8) - second_background[:, :] = second_background_color - - if second_background_width > debug_image.shape[1]: - second_ratio = debug_image.shape[1] / second_background_width - second_background_width = debug_image.shape[1] - second_background_height = int(second_background_height * second_ratio) - second_background = cv2.resize(second_background, (second_background_width, second_background_height)) - - cv2.putText(second_background, second_text, (10, second_text_size[1] + 5), cv2.FONT_HERSHEY_SIMPLEX, second_font_scale, second_text_color, second_font_thickness) - - debug_image_height, debug_image_width, _ = debug_image.shape - combined_height = background_height + second_background_height - if combined_height > debug_image_height: - debug_image = cv2.resize(debug_image, (debug_image_width, combined_height)) - debug_image[background_height:combined_height, 0:second_background_width] = second_background - - filename = os.path.splitext(os.path.basename(image_path))[0] - debug_image_path = os.path.join(debug_output, f"{filename}_face_{i}.jpg") - cv2.imwrite(debug_image_path, debug_image) - - if show_preview == True: - preview(debug_image, resized_image, preview_output_res, preview_debug_max_res, is_error) - return is_error - -def preview(debug_image, - resized_image, - preview_output_res, - preview_debug_max_res, - is_error): - - # Resize the debug image to fit within the maximum window dimensions while maintaining the aspect ratio - window_width = debug_image.shape[1] - window_height = debug_image.shape[0] - - if window_width > preview_debug_max_res or window_height > preview_debug_max_res: - # Check if the width or height exceeds the maximum limits - width_scale_factor = preview_debug_max_res / window_width - height_scale_factor = preview_debug_max_res / window_height - scale_factor = min(width_scale_factor, height_scale_factor) - else: - # Check if the width or height is below the minimum limits - width_scale_factor = preview_output_res / window_width - height_scale_factor = preview_output_res / window_height - scale_factor = max(width_scale_factor, height_scale_factor) - - new_width = int(window_width * scale_factor) - new_height = int(window_height * scale_factor) - debug_preview_image = cv2.resize(debug_image, (new_width, new_height)) - - cv2.namedWindow("Debug Image", cv2.WINDOW_AUTOSIZE) - cv2.imshow("Debug Image", debug_preview_image) - cv2.setWindowProperty("Debug Image", cv2.WND_PROP_TOPMOST, 1) - cv2.setWindowProperty("Debug Image", cv2.WND_PROP_ASPECT_RATIO, cv2.WINDOW_KEEPRATIO) - - if is_error == False: - output_preview_image = cv2.resize(resized_image, (preview_output_res, preview_output_res)) - cv2.namedWindow("Output Image", cv2.WINDOW_AUTOSIZE) - cv2.imshow("Output Image", output_preview_image) - cv2.setWindowProperty("Output Image", cv2.WND_PROP_TOPMOST, 1) - cv2.setWindowProperty("Output Image", cv2.WND_PROP_ASPECT_RATIO, cv2.WINDOW_KEEPRATIO) - - cv2.waitKey(250) # Wait time \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..e1e513c --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,9 @@ +#include +#include +#include + +int main() +{ + std::ifstream config("config.json"); + return 0; +} diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 66df1c0..0000000 --- a/src/main.py +++ /dev/null @@ -1,155 +0,0 @@ -import os -import sys -import cv2 -import multiprocessing -from image_processing import process_image -from user_input import select_option, preview_window, clean_output -import variable -from tqdm import tqdm - -input_folder = variable.input_folder -output_res = variable.output_res -error_folder = variable.error_folder -preview_output_res = variable.preview_output_res -preview_debug_max_res = variable.preview_debug_max_res - -def process_image_worker(args): - input_file, \ - error_folder, \ - output_folder, \ - debug_output, \ - output_res, \ - preview_output_res, \ - preview_debug_max_res, \ - show_preview, \ - croptype, \ - top_margin_value, \ - bottom_margin_value \ - = args - - error_count = process_image(input_file, \ - error_folder, \ - output_folder, \ - debug_output, \ - output_res, \ - preview_output_res, \ - preview_debug_max_res, \ - show_preview, \ - croptype, \ - top_margin_value, \ - bottom_margin_value) - return error_count - - -if __name__ == '__main__': - def main(): - top_margin_value, \ - bottom_margin_value, \ - debug_output, \ - output_folder, \ - croptype = select_option() - - os.makedirs(input_folder, exist_ok=True) - os.makedirs(output_folder, exist_ok=True) - os.makedirs(debug_output, exist_ok=True) - os.makedirs(error_folder, exist_ok=True) - - show_preview, parallel = preview_window() - if parallel == True: - multithread(top_margin_value, \ - bottom_margin_value, \ - debug_output, \ - output_folder, \ - croptype,\ - show_preview) - return - clean_output(output_folder, debug_output, error_folder) - - input_files = [os.path.join(input_folder, file) for file in os.listdir(input_folder)] - progress_bar = tqdm(total=len(input_files), desc="[SINGLETHREAD] Processing images", dynamic_ncols=True) - - error_count = 0 - for image_path in input_files: - error_occurred = process_image(image_path, - error_folder, - output_folder, - debug_output, - output_res, - preview_output_res, - preview_debug_max_res, - show_preview, - croptype, - top_margin_value, - bottom_margin_value) - if error_occurred: - error_count += 1 - progress_bar.update(1) - - cv2.destroyAllWindows() - - total_images = len(input_files) - total_output_images = len(os.listdir(output_folder)) - print(f"Total images: {total_images}") - print(f"Processed images: {total_images - error_count}") - print(f"Total faces detected: {total_output_images}") - print(f"Error images: {error_count}") - - return parallel - - def multithread(top_margin_value, \ - bottom_margin_value, \ - debug_output, \ - output_folder, \ - croptype,\ - show_preview): - manager = multiprocessing.Manager() - error = manager.Value("i", 0) # Shared variable to track error and error count - - if sys.platform == "win32": - multiprocessing.freeze_support() - - clean_output(output_folder, debug_output, error_folder) - - # Get a list of input files - input_files = [os.path.join(input_folder, filename) for filename in os.listdir(input_folder) if - os.path.isfile(os.path.join(input_folder, filename))] - worker_args = [ - ( - input_file, - error_folder, - output_folder, - debug_output, - output_res, - preview_output_res, - preview_debug_max_res, - show_preview, - croptype, - top_margin_value, - bottom_margin_value, - ) - for input_file in input_files - ] - - # Create a multiprocessing Pool - pool = multiprocessing.Pool() - - # Apply the worker function to each input file using the multiprocessing Pool and tqdm - with tqdm(total=len(input_files), desc="[MULTITHREAD] Processing images", dynamic_ncols=True) as progress_bar: - for error_count in pool.imap_unordered(process_image_worker, worker_args): - error.value += error_count - progress_bar.update(1) - - cv2.destroyAllWindows() - - # Close the multiprocessing Pool - pool.close() - pool.join() - - total_images = len(input_files) - total_output_images = len(os.listdir(output_folder)) - print(f"Total images: {total_images}") - print(f"Processed images: {total_images - error.value}") - print(f"Total faces detected: {total_output_images}") - print(f"Error images: {error.value}") - - main() \ No newline at end of file diff --git a/src/user_input.cpp b/src/user_input.cpp new file mode 100644 index 0000000..c62e2e6 --- /dev/null +++ b/src/user_input.cpp @@ -0,0 +1,121 @@ +// user_input.cpp +#include "user_input.h" +#include +#include +#include +#include + +namespace fs = std::filesystem; + +CropConfig select_option() { + int option; + + while (true) { + std::cout << "Select a crop type:\n"; + std::cout << "1. Upper Body\n"; + std::cout << "2. Face\n"; + std::cout << "3. Full Body\n"; + std::cout << "Select: "; + std::cin >> option; + + if (option < 1 || option > 3) { + std::cout << "Invalid option. Please try again.\n\n"; + } else { + break; + } + } + + CropConfig config; + config.crop_type = (CropType)option; + + if (option == 1) { + config.top_margin = 1.0f; + config.bottom_margin = 3.0f; + config.output_folder = "output/upperbody_cropped"; + config.debug_folder = "output/upperbody_debug"; + } else if (option == 2) { + config.top_margin = 0.25f; + config.bottom_margin = 0.25f; + config.output_folder = "output/face_cropped"; + config.debug_folder = "output/face_debug"; + } else if (option == 3) { + config.top_margin = 1.0f; + config.bottom_margin = 12.0f; + config.output_folder = "output/fullbody_cropped"; + config.debug_folder = "output/fullbody_debug"; + } + + return config; +} + + +// Are you sure this is not some bs +PreviewConfig preview_window() { + std::string input; + + while (true) { + std::cout << "Show preview window? [Y]es/[N]o: "; + std::cin >> input; + + if (input == "y" || input == "Y") { + PreviewConfig config; + config.show_preview = true; + config.parallel = false; + return config; + } else if (input == "n" || input == "N") { + PreviewConfig config; + config.show_preview = false; + config.parallel = true; + return config; + } else { + std::cout << "Invalid option. Please enter 'Y' or 'N'.\n\n"; + } + } +} + +int count_files(const std::string& folder) { + int count = 0; + for (const auto& entry : fs::recursive_directory_iterator(folder)) { + if (fs::is_regular_file(entry)) count++; + } + return count; +} + +void delete_files(const std::string& folder) { + for (const auto& entry : fs::recursive_directory_iterator(folder)) { + if (fs::is_regular_file(entry)) { + fs::remove(entry.path()); + } + } +} + +void clean_output(const std::string& output_folder, + const std::string& debug_output, + const std::string& error_folder) { + + while (!fs::is_empty(output_folder)) { + char input; + std::cout << "Output folders are not empty, clean it? [Y]es/[N]o: "; + std::cin >> input; + + if (input == 'y' || input == 'Y') { + for (int i = 5; i > 0; i--) { + std::cout << "\rDeleting files in " << i << "... [PRESS CTRL+C TO CANCEL]" << std::flush; + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + std::cout << "\n"; + + delete_files(output_folder); + delete_files(debug_output); + delete_files(error_folder); + + std::cout << "Done cleaning the output folder.\n\n"; + + } else if (input == 'n' || input == 'N') { + std::cout << "\n"; + break; + } else { + std::cout << "Invalid option. Please enter 'Y' or 'N'.\n\n"; + } + } +} diff --git a/src/user_input.h b/src/user_input.h new file mode 100644 index 0000000..1010f9f --- /dev/null +++ b/src/user_input.h @@ -0,0 +1,16 @@ +#pragma once + +#include "crop_config.h" + +struct PreviewConfig { + bool show_preview; + bool parallel; +}; + +CropConfig select_option(); +PreviewConfig preview_window(); +void clean_output( + const std::string& output_folder, + const std::string& debug_output, + const std::string& error_folder +); diff --git a/src/user_input.py b/src/user_input.py deleted file mode 100644 index 8f44f8b..0000000 --- a/src/user_input.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -import time -import variable -from tqdm import tqdm - -def select_option(): - output_upperbody_folder = variable.output_upperbody_folder - debug_upperbody_folder = variable.debug_upperbody_folder - output_face_folder = variable.output_face_folder - debug_face_folder = variable.debug_face_folder - output_fullbody_folder = variable.output_fullbody_folder - debug_fullbody_folder = variable.debug_fullbody_folder - - while True: - option = input("Select a crop type:\n1. Upper Body\n2. Face\n3. Full body (try to crop as much as possible)\nSelect: ") - if option not in ["1", "2", "3"]: - print("Invalid option selected. Please try again.") - print("") - else: - break - if option == "1": - top_margin_value = 1 - bottom_margin_value = 3 - debug_output = debug_upperbody_folder - output_folder = output_upperbody_folder - croptype = 1 - elif option == "2": - top_margin_value = 0.25 - bottom_margin_value = 0.25 - debug_output = debug_face_folder - output_folder = output_face_folder - croptype = 2 - elif option == "3": - top_margin_value = 1 - bottom_margin_value = 12 - debug_output = debug_fullbody_folder - output_folder = output_fullbody_folder - croptype = 3 - return [top_margin_value, #type: ignore - bottom_margin_value, #type: ignore - debug_output, #type: ignore - output_folder, #type: ignore - croptype] #type: ignore - -def preview_window(): - show_preview = False - multithread = False - while True: - show_preview = input("Show preview window? [Y]es/[N]o: ") - if show_preview.lower() == "y": - show_preview = True - parallel = False - break - elif show_preview.lower() == "n": - show_preview = False - parallel = True - break - else: - print("Invalid option selected. Please enter 'Y' or 'N'.") - print("") - return show_preview, parallel - -def clean_output(output_folder, debug_output, error_folder): - while len(os.listdir(output_folder)) > 0: - file_exist = input("Output folders are not empty, clean it? [Y]es/[N]o: ") - if file_exist.lower() == "y": - i = 5 - while i <= 5 and i > 0: - # Add a delay of 5 seconds before proceeding with the deletion - print(f"\rDeleting files in {i}... [PRESS CTRL+C TO CANCEL]") - time.sleep(1) - i -= 1 - - # Get the total number of files - total_files = sum([len(files) for _, _, files in os.walk(output_folder)]) - - # Deleting files in the output folder and subdirectories with a progress bar - with tqdm(total=total_files, unit='file') as pbar: - for root, dirs, files in os.walk(output_folder): - for file_name in files: - file_path = os.path.join(root, file_name) - os.remove(file_path) - pbar.update(1) - - with tqdm(total=total_files, unit='file') as pbar: - for root, dirs, files in os.walk(debug_output): - for file_name in files: - file_path = os.path.join(root, file_name) - os.remove(file_path) - pbar.update(1) - - with tqdm(total=total_files, unit='file') as pbar: - for root, dirs, files in os.walk(error_folder): - for file_name in files: - file_path = os.path.join(root, file_name) - os.remove(file_path) - pbar.update(1) - - print("Done cleaning the output folder.") - print("") - elif file_exist.lower() == "n": - print("") - break # Exit the while loop - else: - print("Invalid option selected. Please enter 'Y' or 'N'.") - print("") diff --git a/src/variable.py b/src/variable.py deleted file mode 100644 index fda484a..0000000 --- a/src/variable.py +++ /dev/null @@ -1,20 +0,0 @@ -confidence_level = 0.5 - -# Output cropped resolution (pixel) -output_res = 1080 -preview_output_res = 256 -preview_debug_max_res = 640 - -# Minimun face bounding box size (pixel) -min_face_res = 96 -min_upperbody_res = 64 -min_fullbody_res = 32 - -input_folder = "input" -output_upperbody_folder = "output/upperbody_cropped" -debug_upperbody_folder = "output/upperbody_debug" -output_face_folder = "output/face_cropped" -debug_face_folder = "output/face_debug" -output_fullbody_folder = "output/fullbody_cropped" -debug_fullbody_folder = "output/fullbody_debug" -error_folder = "output/error_images" \ No newline at end of file