diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml index 30f9467..296e064 100644 --- a/.github/workflows/android-tests.yml +++ b/.github/workflows/android-tests.yml @@ -5,6 +5,7 @@ on: branches: [ main ] pull_request: branches: [ main ] + workflow_call: jobs: android-tests: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b878cf9 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Publish to GitHub Packages + +on: + workflow_dispatch: + +jobs: + run-tests: + name: Run Test Suite + uses: ./.github/workflows/android-tests.yml + secrets: inherit + + publish: + needs: run-tests + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build libraries + run: ./gradlew build + + - name: Publish to GitHub Packages + env: + USERNAME: ${{ github.actor }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew publish diff --git a/README.md b/README.md index 3bbc50e..089e27a 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,17 @@ detection and [EdgeFace](https://github.com/otroshi/edgeface) for embedding (tem 2. Embedding Creation: This module is used to generate a 512-float vector representation of face images. 3. Matching and Identification: Used for verification and identification purposes. +## SimQ Library + +**SimQ** is a standalone Android library for comprehensive face quality assessment. It can be used independently or as part of SimFace for enhanced quality evaluation. SimQ provides a quality score from 0.0 to 1.0, with customizable weights for each metric and configurable thresholds. + +**📚 [View Full SimQ Documentation](simq/README.md)** for installation, usage examples, and advanced configuration options. + ## Include the library into the project. ### Option 1 (Recommended) -1. Add the repository to your ```settings.gradle.kts``` under ```dependencyResolutionManagement``` under ```respositories```: +1. Add the repository to your `settings.gradle.kts` under `dependencyResolutionManagement` under `respositories`: ```kotlin maven { @@ -25,10 +31,10 @@ maven { } ``` -2. Import the dependencies in ```build.gradle.kts```: +2. Import the dependencies in `build.gradle.kts`: ```kotlin -implementation("com.simprints.biometrics:simface:2025.2.0") +implementation("com.simprints.biometrics:simface:2025.4.0") ``` ## Implement the functionality. @@ -93,20 +99,20 @@ simFace.getFaceDetectionProcessor().detectFace(faceImage, onSuccess = { faces -> ### Face Detection and Embedding We first initialize the library. Then we can use the -```simFace.detectFace``` method to detect faces in images and evaluate their quality. +`simFace.detectFace` method to detect faces in images and evaluate their quality. We can repeat this process multiple times until a sufficiently good face image is selected. -Afterwards, we can use the ```simFace.getEmbedding``` method to obtain a vector template +Afterwards, we can use the `simFace.getEmbedding` method to obtain a vector template from the selected image. The embedding is represented by a 512 float array. ### Verification and Identification The same steps are taken to initialize the library. -Then, the matching of two templates is carried out using the ```simFace.verificationScore``` method, +Then, the matching of two templates is carried out using the `simFace.verificationScore` method, which returns a score in the [0, 1] range, being 1 a perfect match. -Identification can be carried using the ```simFace.identificationScore``` method which returns a mapping of the +Identification can be carried using the `simFace.identificationScore` method which returns a mapping of the referenceVectors to the the score with respect to the probe. Both methods use the cosine similarity between vectors as a measure of the score. @@ -114,13 +120,10 @@ Both methods use the cosine similarity between vectors as a measure of the score ## System Requirements The library works with a minimum version of Android 6.0 (API Level 23). It has been tested and runs -smoothly on *Samsung Galaxy A03 Core* which has the following specifications: +smoothly on _Samsung Galaxy A03 Core_ which has the following specifications: - Android 11 - 1.6GHz Octa-core - 2GB RAM - 8MP f/2.0 Camera - 32GB Storage - - - diff --git a/build.gradle.kts b/build.gradle.kts index ca425ae..398396b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { val projectGroupId = "com.simprints.biometrics" val projectArtifactId = "simface" -val projectVersion = "2025.3.2" +val projectVersion = "2025.4.0" group = projectGroupId version = projectVersion diff --git a/simq/README.md b/simq/README.md new file mode 100644 index 0000000..16ae2d3 --- /dev/null +++ b/simq/README.md @@ -0,0 +1,120 @@ +# SimQ - Face Quality Assessment + +SimQ is an Android library for assessing the quality of face images. It analyzes multiple quality metrics to provide a comprehensive quality score between 0.0 and 1.0. + +## Features + +SimQ evaluates face images based on four key metrics: + +- **Alignment**: Evaluates face pose angles (pitch, yaw, roll) and eye state +- **Blur**: Measures image sharpness using Laplacian variance +- **Brightness**: Assesses image luminance levels +- **Contrast**: Evaluates pixel intensity variation + +## Installation + +Add the dependency to your `build.gradle.kts`: + +```kotlin +implementation("com.simprints.biometrics:simq:2025.4.0") +``` + +## Basic Usage + +```kotlin +// Initialize SimQ with default parameters +val simQ = SimQ() + +// Calculate quality score for a face image +val qualityScore = simQ.calculateFaceQuality( + bitmap = faceBitmap, + pitch = 5.0, + yaw = -3.0, + roll = 0.0 +) + +// Quality score ranges from 0.0 (poor) to 1.0 (excellent) +if (qualityScore >= 0.6) { + // Image quality is sufficient +} +``` + +## Advanced Usage + +### Custom Weights + +Customize how much each metric contributes to the final score: + +```kotlin +val customWeights = QualityWeights( + alignment = 0.25, + blur = 0.35, + brightness = 0.25, + contrast = 0.10, + eyeOpenness = 0.05 +) + +val simQ = SimQ(faceWeights = customWeights) +``` + +### Custom Parameters + +Adjust the thresholds for each quality metric: + +```kotlin +val customParameters = QualityParameters( + maxAlignmentAngle = 15.0, + minBlur = 60_000.0, + maxBlur = 120_000.0, + minBrightness = 40.0, + optimalBrightnessLow = 90.0, + optimalBrightnessHigh = 140.0, + maxBrightness = 180.0, + minContrast = 35.0, + maxContrast = 50.0 +) + +val simQ = SimQ(faceParameters = customParameters) +``` + +## Parameters Reference + +### calculateFaceQuality() + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `bitmap` | `Bitmap` | Required | The cropped face image | +| `pitch` | `Double` | `0.0` | Face pitch angle (head nod) in degrees | +| `yaw` | `Double` | `0.0` | Face yaw angle (head rotation) in degrees | +| `roll` | `Double` | `0.0` | Face roll angle (head tilt) in degrees | +| `leftEyeOpenness` | `Double?` | `null` | Left eye openness probability (0.0-1.0) | +| `rightEyeOpenness` | `Double?` | `null` | Right eye openness probability (0.0-1.0) | +| `centerCrop` | `Float` | `0.5f` | Fraction of image to analyze (0.0-1.0) | +| `horizontalDisplacement` | `Float` | `0.0f` | Horizontal shift for center crop (-1.0 to 1.0) | +| `verticalDisplacement` | `Float` | `0.0f` | Vertical shift for center crop (-1.0 to 1.0) | + +### QualityWeights (Default Values) + +| Weight | Default | Description | +|--------|---------|-------------| +| `alignment` | `0.28` | Face pose contribution | +| `blur` | `0.30` | Sharpness contribution | +| `brightness` | `0.30` | Luminance contribution | +| `contrast` | `0.10` | Contrast contribution | +| `eyeOpenness` | `0.02` | Eye state contribution | + +### QualityParameters (Default Values) + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `maxAlignmentAngle` | `20.0` | Maximum combined angle deviation | +| `maxIndividualAngle` | `25.0` | Maximum single angle deviation | +| `minBlur` | `50,000.0` | Minimum acceptable Laplacian variance | +| `maxBlur` | `100,000.0` | Optimal Laplacian variance | +| `minBrightness` | `30.0` | Minimum acceptable brightness (0-255) | +| `optimalBrightnessLow` | `80.0` | Lower bound of optimal brightness | +| `optimalBrightnessHigh` | `150.0` | Upper bound of optimal brightness | +| `maxBrightness` | `190.0` | Maximum acceptable brightness | +| `brightnessSteepness` | `0.3` | Brightness scoring curve steepness | +| `minContrast` | `30.0` | Minimum acceptable contrast (std dev) | +| `maxContrast` | `47.0` | Optimal contrast (std dev) | diff --git a/simq/build.gradle.kts b/simq/build.gradle.kts index 942f493..deac50c 100644 --- a/simq/build.gradle.kts +++ b/simq/build.gradle.kts @@ -1,8 +1,16 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.jetbrains.kotlin.android) + `maven-publish` } +val projectGroupId = "com.simprints.biometrics" +val projectArtifactId = "simq" +val projectVersion = "2025.4.0" + +group = projectGroupId +version = projectVersion + android { namespace = "com.simprints.simq" compileSdk = 36 @@ -50,3 +58,36 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.kotlinx.coroutines.test) } + +publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/simprints/Biometrics-SimFace") + credentials { + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN") + } + } + } + publications { + create("ReleaseAar") { + groupId = projectGroupId + artifactId = projectArtifactId + version = projectVersion + afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) } + + pom.withXml { + val dependenciesNode = asNode().appendNode("dependencies") + + configurations.getByName("api").dependencies.map { dependency -> + dependenciesNode.appendNode("dependency").also { + it.appendNode("groupId", dependency.group) + it.appendNode("artifactId", dependency.name) + it.appendNode("version", dependency.version) + } + } + } + } + } +}