diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 63f892e..37f8bc5 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -9,31 +9,162 @@ on: jobs: build: runs-on: macos-latest + steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - name: Install dependencies run: | - brew install pkg-config opencv libimobiledevice libplist + echo "==== brew update ====" + brew update + + echo "==== Unlink bazelisk if present ====" + brew unlink bazelisk || true + + echo "==== Install core libraries ====" + brew install pkg-config opencv libimobiledevice libplist cmake make bazelisk + + echo "==== Installed versions ====" + echo "pkg-config version: $(pkg-config --version)" + echo "opencv version (pkg-config): $(pkg-config --modversion opencv4 || pkg-config --modversion opencv)" + echo "libimobiledevice include dir: $(ls /usr/local/include/libimobiledevice 2>/dev/null || ls /opt/homebrew/include/libimobiledevice 2>/dev/null)" + echo "libplist include dir: $(ls /usr/local/include/plist 2>/dev/null || ls /opt/homebrew/include/plist 2>/dev/null)" + echo "cmake version: $(cmake --version)" + echo "bazelisk version: $(bazelisk version)" + + - name: Build & “private-install” FlatBuffers v24.3.25 + run: | + echo "==== Cloning FlatBuffers v24.3.25 ====" + git clone https://github.com/google/flatbuffers.git flatbuffers_src + cd flatbuffers_src + git checkout v24.3.25 + + INSTALL_DIR="${GITHUB_WORKSPACE}/flatbuffers_install" + echo "→ Installing FlatBuffers into: ${INSTALL_DIR}" + mkdir -p build && cd build + + echo "→ Running cmake for FlatBuffers" + cmake -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="${INSTALL_DIR}" \ + .. + echo "→ Building and installing FlatBuffers" + cmake --build . --target install + + echo "==== FlatBuffers install complete ====" + ls -R "${INSTALL_DIR}/include/flatbuffers" + ls -R "${INSTALL_DIR}/lib" + + - name: Add Bazelisk to PATH + run: | + BREW_BAZELISK="$(brew --prefix bazelisk)" + echo "→ brew prefix for bazelisk: ${BREW_BAZELISK}" + echo "${BREW_BAZELISK}/bin" >> $GITHUB_PATH + echo "→ Updated PATH: $PATH" + + - name: Clone TensorFlow + run: | + echo "==== Cloning TensorFlow repository ====" + git clone https://github.com/tensorflow/tensorflow.git src/tensorflow + ls -R "${GITHUB_WORKSPACE}/src/tensorflow" | head -n 50 + + - name: Build TensorFlow Lite via Bazelisk (with debug) + working-directory: src/tensorflow + env: + TF_NEED_CUDA: 0 + TF_NEED_ROCM: 0 + TF_NEED_OPENCL_SYCL: 0 + TF_ENABLE_XLA: 0 + TF_NEED_TENSORRT: 0 + run: | + echo "==== TensorFlow configure environment variables ====" + echo "TF_NEED_CUDA=${TF_NEED_CUDA}" + echo "TF_NEED_ROCM=${TF_NEED_ROCM}" + echo "TF_NEED_OPENCL_SYCL=${TF_NEED_OPENCL_SYCL}" + echo "TF_ENABLE_XLA=${TF_ENABLE_XLA}" + echo "TF_NEED_TENSORRT=${TF_NEED_TENSORRT}" - - name: Configure with CMake + echo "==== Running TensorFlow's ./configure script ====" + yes "" | ./configure 2>&1 | tee configure.log + + echo "==== Contents of configure.log ====" + head -n 200 configure.log + + echo "==== Starting Bazel build for TensorFlow Lite ====" + bazelisk build -c opt //tensorflow/lite:libtensorflowlite.dylib 2>&1 | tee tflite_build.log + + echo "==== Bazel build artifacts under $GITHUB_WORKSPACE/src/tensorflow/bazel-bin/tensorflow/lite ====" + ls "${GITHUB_WORKSPACE}/src/tensorflow/bazel-bin/tensorflow/lite" || echo " (no bazel-bin/tensorflow/lite directory yet)" + + - name: Configure with CMake (with debug echos) run: | - ./configure + echo "==== Entering build directory ====" mkdir -p build cd build - cmake .. + pwd + ls -la "${GITHUB_WORKSPACE}" + + # Grab the FlatBuffers include path from our private install: + FLAT_DIR="${GITHUB_WORKSPACE}/flatbuffers_install/include" + echo "→ FLATBUFFERS_INCLUDE_DIR = ${FLAT_DIR}" + ls "${FLAT_DIR}" || echo " (flatbuffers include dir missing)" + + # Determine Brew prefix and dump it + BREW_PREFIX="$(brew --prefix)" + echo "→ BREW_PREFIX = ${BREW_PREFIX}" + echo "→ ls ${BREW_PREFIX}/include:" + ls "${BREW_PREFIX}/include" || echo " (no include directory?)" + + # Check that libimobiledevice headers actually exist + echo "→ ls ${BREW_PREFIX}/include/libimobiledevice:" + ls "${BREW_PREFIX}/include/libimobiledevice" || echo " (no libimobiledevice directory)" + + # Check that afc.h is present + echo "→ file ${BREW_PREFIX}/include/libimobiledevice/afc.h:" + if [ -f "${BREW_PREFIX}/include/libimobiledevice/afc.h" ]; then + echo " OK: afc.h exists" + else + echo " MISSING: afc.h not found" + fi - - name: Build + # Check for libimobiledevice dylib symlink and versioned file + echo "→ ls ${BREW_PREFIX}/lib/libimobiledevice*.dylib:" + ls "${BREW_PREFIX}/lib/libimobiledevice"*.dylib || echo " (no libimobiledevice*.dylib found)" + + # Check for libplist headers and dylib + echo "→ ls ${BREW_PREFIX}/include/plist:" + ls "${BREW_PREFIX}/include/plist" || echo " (no plist directory)" + + echo "→ ls ${BREW_PREFIX}/lib/libplist*.dylib:" + ls "${BREW_PREFIX}/lib/libplist"*.dylib || echo " (no libplist*.dylib found)" + + # Print the exact paths being passed to CMake + echo "==== CMake variables ====" + echo "TFLITE_INCLUDE_DIR = ${GITHUB_WORKSPACE}/src/tensorflow" + echo "TFLITE_LIB = ${GITHUB_WORKSPACE}/src/tensorflow/bazel-bin/tensorflow/lite/libtensorflowlite.dylib" + echo "FLATBUFFERS_INCLUDE_DIR = ${FLAT_DIR}" + echo "IMOBILEDEVICE_INCLUDE_DIR = ${BREW_PREFIX}/include" + echo "IMOBILEDEVICE_LIB = ${BREW_PREFIX}/lib/libimobiledevice.dylib" + echo "PLIST_LIB = ${BREW_PREFIX}/lib/libplist.dylib" + + echo "==== Running CMake configure ====" + cmake \ + -DTFLITE_INCLUDE_DIR="${GITHUB_WORKSPACE}/src/tensorflow" \ + -DTFLITE_LIB="${GITHUB_WORKSPACE}/src/tensorflow/bazel-bin/tensorflow/lite/libtensorflowlite.dylib" \ + -DFLATBUFFERS_INCLUDE_DIR="${FLAT_DIR}" \ + -DIMOBILEDEVICE_INCLUDE_DIR="${BREW_PREFIX}/include" \ + -DIMOBILEDEVICE_LIB="${BREW_PREFIX}/lib/libimobiledevice.dylib" \ + -DPLIST_LIB="${BREW_PREFIX}/lib/libplist.dylib" \ + .. 2>&1 | tee cmake_configure.log + + echo "==== Contents of cmake_configure.log ====" + head -n 200 cmake_configure.log + + - name: Build the project run: | + echo "==== Starting CMake build ====" cd build - cmake --build . - - # - name: Test - # run: | - # cd build - # cmake --build . --target check + cmake --build . 2>&1 | tee cmake_build.log - # - name: Dist Check - # run: | - # cd build - # cmake --build . --target distcheck + echo "==== Build output summary ====" + tail -n 50 cmake_build.log diff --git a/CMakeLists.txt b/CMakeLists.txt index 38e9156..1c1e758 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,74 +1,191 @@ cmake_minimum_required(VERSION 3.10) -project(ipurity) +project(iPurity) -# Use C++11 standard +# Require C++17 set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED True) +set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Include directories -include_directories( - ${PROJECT_SOURCE_DIR}/include - /opt/homebrew/opt/opencv/include/opencv4 - /opt/homebrew/include -) +# ----------------------------------------------------------------------------- +# A) Allow the user to override key include/lib paths on the command line +# ----------------------------------------------------------------------------- + +# 1) FlatBuffers +if (NOT DEFINED FLATBUFFERS_INCLUDE_DIR) + set(FLATBUFFERS_INCLUDE_DIR "" CACHE PATH "Path to FlatBuffers include directory") +endif() + +# 2) TensorFlow Lite +if (NOT DEFINED TFLITE_INCLUDE_DIR) + set(TFLITE_INCLUDE_DIR "" CACHE PATH "Path to TensorFlow Lite include directory") +endif() +if (NOT DEFINED TFLITE_LIB) + set(TFLITE_LIB "" CACHE FILEPATH "Path to TensorFlow Lite library (libtensorflowlite.dylib or .so)") +endif() + +# 3) libimobiledevice / libplist +if (NOT DEFINED IMOBILEDEVICE_INCLUDE_DIR) + set(IMOBILEDEVICE_INCLUDE_DIR "" CACHE PATH "Path to libimobiledevice headers") +endif() +if (NOT DEFINED IMOBILEDEVICE_LIB) + set(IMOBILEDEVICE_LIB "" CACHE FILEPATH "Path to libimobiledevice library") +endif() +if (NOT DEFINED PLIST_LIB) + set(PLIST_LIB "" CACHE FILEPATH "Path to libplist library") +endif() + +# ----------------------------------------------------------------------------- +# 1. Find OpenCV (unchanged) +# ----------------------------------------------------------------------------- +find_package(OpenCV REQUIRED) +message(STATUS "→ OpenCV include dirs: ${OpenCV_INCLUDE_DIRS}") +message(STATUS "→ OpenCV libraries: ${OpenCV_LIBS}") + +# ----------------------------------------------------------------------------- +# 2. Find TensorFlow Lite (only if not overridden) +# ----------------------------------------------------------------------------- +if (NOT TFLITE_INCLUDE_DIR) + find_path( + TFLITE_INCLUDE_DIR + NAMES tensorflow/lite/interpreter.h + PATHS /usr/local/include /opt/homebrew/include + ) +endif() + +if (NOT TFLITE_LIB) + find_library( + TFLITE_LIB + NAMES tensorflow-lite tensorflowlite + PATHS /usr/local/lib /opt/homebrew/lib + ) +endif() + +if (NOT TFLITE_INCLUDE_DIR OR NOT TFLITE_LIB) + message(FATAL_ERROR + "TensorFlow Lite not found!\n" + "Please run ./install.sh to install dependencies, or pass:\n" + " -DTFLITE_INCLUDE_DIR=/path/to/tensorflow/lite/includes\n" + " -DTFLITE_LIB=/path/to/libtensorflowlite.dylib" + ) +endif() +message(STATUS "→ TFLite include dir: ${TFLITE_INCLUDE_DIR}") +message(STATUS "→ TFLite library: ${TFLITE_LIB}") -# Find OpenCV via pkg-config (try opencv4, then fallback to opencv) -find_package(PkgConfig REQUIRED) -pkg_check_modules(OPENCV opencv4) -if(NOT OPENCV_FOUND) - pkg_check_modules(OPENCV opencv) +# ----------------------------------------------------------------------------- +# 3. Find FlatBuffers (only if FLATBUFFERS_INCLUDE_DIR is empty) +# ----------------------------------------------------------------------------- +if (NOT FLATBUFFERS_INCLUDE_DIR) + find_path( + FLATBUFFERS_INCLUDE_DIR + NAMES flatbuffers/flatbuffers.h + PATHS /usr/local/include /opt/homebrew/include + ) + if (NOT FLATBUFFERS_INCLUDE_DIR) + message(FATAL_ERROR + "FlatBuffers headers not found!\n" + "Please run ./install.sh to install dependencies, or pass:\n" + " -DFLATBUFFERS_INCLUDE_DIR=/path/to/flatbuffers/includes" + ) + endif() + message(STATUS "→ Using system FlatBuffers headers: ${FLATBUFFERS_INCLUDE_DIR}") +else() + message(STATUS "→ Using user-provided FlatBuffers headers: ${FLATBUFFERS_INCLUDE_DIR}") endif() -if(NOT OPENCV_FOUND) - message(FATAL_ERROR "OpenCV not found!") + +# ----------------------------------------------------------------------------- +# 4. Find libimobiledevice and libplist +# ----------------------------------------------------------------------------- +# If the user did not pass any of these on the command line, try to locate. +if (NOT IMOBILEDEVICE_INCLUDE_DIR) + find_path( + IMOBILEDEVICE_INCLUDE_DIR + NAMES libimobiledevice/afc.h + PATHS /usr/local/include /opt/homebrew/include + ) endif() -include_directories(${OPENCV_INCLUDE_DIRS}) -link_directories(${OPENCV_LIBRARY_DIRS}) +if (NOT IMOBILEDEVICE_LIB) + find_library( + IMOBILEDEVICE_LIB + NAMES imobiledevice + PATHS /usr/local/lib /opt/homebrew/lib + ) +endif() -# Set additional libraries (iMobileDevice) -set(IMOBILEDEVICE_LIBS "-limobiledevice -lplist") -link_directories(/opt/homebrew/lib) +if (NOT PLIST_LIB) + find_library( + PLIST_LIB + NAMES plist + PATHS /usr/local/lib /opt/homebrew/lib + ) +endif() + +if (NOT IMOBILEDEVICE_INCLUDE_DIR OR NOT IMOBILEDEVICE_LIB OR NOT PLIST_LIB) + message(FATAL_ERROR + "libimobiledevice or libplist not found!\n" + " • Install via Homebrew: brew install libimobiledevice libplist\n" + " • Or specify paths explicitly when invoking CMake:\n" + " -DIMOBILEDEVICE_INCLUDE_DIR=/path/to/libimobiledevice/includes \\\n" + " -DIMOBILEDEVICE_LIB=/path/to/libimobiledevice.dylib \\\n" + " -DPLIST_LIB=/path/to/libplist.dylib" + ) +endif() -# Define the executable and its source files -add_executable(ipurity +message(STATUS "→ libimobiledevice include dir: ${IMOBILEDEVICE_INCLUDE_DIR}") +message(STATUS "→ libimobiledevice lib: ${IMOBILEDEVICE_LIB}") +message(STATUS "→ libplist lib: ${PLIST_LIB}") + +# ----------------------------------------------------------------------------- +# 5. Define the iPurity executable +# ----------------------------------------------------------------------------- +add_executable(iPurity src/main.cpp src/scanner.cpp - src/afc_client_pool.cpp src/nsfw_detector.cpp + src/afc_client_pool.cpp src/afc_helpers.cpp ) +# ----------------------------------------------------------------------------- +# 6. Set include directories (in the order we want) +# ----------------------------------------------------------------------------- +target_include_directories(iPurity PRIVATE + ${OpenCV_INCLUDE_DIRS} + ${TFLITE_INCLUDE_DIR} + ${FLATBUFFERS_INCLUDE_DIR} + ${IMOBILEDEVICE_INCLUDE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/include +) -# Set compiler warnings -target_compile_options(ipurity PRIVATE -Wall -Wextra) - -# Link libraries -target_link_libraries(ipurity ${OPENCV_LIBRARIES} ${IMOBILEDEVICE_LIBS}) +# ----------------------------------------------------------------------------- +# 7. Compiler warnings +# ----------------------------------------------------------------------------- +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(iPurity PRIVATE -Wall -Wextra) +endif() -#------------------------------------------------------------------------------ -# Custom targets to mimic Makefile phony targets for CI pipeline -#------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- +# 8. Link libraries +# ----------------------------------------------------------------------------- +target_link_libraries(iPurity PRIVATE + ${OpenCV_LIBS} + ${TFLITE_LIB} + ${IMOBILEDEVICE_LIB} + ${PLIST_LIB} + "-framework Accelerate" # TFLite on macOS needs Accelerate +) -# Dummy "configure" target +# ----------------------------------------------------------------------------- +# 9. Custom targets (configure, check, dist, install) +# ----------------------------------------------------------------------------- add_custom_target(configure - COMMAND ${CMAKE_COMMAND} -E echo "Running dummy configure script..." - COMMAND ${CMAKE_COMMAND} -E echo "All dependencies are assumed to be in place for Apple Silicon." + COMMAND ${CMAKE_COMMAND} -E echo "Ensure all dependencies are installed." ) - -# Dummy "check" target add_custom_target(check - COMMAND ${CMAKE_COMMAND} -E echo "Running 'make check' (no tests implemented)." + COMMAND ${CMAKE_COMMAND} -E echo "No tests implemented." ) - -# "dist" target: creates a tarball of the distribution add_custom_target(dist - COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_SOURCE_DIR}/dist - COMMAND ${CMAKE_COMMAND} -E chdir ${PROJECT_SOURCE_DIR} /usr/bin/tar -czf dist/ipurity.tar.gz CMakeLists.txt LICENSE README.md configure include src - COMMENT "Creating distribution tarball..." -) - -# Installation -install(TARGETS ipurity - RUNTIME DESTINATION bin + COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_SOURCE_DIR}/dist + COMMAND ${CMAKE_COMMAND} -E chdir ${PROJECT_SOURCE_DIR} /usr/bin/tar -czf dist/iPurity.tar.gz CMakeLists.txt LICENSE README.md configure install.sh include src models + COMMENT "Creating distribution tarball..." ) - +install(TARGETS iPurity RUNTIME DESTINATION bin) diff --git a/README.md b/README.md index 4fe27be..f43a813 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ iPurity is a simple NSFW (Not Safe For Work) detector for iOS devices. -It utilizes AFC (Apple File Conduit) to list and open each media file and OpenCV to detect nsfw images. +It utilizes AFC (Apple File Conduit) to list and open each media file and OpenCV to detect NSFW images. -Note: Tested only on Apple Silicon Mac +> **Note**: Tested only on Apple Silicon Mac ## Why did I make this? - The program was created because one of my younger siblings accidentally encountered NSFW content. @@ -14,16 +14,95 @@ Note: Tested only on Apple Silicon Mac - This program aims to assist in identifying potentially inappropriate content. ## Disclaimer -- There may be many false negatives; however, it can help reduce the dataset to scan by approximately 90-95%. +- There may be many false negatives; however, it can help reduce the dataset to scan by approximately 90–95%. ## Prerequisites -- libimobiledevice -- OpenCV -- Make +Before building iPurity from source, ensure you have the following installed: + +- **Homebrew** (for macOS package management) +- **libimobiledevice** (C library and headers) +- **libplist** (C library and headers) +- **OpenCV** (C++ library and headers) +- **TensorFlow Lite** (C++ library and headers, built via Bazel) +- **Bazel** (to build TensorFlow Lite) +- **CMake** (≥ 3.10) +- **Make** +- **FlatBuffers** (via Homebrew) + +Below are installation steps to get each prerequisite in place. + +### 1. Install Homebrew (if not already installed) + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +### 2. Install system‐wide dependencies via Homebrew + +```bash +brew install libimobiledevice libplist opencv flatbuffers cmake make bazel +``` +- `libimobiledevice` and `libplist` allow AFC access to iOS devices. +- `opencv` provides image processing routines. +- `flatbuffers` is required by TensorFlow Lite headers. +- `cmake` and `make` drive the build system. +- `bazel` is needed to build TensorFlow Lite from source. + +### 3. Clone and build TensorFlow Lite + +iPurity expects TensorFlow Lite’s C++ headers and `libtensorflowlite.so` (or `.dylib`) built with Bazel. Follow these steps: + +1. **Clone TensorFlow** under your project folder (we’ll assume `/Users/agent-hellboy/iPurity/src/tensorflow`): + + ```bash + cd /Users/agent-hellboy/iPurity/src + git clone https://github.com/tensorflow/tensorflow.git + ``` + +2. **Enter the TensorFlow directory**: + + ```bash + cd tensorflow + ``` + +3. **Configure (optional, most defaults suffice)**: + + ```bash + ./configure + ``` + + - If prompted about Python, just press Enter (we only need C++ build). + - When asked for iOS or ROCm support, answer `N`. + +4. **Build TFLite with Bazel**: + + ```bash + bazel build -c opt //tensorflow/lite:libtensorflowlite.so + ``` + + This produces: + + ``` + /Users/agent-hellboy/iPurity/src/tensorflow/bazel-bin/tensorflow/lite/libtensorflowlite.so + ``` + +5. **Note the include directory**: + + ``` + /Users/agent-hellboy/iPurity/src/tensorflow (root containing `tensorflow/lite/interpreter.h`) + ``` + + and the FlatBuffers headers will be available at: + ``` + /usr/local/include/flatbuffers (via Homebrew) + ``` + ## Installation +You have two main options: install via our Homebrew tap, or build from source. + ### Option 1: Homebrew (Custom Tap) You can install iPurity from our custom Homebrew tap: @@ -33,37 +112,94 @@ brew tap Agent-Hellboy/homebrew-agent-hellboy-formula brew install agent-hellboy/homebrew-agent-hellboy-formula/ipurity ``` +This installs the prebuilt binary and handles dependencies automatically. + ### Option 2: Build from Source -```bash -./configure -mkdir build -cd build -cmake .. -cmake --build . -``` -### Installation -```bash -sudo cmake --build . --target install -``` +1. **Ensure all prerequisites are installed** (see above). + +2. **Clone iPurity** and enter the project root: + + ```bash + git clone https://github.com/Agent-Hellboy/iPurity.git + cd iPurity + ``` + +3. **Configure (optional, most defaults suffice)**: + + TODO + configure to build for macOS so that we avoid passing build flags during CMake configuration. + +4. **Create a `build/` directory** and configure with CMake, passing the TensorFlow Lite include and library paths: + + ```bash + mkdir build && cd build + cmake -DTFLITE_INCLUDE_DIR=/Users/agent-hellboy/iPurity/src/tensorflow -DTFLITE_LIB=/Users/agent-hellboy/iPurity/src/tensorflow/bazel-bin/tensorflow/lite/libtensorflowlite.so -DCMAKE_BUILD_TYPE=Release .. + ``` + + - `TFLITE_INCLUDE_DIR` should point to the TensorFlow root that contains `tensorflow/lite/interpreter.h`. + - `TFLITE_LIB` should point to the Bazel‐built `libtensorflowlite.so`. + - Homebrew’s FlatBuffers are found automatically via `/usr/local/include` or `/opt/homebrew/include`. + +5. **Build**: + + ```bash + cmake --build . --config Release + ``` + +6. **Install (optional)**: + + ```bash + sudo cmake --build . --target install + ``` + + This installs the `ipurity` binary to `/usr/local/bin/ipurity`. ## Usage -```bash -./ipurity //By default is set to 0.6 +From the project root (so that `models/nsfw_model.tflite` is found relative to the binary), run: + +```bash +./build/iPurity ``` +- `` is the NSFW confidence threshold (default `0.6` if omitted). +- Example: + ```bash + ./build/iPurity 0.7 + ``` + +If an iPhone/iPad is connected, unlocked, and trusted, iPurity will scan `/DCIM` on the device. Otherwise, it will attempt to scan `/DCIM` on the local filesystem. + ## Development -### Build with `-fsanitize=thread` option +### Scanning a Local Folder Instead of a Device + +By default, iPurity tries to open AFC sessions to scan an attached iOS device. If you want to scan a local directory (e.g. a sample “DCIM” of images): + +1. Create a local folder and copy some images: + ```bash + mkdir -p LocalDCIM + cp ~/Pictures/*.jpg LocalDCIM/ + ``` +2. Edit `src/scanner.cpp` (or `nsfw_detector.cpp`) to replace `"/DCIM"` with `"LocalDCIM"`, then rebuild. + +### Build with Thread Sanitizer + +To detect data races, build with `-fsanitize=thread`: ```bash export CFLAGS="-fsanitize=thread" export LDFLAGS="-fsanitize=thread" -cmake -DCMAKE_CXX_FLAGS="-fsanitize=thread" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread" .. +cd build +cmake -DCMAKE_CXX_FLAGS="-fsanitize=thread" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread" .. +cmake --build . --config Debug ``` -### Architecture Overview +--- + +## Architecture Overview + ``` +----------------+ | main() | @@ -102,8 +238,6 @@ cmake -DCMAKE_CXX_FLAGS="-fsanitize=thread" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize +-------------------------------+ ``` -## License +## License iPurity is released under the MIT License. - - diff --git a/configure b/configure index 0745e06..6101898 100755 --- a/configure +++ b/configure @@ -6,6 +6,8 @@ # - OpenCV (via pkg-config) # - libimobiledevice (via a compile test) # - libplist (via a compile test) +# - TensorFlow Lite headers & library (existence check) +# - FlatBuffers headers (existence check in Bazel output or Homebrew) # # Exits with an error if any dependency is missing. # Also creates unversioned symlinks for libimobiledevice/libplist if needed. @@ -48,14 +50,13 @@ echo "OpenCV: found." ############################################################################### # 4. Check for libimobiledevice ############################################################################### -# Many times, libimobiledevice doesn't ship a pkg-config file, so we do a compile test. TMPFILE=$(mktemp /tmp/check_imobile.XXXXXX.cpp) cat << EOF > "$TMPFILE" #include int main() { return 0; } EOF -# Attempt to compile it. For Apple Silicon, Homebrew is usually /opt/homebrew/include +# Compile test (Homebrew on Apple Silicon typically lives under /opt/homebrew/include) if ! g++ -I/opt/homebrew/include -c "$TMPFILE" -o /dev/null 2>/dev/null; then echo "Error: Could not compile a test program with libimobiledevice." echo " Make sure libimobiledevice is installed: brew install libimobiledevice" @@ -68,7 +69,6 @@ echo "libimobiledevice: found." ############################################################################### # 5. Check for libplist ############################################################################### -# We'll do a similar compile test. The main header is . TMPFILE=$(mktemp /tmp/check_plist.XXXXXX.cpp) cat << EOF > "$TMPFILE" #include @@ -89,7 +89,91 @@ rm -f "$TMPFILE" echo "libplist: found." ############################################################################### -# 6. Create Symlinks for Versioned .dylib Files (if needed) +# 6. Check for TensorFlow Lite headers & library +############################################################################### +PROJECT_ROOT="$(cd "$(dirname "$0")" && pwd)" + +# 6a. Header checks - check system locations first +TFLITE_HDR_SYSTEM1="/usr/local/include/tensorflow/lite/interpreter.h" +TFLITE_HDR_SYSTEM2="/opt/homebrew/include/tensorflow/lite/interpreter.h" +TFLITE_HDR_SRC="$PROJECT_ROOT/src/tensorflow/tensorflow/lite/interpreter.h" + +if [ -f "$TFLITE_HDR_SYSTEM1" ]; then + TFLITE_HDR="$TFLITE_HDR_SYSTEM1" + echo "Found TensorFlow Lite headers in system location: $TFLITE_HDR" +elif [ -f "$TFLITE_HDR_SYSTEM2" ]; then + TFLITE_HDR="$TFLITE_HDR_SYSTEM2" + echo "Found TensorFlow Lite headers in system location: $TFLITE_HDR" +elif [ -f "$TFLITE_HDR_SRC" ]; then + TFLITE_HDR="$TFLITE_HDR_SRC" + echo "Found TensorFlow Lite headers in project directory: $TFLITE_HDR" +else + echo "Error: TensorFlow Lite header not found (tensorflow/lite/interpreter.h)." + echo " Please run ./install.sh to install TensorFlow Lite, or" + echo " build it manually and ensure it's in one of these locations:" + echo " $TFLITE_HDR_SYSTEM1" + echo " $TFLITE_HDR_SYSTEM2" + echo " $TFLITE_HDR_SRC" + exit 1 +fi + +# 6b. Library checks - check system locations first +TFLITE_LIB_SYSTEM1="/usr/local/lib/libtensorflowlite.dylib" +TFLITE_LIB_SYSTEM2="/opt/homebrew/lib/libtensorflowlite.dylib" +TFLITE_LIB_SRC="$PROJECT_ROOT/src/tensorflow/bazel-bin/tensorflow/lite/libtensorflowlite.dylib" + +if [ -f "$TFLITE_LIB_SYSTEM1" ]; then + TFLITE_LIB="$TFLITE_LIB_SYSTEM1" + echo "Found TensorFlow Lite library in system location: $TFLITE_LIB" +elif [ -f "$TFLITE_LIB_SYSTEM2" ]; then + TFLITE_LIB="$TFLITE_LIB_SYSTEM2" + echo "Found TensorFlow Lite library in system location: $TFLITE_LIB" +elif [ -f "$TFLITE_LIB_SRC" ]; then + TFLITE_LIB="$TFLITE_LIB_SRC" + echo "Found TensorFlow Lite library in project directory: $TFLITE_LIB" +else + echo "Error: libtensorflowlite.dylib not found." + echo " Please run ./install.sh to install TensorFlow Lite, or" + echo " build it manually and ensure it's in one of these locations:" + echo " $TFLITE_LIB_SYSTEM1" + echo " $TFLITE_LIB_SYSTEM2" + echo " $TFLITE_LIB_SRC" + exit 1 +fi + +############################################################################### +# 7. Check for FlatBuffers headers (system locations first) +############################################################################### +FLAT_HDR_SYSTEM1="/usr/local/include/flatbuffers/flatbuffers.h" +FLAT_HDR_SYSTEM2="/opt/homebrew/include/flatbuffers/flatbuffers.h" +FLAT_HDR_BAZEL_BIN="$PROJECT_ROOT/src/tensorflow/bazel-bin/external/flatbuffers/include/flatbuffers/flatbuffers.h" +FLAT_HDR_BAZEL_OUT="$PROJECT_ROOT/src/tensorflow/bazel-out/darwin_arm64-opt/bin/external/flatbuffers/include/flatbuffers/flatbuffers.h" + +if [ -f "$FLAT_HDR_SYSTEM1" ]; then + FLAT_HDR="$FLAT_HDR_SYSTEM1" + echo "Found FlatBuffers headers in system location: $FLAT_HDR" +elif [ -f "$FLAT_HDR_SYSTEM2" ]; then + FLAT_HDR="$FLAT_HDR_SYSTEM2" + echo "Found FlatBuffers headers in system location: $FLAT_HDR" +elif [ -f "$FLAT_HDR_BAZEL_BIN" ]; then + FLAT_HDR="$FLAT_HDR_BAZEL_BIN" + echo "Found FlatBuffers headers in Bazel build: $FLAT_HDR" +elif [ -f "$FLAT_HDR_BAZEL_OUT" ]; then + FLAT_HDR="$FLAT_HDR_BAZEL_OUT" + echo "Found FlatBuffers headers in Bazel build: $FLAT_HDR" +else + echo "Error: FlatBuffers header not found (flatbuffers/flatbuffers.h)." + echo " Please run ./install.sh to install FlatBuffers, or" + echo " install it manually and ensure it's in one of these locations:" + echo " $FLAT_HDR_SYSTEM1" + echo " $FLAT_HDR_SYSTEM2" + echo " $FLAT_HDR_BAZEL_BIN" + echo " $FLAT_HDR_BAZEL_OUT" + exit 1 +fi + +############################################################################### +# 8. Create Symlinks for Versioned .dylib Files (if needed) ############################################################################### SYMLINK_DIR="/opt/homebrew/lib" @@ -98,21 +182,21 @@ if [ -d "$SYMLINK_DIR" ]; then pushd "$SYMLINK_DIR" >/dev/null || exit 1 - # Symlink for libimobiledevice (e.g. libimobiledevice-1.0.dylib → libimobiledevice.dylib) + # Symlink for libimobiledevice IMOBILE=$(ls libimobiledevice-*.dylib 2>/dev/null | head -n 1) if [[ -n "$IMOBILE" ]]; then echo "Found $IMOBILE. Creating symlink libimobiledevice.dylib → $IMOBILE" ln -sf "$IMOBILE" libimobiledevice.dylib fi - # Symlink for libplist (non-++), e.g. libplist-2.0.dylib → libplist.dylib + # Symlink for libplist PLIST=$(ls libplist-*.dylib 2>/dev/null | grep -v '++' | head -n 1) if [[ -n "$PLIST" ]]; then echo "Found $PLIST. Creating symlink libplist.dylib → $PLIST" ln -sf "$PLIST" libplist.dylib fi - # Symlink for libplist++ (C++ version), e.g. libplist++-2.0.dylib → libplist++.dylib + # Symlink for libplist++ PLISTPP=$(ls libplist++-*.dylib 2>/dev/null | head -n 1) if [[ -n "$PLISTPP" ]]; then echo "Found $PLISTPP. Creating symlink libplist++.dylib → $PLISTPP" @@ -123,9 +207,11 @@ if [ -d "$SYMLINK_DIR" ]; then fi ############################################################################### -# 7. Success +# 9. Success ############################################################################### echo "All checks passed for Apple Silicon macOS." -echo "Unversioned symlinks created (if needed)." -echo "You can now run 'make' or 'make all'." +echo "TensorFlow Lite include path: $TFLITE_HDR" +echo "TensorFlow Lite library path: $TFLITE_LIB" +echo "FlatBuffers header path: $FLAT_HDR" +echo "You can now run 'make' or proceed with CMake + build as usual." exit 0 diff --git a/include/nsfw_detector.h b/include/nsfw_detector.h index d8ed49d..8d8a579 100644 --- a/include/nsfw_detector.h +++ b/include/nsfw_detector.h @@ -5,12 +5,6 @@ const float DEFAULT_SKIN_THRESHOLD = 0.6f; -/** - * Loads an image from 'imagePath', converts to YCrCb, - * and checks the ratio of "skin" pixels. If ratio >= skinThreshold, - * returns true (flag as NSFW). Otherwise false. - */ -bool naiveNSFWCheck(const std::string& imagePath, - float skinThreshold = DEFAULT_SKIN_THRESHOLD); + #endif // NSFW_DETECTOR_H diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..d4f233c --- /dev/null +++ b/install.sh @@ -0,0 +1,207 @@ +#!/bin/bash +set -e + +# ------------------------------------------------------------ +# Helper Functions +# ------------------------------------------------------------ +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +brew_package_exists() { + brew list "$1" >/dev/null 2>&1 +} + +install_brew_package() { + local pkg="$1" + if ! brew_package_exists "$pkg"; then + echo "→ Installing Homebrew package: $pkg" + brew install "$pkg" + else + echo "→ Homebrew package already present: $pkg" + fi +} + +install_flatbuffers() { + local install_dir="$1" + echo "=== Installing FlatBuffers v24.3.25 from source into ${install_dir} ===" + local tmpdir + tmpdir=$(mktemp -d /tmp/flatbuffers_install.XXXXXX) + echo "→ Cloning FlatBuffers into temporary dir: $tmpdir" + git clone https://github.com/google/flatbuffers.git "$tmpdir/flatbuffers_src" + pushd "$tmpdir/flatbuffers_src" >/dev/null + git checkout v24.3.25 + + echo "→ Running CMake (FlatBuffers)..." + mkdir -p build && cd build + cmake -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="${install_dir}" \ + .. + echo "→ Building & installing FlatBuffers (requires sudo to write under ${install_dir})..." + sudo cmake --build . --target install + + popd >/dev/null + echo "→ Cleaning up FlatBuffers temp dir (using sudo because some build artifacts are root-owned)" + sudo rm -rf "$tmpdir" + echo "→ FlatBuffers v24.3.25 installed to ${install_dir}" +} + +# ------------------------------------------------------------ +# Main Installer +# ------------------------------------------------------------ +printf "\n=== iPurity/macOS Dependency Installer ===\n\n" + +# 1) Determine Homebrew prefix (Intel vs Apple Silicon) +if [[ "$(uname -m)" == "arm64" ]]; then + INSTALL_PREFIX="/opt/homebrew" +else + INSTALL_PREFIX="/usr/local" +fi +echo "→ Homebrew prefix detected: ${INSTALL_PREFIX}" + +# 2) Ensure Homebrew is installed +if ! command_exists brew; then + echo "→ Homebrew not found. Installing Homebrew..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +else + echo "→ Homebrew already present." +fi + +echo "→ Updating Homebrew ..." +brew update + +# 3) Install required packages via Homebrew +echo "→ Installing pkg-config, cmake, make, bazelisk, opencv, libimobiledevice, libplist" +install_brew_package pkg-config +install_brew_package cmake +install_brew_package make +install_brew_package bazelisk +install_brew_package opencv +install_brew_package libimobiledevice +install_brew_package libplist + +# 4) If Homebrew’s "bazel" formula exists, unlink it so it does not shadow bazelisk +if brew_package_exists bazel; then + echo "→ Homebrew 'bazel' is installed; unlinking to avoid conflicts with bazelisk" + brew unlink bazel || true +fi + +# 5) Create a symlink so that 'bazel' points to 'bazelisk' +BREWISK_BIN="$(brew --prefix bazelisk)/bin/bazelisk" +TARGET_BAZEL_SYMLINK="${INSTALL_PREFIX}/bin/bazel" + +echo "→ Ensuring that 'bazel' → 'bazelisk' symlink exists:" +echo " bazelisk path: ${BREWISK_BIN}" +echo " desired symlink: ${TARGET_BAZEL_SYMLINK}" +if [[ ! -f "$BREWISK_BIN" ]]; then + echo "Error: bazelisk binary not found at: ${BREWISK_BIN}" + echo "Make sure 'brew install bazelisk' succeeded." + exit 1 +fi + +# Make sure the directory exists +mkdir -p "$(dirname "$TARGET_BAZEL_SYMLINK")" + +# Create or overwrite the symlink: +ln -sf "$BREWISK_BIN" "$TARGET_BAZEL_SYMLINK" +echo "→ Created symlink: bazel → bazelisk" + +# 6) Verify pkg-config and OpenCV +echo "→ Verifying pkg-config + OpenCV ..." +if ! pkg-config --exists opencv4 && ! pkg-config --exists opencv; then + echo "Error: OpenCV not detected by pkg-config." + echo "Run: brew reinstall opencv" + exit 1 +fi +echo "→ pkg-config and OpenCV checks passed." + +# 7) Install FlatBuffers v24.3.25 from source +install_flatbuffers "${INSTALL_PREFIX}" + +# 8) Build and install TensorFlow Lite (non-interactive) +echo "=== Installing TensorFlow Lite under ${INSTALL_PREFIX} ===" +TMPDIR=$(mktemp -d /tmp/tflite_install.XXXXXX) +echo "→ Created temp dir: $TMPDIR" +git clone --depth 1 https://github.com/tensorflow/tensorflow.git "$TMPDIR/tensorflow" +pushd "$TMPDIR/tensorflow" >/dev/null + +echo "→ Running TensorFlow's ./configure (non-interactive) ..." +export TF_NEED_CUDA=0 +export TF_NEED_ROCM=0 +export TF_NEED_OPENCL_SYCL=0 +export TF_ENABLE_XLA=0 +export TF_NEED_TENSORRT=0 + +yes "" | ./configure 2>&1 | tee ../configure-tf.log + +echo "→ Building TensorFlow Lite via bazelisk ..." +bazel build -c opt //tensorflow/lite:libtensorflowlite.dylib 2>&1 | tee ../tflite-build.log + +echo "→ Copying TensorFlow Lite artifacts into ${INSTALL_PREFIX} ..." +mkdir -p "${INSTALL_PREFIX}/include/tensorflow/lite" +mkdir -p "${INSTALL_PREFIX}/lib" + +sudo cp "bazel-bin/tensorflow/lite/libtensorflowlite.dylib" "${INSTALL_PREFIX}/lib/" +sudo cp -r tensorflow/lite/*.h "${INSTALL_PREFIX}/include/tensorflow/lite/" + +# —––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––— +# Copy over the missing MLIR “lite” headers (allocation.h, etc.) from +# the TensorFlow source into INSTALL_PREFIX. +# These live under tensorflow/compiler/mlir/lite/ in the TF repo. +echo "→ Copying MLIR-lit e headers (tensorflow/compiler/mlir/lite/…) into ${INSTALL_PREFIX}/include" +sudo mkdir -p "${INSTALL_PREFIX}/include/tensorflow/compiler/mlir/lite" +sudo cp -r tensorflow/compiler/mlir/lite/* "${INSTALL_PREFIX}/include/tensorflow/compiler/mlir/lite/" + +echo "→ Cleaning up TensorFlow temp dir" +popd >/dev/null +sudo rm -rf "$TMPDIR" + +# 9) Create unversioned symlinks (libimobiledevice & libplist) +echo "→ Checking for versioned libraries in ${INSTALL_PREFIX}/lib ..." +pushd "${INSTALL_PREFIX}/lib" >/dev/null || exit 1 + +IMOBILE_VER=$(ls libimobiledevice-*.dylib 2>/dev/null | head -n1) +if [[ -n "$IMOBILE_VER" ]]; then + echo " Found $IMOBILE_VER → symlinking to libimobiledevice.dylib" + sudo ln -sf "$IMOBILE_VER" libimobiledevice.dylib +fi + +PLIST_VER=$(ls libplist-*.dylib 2>/dev/null | grep -v '++' | head -n1) +if [[ -n "$PLIST_VER" ]]; then + echo " Found $PLIST_VER → symlinking to libplist.dylib" + sudo ln -sf "$PLIST_VER" libplist.dylib +fi + +PLISTPP_VER=$(ls libplist++-*.dylib 2>/dev/null | head -n1) +if [[ -n "$PLISTPP_VER" ]]; then + echo " Found $PLISTPP_VER → symlinking to libplist++.dylib" + sudo ln -sf "$PLISTPP_VER" libplist++.dylib +fi + +popd >/dev/null + +# ------------------------------------------------------------ +# Final summary +# ------------------------------------------------------------ +echo "" +echo "=== Installation Summary ===" +echo " • pkg-config installed" +echo " • cmake installed" +echo " • make installed" +echo " • bazelisk installed" +echo " • bazel → bazelisk symlink created" +echo " • opencv installed" +echo " • libimobiledevice installed" +echo " • libplist installed" +echo " • FlatBuffers v24.3.25 installed at ${INSTALL_PREFIX}" +echo " • TensorFlow Lite installed under ${INSTALL_PREFIX}" +echo " • MLIR headers (tensorflow/compiler/mlir/lite/…) copied into ${INSTALL_PREFIX}/include" +echo " • Symlinks created for libimobiledevice & libplist in ${INSTALL_PREFIX}/lib" +echo "" +echo "Done! Now you can run:" +echo " cd " +echo " mkdir -p build && cd build" +echo " cmake .." +echo " make" +echo "" +exit 0 diff --git a/models/nsfw_model.tflite b/models/nsfw_model.tflite new file mode 100644 index 0000000..8ce8776 Binary files /dev/null and b/models/nsfw_model.tflite differ diff --git a/src/main.cpp b/src/main.cpp index c32bda6..a744a04 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,7 +22,14 @@ int main(int argc, char* argv[]) { std::cout << "----------------------" << std::endl; } - float threshold = DEFAULT_SKIN_THRESHOLD; + // Initialize NSFW detector + NSFWDetector detector; + if (!detector.initialize("models/nsfw_model.tflite")) { + std::cerr << "Failed to initialize NSFW detector" << std::endl; + return 1; + } + + float threshold = 0.5f; // Default threshold for NSFW detection if (argc > 1) { threshold = std::stof(argv[1]); if (threshold < 0.0 || threshold > 1.0) { @@ -52,7 +59,7 @@ int main(int argc, char* argv[]) { } ScanStats stats; - scan_directory(&clientPool, "/DCIM", stats, threshold); + scan_directory(&clientPool, "/DCIM", stats, threshold, &detector); // Wait for all asynchronous tasks to complete. for (auto& fut : futures) { diff --git a/src/nsfw_detector.cpp b/src/nsfw_detector.cpp index 6b16669..66d9584 100644 --- a/src/nsfw_detector.cpp +++ b/src/nsfw_detector.cpp @@ -1,55 +1,134 @@ #include "nsfw_detector.h" - -#include #include +#include +#include +#include #include -/** - * Check if a pixel (in YCrCb) is within a naive "skin" range. - * This function assumes the pixel is in [Y, Cr, Cb] order. - */ -static bool isSkinPixel(const cv::Vec3b& ycrcb) { - uchar Cr = ycrcb[1]; - uchar Cb = ycrcb[2]; - - // Example naive thresholds for skin detection: - // 140 < Cr < 175 - // 100 < Cb < 135 - if (Cr >= 140 && Cr <= 175 && Cb >= 100 && Cb <= 135) { - return true; - } - return false; +namespace fs = std::filesystem; + +NSFWDetector::NSFWDetector() : isInitialized(false) {} + +NSFWDetector::~NSFWDetector() { + // Cleanup resources + interpreter.reset(); + model.reset(); +} + +std::string find_model_path() { + // Try Homebrew's share directory first + std::string homebrew_share = "/usr/local/share/ipurity/nsfw_model.tflite"; + if (fs::exists(homebrew_share)) { + return homebrew_share; + } + + // Try Apple Silicon Homebrew location + homebrew_share = "/opt/homebrew/share/ipurity/nsfw_model.tflite"; + if (fs::exists(homebrew_share)) { + return homebrew_share; + } + + // Try user's home directory + const char* home = getenv("HOME"); + if (home) { + std::string user_model = std::string(home) + "/ipurity/models/nsfw_model.tflite"; + if (fs::exists(user_model)) { + return user_model; + } + } + + // Fall back to local models directory for development + return "models/nsfw_model.tflite"; } -bool naiveNSFWCheck(const std::string& imagePath, float skinThreshold) { - // 1. Load the image in BGR format - cv::Mat imgBGR = cv::imread(imagePath, cv::IMREAD_COLOR); - if (imgBGR.empty()) { - std::cerr << "Could not load image: " << imagePath << std::endl; +bool NSFWDetector::initialize(const std::string& model_path) { + std::string actual_path = model_path.empty() ? find_model_path() : model_path; + + if (!fs::exists(actual_path)) { + std::cerr << "Error: Model file not found at " << actual_path << std::endl; + std::cerr << "Please ensure the model is installed in one of these locations:" << std::endl; + std::cerr << " - /usr/local/share/ipurity/nsfw_model.tflite" << std::endl; + std::cerr << " - /opt/homebrew/share/ipurity/nsfw_model.tflite" << std::endl; + std::cerr << " - ~/ipurity/models/nsfw_model.tflite" << std::endl; + std::cerr << " - ./models/nsfw_model.tflite" << std::endl; + return false; + } + + // Load the TensorFlow Lite model + model = tflite::FlatBufferModel::BuildFromFile(actual_path.c_str()); + if (!model) { + std::cerr << "Failed to load model: " << actual_path << std::endl; + return false; + } + + // Build the interpreter + tflite::ops::builtin::BuiltinOpResolver resolver; + tflite::InterpreterBuilder builder(*model, resolver); + builder(&interpreter); + + if (!interpreter) { + std::cerr << "Failed to build interpreter" << std::endl; return false; } - // 2. Convert to YCrCb - cv::Mat imgYCrCb; - cv::cvtColor(imgBGR, imgYCrCb, cv::COLOR_BGR2YCrCb); + // Allocate tensors + if (interpreter->AllocateTensors() != kTfLiteOk) { + std::cerr << "Failed to allocate tensors" << std::endl; + return false; + } - // 3. Count how many pixels fall in "skin" range - long totalPixels = static_cast(imgYCrCb.rows) * imgYCrCb.cols; - long skinCount = 0; + isInitialized = true; + return true; +} - for (int y = 0; y < imgYCrCb.rows; y++) { - const cv::Vec3b* rowPtr = imgYCrCb.ptr(y); - for (int x = 0; x < imgYCrCb.cols; x++) { - if (isSkinPixel(rowPtr[x])) { - skinCount++; - } +bool NSFWDetector::preprocessImage(const cv::Mat& input, float* output) { + cv::Mat resized; + cv::resize(input, resized, cv::Size(INPUT_SIZE, INPUT_SIZE)); + + // Convert to RGB and normalize + cv::Mat rgb; + cv::cvtColor(resized, rgb, cv::COLOR_BGR2RGB); + rgb.convertTo(rgb, CV_32FC3, 1.0/255.0); + + // Copy data to input tensor + float* input_tensor = interpreter->typed_input_tensor(0); + for (int y = 0; y < INPUT_SIZE; y++) { + for (int x = 0; x < INPUT_SIZE; x++) { + cv::Vec3f pixel = rgb.at(y, x); + *input_tensor++ = pixel[0]; + *input_tensor++ = pixel[1]; + *input_tensor++ = pixel[2]; } } + + return true; +} + +float NSFWDetector::detectNSFW(const cv::Mat& image) { + if (!isInitialized) { + std::cerr << "Detector not initialized" << std::endl; + return -1.0f; + } + + if (image.empty()) { + std::cerr << "Input image is empty" << std::endl; + return -1.0f; + } - // 4. Compute ratio of skin pixels - float ratio = - static_cast(skinCount) / static_cast(totalPixels); + // Preprocess image + float* input = interpreter->typed_input_tensor(0); + if (!preprocessImage(image, input)) { + return -1.0f; + } + + // Run inference + if (interpreter->Invoke() != kTfLiteOk) { + std::cerr << "Failed to run inference" << std::endl; + return -1.0f; + } - // 5. Return true if ratio >= threshold - return (ratio >= skinThreshold); + // Get output + float* output = interpreter->typed_output_tensor(0); + return output[1]; // Assuming output[1] is the NSFW probability } + diff --git a/src/nsfw_detector.h b/src/nsfw_detector.h new file mode 100644 index 0000000..381434f --- /dev/null +++ b/src/nsfw_detector.h @@ -0,0 +1,29 @@ +#ifndef NSFW_DETECTOR_H +#define NSFW_DETECTOR_H + +#include +#include +#include +#include +#include +#include + +class NSFWDetector { +public: + NSFWDetector(); + ~NSFWDetector(); + + bool initialize(const std::string& modelPath); + float detectNSFW(const cv::Mat& image); + +private: + std::unique_ptr model; + std::unique_ptr interpreter; + bool isInitialized; + + bool preprocessImage(const cv::Mat& input, float* output); + static constexpr int INPUT_SIZE = 224; + static constexpr float NSFW_THRESHOLD = 0.5f; +}; + +#endif // NSFW_DETECTOR_H \ No newline at end of file diff --git a/src/scanner.cpp b/src/scanner.cpp index f4ecebb..ca111be 100644 --- a/src/scanner.cpp +++ b/src/scanner.cpp @@ -4,10 +4,12 @@ #include #include #include +#include #include "scanner.h" #include "afc_helpers.h" #include "nsfw_detector.h" +#include // External global objects (could also be placed in a dedicated logging module) extern std::mutex coutMutex; @@ -19,45 +21,27 @@ const char* COLOR_GREEN = "\033[32m"; const char* COLOR_RED = "\033[31m"; const char* COLOR_RESET = "\033[0m"; -bool download_file(afc_client_t afc, const char* remotePath, - const char* localPath) { +bool download_file_to_buffer(afc_client_t afc, const char* remotePath, std::vector& buffer) { uint64_t fileRef = 0; - if (afc_file_open(afc, remotePath, AFC_FOPEN_RDONLY, &fileRef) != - AFC_E_SUCCESS) { + if (afc_file_open(afc, remotePath, AFC_FOPEN_RDONLY, &fileRef) != AFC_E_SUCCESS) { std::lock_guard lock(coutMutex); std::cerr << "Failed to open remote file: " << remotePath << std::endl; return false; } - FILE* outFile = std::fopen(localPath, "wb"); - if (!outFile) { - std::lock_guard lock(coutMutex); - std::cerr << "Failed to open local file: " << localPath << std::endl; - afc_file_close(afc, fileRef); - return false; - } const size_t BUF_SIZE = 4096; - char buffer[BUF_SIZE]; + std::vector temp(BUF_SIZE); uint32_t bytesRead = 0; while (true) { - afc_error_t readErr = - afc_file_read(afc, fileRef, buffer, BUF_SIZE, &bytesRead); + afc_error_t readErr = afc_file_read(afc, fileRef, temp.data(), BUF_SIZE, &bytesRead); if (readErr != AFC_E_SUCCESS || bytesRead == 0) break; - size_t bytesWritten = std::fwrite(buffer, 1, bytesRead, outFile); - if (bytesWritten < bytesRead) { - std::lock_guard lock(coutMutex); - std::cerr << "Failed to write all data to local file." << std::endl; - std::fclose(outFile); - afc_file_close(afc, fileRef); - return false; - } + buffer.insert(buffer.end(), temp.begin(), temp.begin() + bytesRead); } afc_file_close(afc, fileRef); - std::fclose(outFile); - return true; + return !buffer.empty(); } void process_image_file(AfcClientPool* pool, const char* fullPath, - ScanStats& stats, float threshold) { + ScanStats& stats, float threshold, NSFWDetector* detector) { std::string filePathStr(fullPath); if (!is_image_file(filePathStr)) return; @@ -70,36 +54,41 @@ void process_image_file(AfcClientPool* pool, const char* fullPath, std::lock_guard lock(coutMutex); std::cout << "Found image file: " << fullPath << std::endl; } - std::string localFile = - "/tmp/ios_" + filePathStr.substr(filePathStr.find_last_of("/") + 1); afc_client_t client = pool->acquire(); - if (download_file(client, fullPath, localFile.c_str())) { - bool isNSFW = naiveNSFWCheck(localFile, threshold); - std::string message; - { - std::lock_guard lock(statsMutex); - if (isNSFW) { - stats.nsfwFiles++; - stats.nsfwFilesList.push_back(localFile); - message = std::string(COLOR_RED) + "[NSFW DETECTED] " + - localFile + COLOR_RESET; - } else { - stats.safeFiles++; - message = std::string(COLOR_GREEN) + "[SAFE] " + localFile + - COLOR_RESET; + std::vector buffer; + if (download_file_to_buffer(client, fullPath, buffer)) { + cv::Mat image = cv::imdecode(buffer, cv::IMREAD_COLOR); + if (!image.empty()) { + float nsfwProbability = detector->detectNSFW(image); + bool isNSFW = nsfwProbability >= threshold; + std::string message; + { + std::lock_guard lock(statsMutex); + if (isNSFW) { + stats.nsfwFiles++; + stats.nsfwFilesList.push_back(filePathStr); + message = std::string(COLOR_RED) + "[NSFW DETECTED] " + + filePathStr + " (Probability: " + + std::to_string(nsfwProbability) + ")" + COLOR_RESET; + } else { + stats.safeFiles++; + message = std::string(COLOR_GREEN) + "[SAFE] " + filePathStr + + " (Probability: " + std::to_string(nsfwProbability) + + ")" + COLOR_RESET; + } + } + { + std::lock_guard lock(coutMutex); + std::cout << message << std::endl; } - } - { - std::lock_guard lock(coutMutex); - std::cout << message << std::endl; } } pool->release(client); } void scan_directory(AfcClientPool* pool, const char* path, ScanStats& stats, - float threshold) { + float threshold, NSFWDetector* detector) { afc_client_t client = pool->acquire(); char** dirList = nullptr; afc_error_t err = afc_read_directory(client, path, &dirList); @@ -123,11 +112,11 @@ void scan_directory(AfcClientPool* pool, const char* path, ScanStats& stats, pool->release(client); if (isDir) { - scan_directory(pool, fullPath, stats, threshold); + scan_directory(pool, fullPath, stats, threshold, detector); } else { futures.push_back(std::async(std::launch::async, process_image_file, pool, fullPath, std::ref(stats), - threshold)); + threshold, detector)); } free(fullPath); } diff --git a/src/scanner.h b/src/scanner.h new file mode 100644 index 0000000..e987424 --- /dev/null +++ b/src/scanner.h @@ -0,0 +1,18 @@ +#ifndef SCANNER_H +#define SCANNER_H + +#include +#include "afc_client_pool.h" +#include "nsfw_detector.h" + +struct ScanStats { + int totalFiles = 0; + int nsfwFiles = 0; + int safeFiles = 0; + std::vector nsfwFilesList; +}; + +void scan_directory(AfcClientPool* pool, const char* path, ScanStats& stats, + float threshold, NSFWDetector* detector); + +#endif // SCANNER_H \ No newline at end of file