| import gradio as gr |
| import os |
| import requests |
| from PIL import Image |
| import json |
|
|
| |
| API_BASE_URL = os.getenv("API_BASE_URL") |
|
|
| def face_compare(frame1, frame2, request: gr.Request = None): |
| """Face comparison with enhanced result display""" |
| try: |
| url = f"{API_BASE_URL}" |
| |
| |
| files = {} |
| if frame1: |
| files['image1'] = open(frame1, 'rb') |
| if frame2: |
| files['image2'] = open(frame2, 'rb') |
| |
| if not files: |
| return "<div class='error-message'>Please upload both images</div>" |
| |
| |
| response = requests.post(url=url, files=files) |
| result = response.json() |
| |
| |
| for file in files.values(): |
| file.close() |
| |
| |
| return format_face_comparison_result(result, frame1, frame2) |
| |
| except Exception as e: |
| return f"<div class='error-message'>Error processing request: {str(e)}</div>" |
|
|
| def format_face_comparison_result(result, img1_path, img2_path): |
| """Format face comparison results with professional styling""" |
| |
| detections = result.get("detections", []) |
| matches = result.get("match", []) |
| |
| |
| html = "<div class='result-content'>" |
| |
| |
| if detections: |
| for i, detection in enumerate(detections): |
| face_image = detection.get("face", "") |
| first_face_index = detection.get("firstFaceIndex") |
| second_face_index = detection.get("secondFaceIndex") |
| |
| |
| if matches: |
| html += """ |
| <div class="matching-section"> |
| <div class="matches-table"> |
| <table> |
| <thead> |
| <tr> |
| <th>First Image Face</th> |
| <th>Second Image Face</th> |
| <th>Similarity Score</th> |
| <th>Result</th> |
| </tr> |
| </thead> |
| <tbody> |
| """ |
| |
| |
| match_groups = {} |
| for match in matches: |
| first_face_index = match.get("firstFaceIndex", "N/A") |
| if first_face_index not in match_groups: |
| match_groups[first_face_index] = [] |
| match_groups[first_face_index].append(match) |
| |
| row_number = 1 |
| for first_face_index in sorted(match_groups.keys()): |
| for match in match_groups[first_face_index]: |
| first_face_index = match.get("firstFaceIndex", "N/A") |
| second_face_index = match.get("secondFaceIndex", "N/A") |
| similarity = match.get("similarity", 0) |
| |
| |
| first_face_img = "" |
| second_face_img = "" |
| |
| for detection in detections: |
| if detection.get("firstFaceIndex") == first_face_index: |
| first_face_img = detection.get("face", "") |
| if detection.get("secondFaceIndex") == second_face_index: |
| second_face_img = detection.get("face", "") |
| |
| |
| if similarity >= 0.6: |
| result_text = "same person" |
| result_class = "result-same" |
| else: |
| result_text = "different person" |
| result_class = "result-different" |
| |
| first_face_display = f"<img src='data:image/png;base64,{first_face_img}' class='table-face-thumbnail' />" if first_face_img else f"Face {first_face_index}" |
| second_face_display = f"<img src='data:image/png;base64,{second_face_img}' class='table-face-thumbnail' />" if second_face_img else f"Face {second_face_index}" |
| |
| html += f""" |
| <tr> |
| <td class="face-cell"> |
| <div class="face-display"> |
| {first_face_display} |
| <div class="face-label">Face {first_face_index}</div> |
| </div> |
| </td> |
| <td class="face-cell"> |
| <div class="face-display"> |
| {second_face_display} |
| <div class="face-label">Face {second_face_index}</div> |
| </div> |
| </td> |
| <td class="similarity-score">{similarity:.4f}</td> |
| <td><span class="result-text {result_class}">{result_text}</span></td> |
| </tr> |
| """ |
| row_number += 1 |
| |
| html += """ |
| </tbody> |
| </table> |
| </div> |
| </div> |
| """ |
| else: |
| html += "<div class='no-results'>No face matches found.</div>" |
| |
| html += "</div>" |
| return html |
|
|
| def get_custom_css(): |
| """Return simplified CSS styling that works for both light and dark themes""" |
| return """ |
| footer {visibility: hidden} |
| |
| /* Full width container */ |
| .gradio-container { |
| max-width: 95% !important; |
| width: 95% !important; |
| margin: 0 auto !important; |
| font-size: 16px !important; |
| } |
| |
| /* Center everything */ |
| .container { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| width: 100%; |
| } |
| |
| /* Header styling - logo and text in same line */ |
| .company-header { |
| background: var(--background-fill-primary); |
| padding: 25px; |
| margin-bottom: 25px; |
| text-align: center; |
| width: 100%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 25px; |
| flex-wrap: wrap; |
| } |
| |
| .header-logo { |
| flex-shrink: 0; |
| } |
| |
| .header-logo img { |
| width: 140px; |
| height: auto; |
| } |
| |
| .header-text { |
| text-align: center; |
| } |
| |
| .header-text h1 { |
| font-size: 2.4em !important; |
| margin-bottom: 12px; |
| font-weight: 700; |
| margin-top: 0; |
| color: var(--body-text-color); |
| } |
| |
| .header-text p { |
| font-size: 1.3em !important; |
| margin-bottom: 0; |
| color: var(--body-text-color); |
| opacity: 0.8; |
| } |
| |
| /* About section layout */ |
| .about-section { |
| width: 100%; |
| margin-bottom: 25px; |
| } |
| |
| .about-content { |
| display: flex; |
| gap: 25px; |
| width: 100%; |
| } |
| |
| .features-section { |
| flex: 0.6; |
| padding: 25px; |
| background: var(--background-fill-secondary); |
| border-radius: 10px; |
| font-size: 1.05em !important; |
| } |
| |
| .demo-items-section { |
| flex: 0.4; |
| padding: 10px; |
| background: var(--background-fill-secondary); |
| border-radius: 10px; |
| } |
| |
| .demo-items-grid { |
| display: grid; |
| grid-template-columns: repeat(2, 1fr); |
| gap: 10px; |
| } |
| |
| .demo-item { |
| background: var(--background-fill-primary); |
| padding: 5px; |
| border-radius: 8px; |
| text-align: center; |
| transition: background-color 0.2s ease; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| text-decoration: none; |
| color: var(--body-text-color); |
| font-size: 1em !important; |
| } |
| |
| .demo-item:hover { |
| background: var(--button-secondary-background-fill-hover); |
| } |
| |
| .demo-item img { |
| height: 40px; |
| margin-bottom: 5px; |
| } |
| |
| .demo-item span { |
| font-size: 0.95em !important; |
| font-weight: 600; |
| } |
| |
| /* Main content layout */ |
| .main-content-row { |
| display: flex; |
| gap: 25px; |
| width: 100%; |
| margin-bottom: 25px; |
| } |
| |
| .upload-section { |
| flex: 2; |
| display: flex; |
| flex-direction: column; |
| gap: 20px; |
| } |
| |
| .result-section { |
| flex: 1.2; |
| } |
| |
| .upload-images-row { |
| display: flex; |
| gap: 20px; |
| width: 100%; |
| } |
| |
| .upload-image-col { |
| flex: 1; |
| } |
| |
| /* Button styling */ |
| .button-primary { |
| background: var(--button-primary-background-fill) !important; |
| border: none !important; |
| padding: 4px 8px !important; |
| font-size: 1.2em !important; |
| font-weight: 600 !important; |
| color: var(--button-primary-text-color) !important; |
| border-radius: 8px !important; |
| cursor: pointer !important; |
| transition: background-color 0.2s ease !important; |
| width: 100% !important; |
| } |
| |
| .button-primary:hover { |
| background: var(--button-primary-background-fill-hover) !important; |
| } |
| |
| /* Result container styling */ |
| .result-container { |
| background: var(--background-fill-primary); |
| padding: 8px; |
| border-radius: 10px; |
| margin-top: 0; |
| width: 100%; |
| text-align: center; |
| height: fit-content; |
| } |
| |
| .result-content { |
| width: 100%; |
| } |
| |
| /* Detection cards */ |
| .detections-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); |
| gap: 15px; |
| margin-top: 20px; |
| justify-content: center; |
| } |
| |
| .detection-card { |
| background: var(--background-fill-secondary); |
| padding: 15px; |
| border-radius: 8px; |
| text-align: center; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| } |
| |
| .face-thumbnail { |
| width: 60px; |
| height: 60px; |
| border-radius: 50%; |
| object-fit: cover; |
| margin-bottom: 10px; |
| } |
| |
| .no-face { |
| width: 60px; |
| height: 60px; |
| background: var(--background-fill-primary); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| margin: 0 auto 10px; |
| color: var(--body-text-color); |
| font-size: 0.8em; |
| opacity: 0.7; |
| } |
| |
| .face-source { |
| font-size: 0.9em !important; |
| color: var(--body-text-color); |
| opacity: 0.8; |
| margin-top: 8px; |
| } |
| |
| /* Matching table - NEW STYLING */ |
| .matches-table { |
| display: flex; |
| justify-content: center; |
| width: 100%; |
| overflow-x: auto; |
| } |
| |
| .matches-table table { |
| width: 100%; |
| border-collapse: collapse; |
| margin-top: 8px; |
| font-size: 1em !important; |
| min-width: 550px; |
| } |
| |
| .matches-table th { |
| background: var(--background-fill-secondary); |
| color: var(--body-text-color); |
| padding: 12px 8px !important; |
| text-align: center; |
| font-size: 1em !important; |
| font-weight: 700; |
| border-bottom: 2px solid var(--border-color-primary); |
| } |
| |
| .matches-table td { |
| padding: 10px 8px !important; |
| border-bottom: 1px solid var(--border-color-primary); |
| text-align: center; |
| font-size: 0.95em !important; |
| color: var(--body-text-color); |
| } |
| |
| .row-number { |
| font-weight: 600; |
| color: var(--body-text-color); |
| } |
| |
| .face-cell { |
| vertical-align: middle; |
| } |
| |
| .face-display { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 5px; |
| } |
| |
| .table-face-thumbnail { |
| width: 70px; |
| height: 70px; |
| border-radius: 50%; |
| object-fit: cover; |
| border: 2px solid var(--border-color-primary); |
| } |
| |
| .face-label { |
| font-size: 0.9em !important; |
| color: var(--body-text-color); |
| opacity: 1; |
| font-weight: 600; |
| } |
| |
| .similarity-score { |
| font-weight: 700; |
| color: var(--body-text-color); |
| font-size: 1.05em !important; |
| } |
| |
| .result-text { |
| padding: 8px 12px !important; |
| border-radius: 12px; |
| font-size: 1.1em !important; |
| font-weight: 700; |
| text-transform: capitalize; |
| } |
| |
| .result-same { |
| background: #d4edda; |
| color: #155724; |
| } |
| |
| .result-different { |
| background: #f8d7da; |
| color: #721c24; |
| } |
| |
| .no-results { |
| text-align: center; |
| padding: 40px; |
| color: var(--body-text-color); |
| opacity: 0.7; |
| font-style: italic; |
| font-size: 1.1em !important; |
| } |
| |
| /* Error messages */ |
| .error-message { |
| background: var(--background-fill-secondary); |
| color: var(--body-text-color); |
| padding: 20px; |
| border-radius: 8px; |
| text-align: center; |
| width: 100%; |
| opacity: 0.9; |
| font-size: 1.1em !important; |
| } |
| |
| /* Input images styling */ |
| .gradio-image { |
| display: flex !important; |
| justify-content: center !important; |
| align-items: center !important; |
| height: 320px !important; |
| } |
| |
| .gradio-image .wrap { |
| display: flex !important; |
| justify-content: center !important; |
| align-items: center !important; |
| width: 100% !important; |
| height: 100% !important; |
| } |
| |
| /* Examples styling */ |
| .examples-container { |
| background: var(--background-fill-secondary); |
| padding: 15px; |
| border-radius: 8px; |
| margin-top: 15px; |
| width: 100%; |
| text-align: center; |
| font-size: 1em !important; |
| } |
| |
| /* Section titles */ |
| .section-title { |
| margin-bottom: 15px !important; |
| text-align: center; |
| color: var(--body-text-color); |
| font-weight: 700; |
| font-size: 1.4em !important; |
| } |
| |
| /* Ensure proper text visibility in dark mode */ |
| .gradio-markdown { |
| color: var(--body-text-color) !important; |
| font-size: 1.05em !important; |
| line-height: 1.5 !important; |
| } |
| |
| .gradio-markdown h3 { |
| color: var(--body-text-color) !important; |
| font-size: 1.3em !important; |
| } |
| |
| .gradio-markdown p { |
| color: var(--body-text-color) !important; |
| font-size: 1.05em !important; |
| line-height: 1.5 !important; |
| } |
| |
| .gradio-markdown li { |
| color: var(--body-text-color) !important; |
| font-size: 1.05em !important; |
| line-height: 1.5 !important; |
| margin-bottom: 8px !important; |
| } |
| |
| /* Label styling */ |
| .gr-label { |
| font-size: 1.1em !important; |
| font-weight: 600 !important; |
| } |
| |
| /* Accordion styling */ |
| .gr-accordion { |
| font-size: 1.1em !important; |
| } |
| |
| /* Examples text */ |
| .gr-examples { |
| font-size: 1em !important; |
| } |
| """ |
|
|
| |
| with gr.Blocks( |
| title="MiniAiLive - Face Recognition WebAPI Playground", |
| theme=gr.themes.Glass(), |
| css=get_custom_css() |
| ) as demo: |
| |
| with gr.Column(elem_classes="container"): |
| |
| gr.HTML(""" |
| <div class="company-header"> |
| <div class="header-logo"> |
| <img src="https://miniai.live/wp-content/uploads/2024/02/logo_name-1-768x426-1.png" alt="MiniAiLive Logo"> |
| </div> |
| <div class="header-text"> |
| <h1>MiniAiLive Face Recognition WebAPI Playground</h1> |
| <p>Experience our NIST FRVT Top Ranked 1:1 & 1:N Face Matching Technology</p> |
| </div> |
| </div> |
| """) |
| |
| |
| with gr.Column(elem_classes="about-section"): |
| with gr.Row(elem_classes="about-content"): |
| |
| with gr.Column(scale=0.6, elem_classes="features-section"): |
| gr.Markdown(""" |
| **MiniAiLive** is a leading provider of cutting-edge face recognition and liveness detection solutions. |
| Our technology is **NIST FRVT Top Ranked** for Face Recognition. |
| |
| ### 🏆 Key Features: |
| - **NIST FRVT Top Ranked** Top Accuracy in both 1:1 & 1:N Face Matching Technology |
| - **Real-time** Face Matching & Verification |
| - **On-Premise** Offers complete data control and privacy |
| - **On-Device, Offline** Runs on machine. No internet connection is required. |
| - **Cross-Platform Support** Supports Windows, Linux, Android & iOS |
| - **Free Service** Completely free to use, Free technical support & update |
| |
| [Visit our website](https://miniai.live) to explore our complete suite of AI solutions! |
| """) |
| |
| |
| with gr.Column(scale=0.4, elem_classes="demo-items-section"): |
| with gr.Column(elem_classes="demo-items-grid"): |
| gr.HTML(""" |
| <a href="https://github.com/MiniAiLive" target="_blank" class="demo-item"> |
| <img src="https://miniai.live/wp-content/uploads/2024/10/new_git-1-300x67.png" alt="GitHub"> |
| <span>GitHub</span> |
| </a> |
| """) |
| |
| gr.HTML(""" |
| <a href="https://huggingface.co/MiniAiLive" target="_blank" class="demo-item"> |
| <img src="https://miniai.live/wp-content/uploads/2024/10/new_hugging-1-300x67.png" alt="HuggingFace"> |
| <span>HuggingFace</span> |
| </a> |
| """) |
| |
| gr.HTML(""" |
| <a href="https://demo.miniai.live" target="_blank" class="demo-item"> |
| <img src="https://miniai.live/wp-content/uploads/2024/10/new_gradio-300x67.png" alt="Gradio"> |
| <span>Live Demo</span> |
| </a> |
| """) |
| |
| gr.HTML(""" |
| <a href="https://docs.miniai.live/" target="_blank" class="demo-item"> |
| <img src="https://miniai.live/wp-content/uploads/2024/10/a-300x70.png" alt="Documentation"> |
| <span>Documentation</span> |
| </a> |
| """) |
| |
| gr.HTML(""" |
| <a href="https://www.youtube.com/@miniailive" target="_blank" class="demo-item"> |
| <img src="https://miniai.live/wp-content/uploads/2024/10/Untitled-1-300x70.png" alt="YouTube"> |
| <span>YouTube</span> |
| </a> |
| """) |
| |
| gr.HTML(""" |
| <a href="https://play.google.com/store/apps/dev?id=5831076207730531667" target="_blank" class="demo-item"> |
| <img src="https://miniai.live/wp-content/uploads/2024/10/googleplay-300x62.png" alt="Google Play"> |
| <span>Google Play</span> |
| </a> |
| """) |
| |
| |
| with gr.Row(elem_classes="main-content-row"): |
| |
| with gr.Column(scale=0.6, elem_classes="upload-section"): |
| gr.Markdown("### 1. Upload Images for Comparison", elem_classes="section-title") |
| |
| with gr.Row(elem_classes="upload-images-row"): |
| |
| with gr.Column(scale=1, elem_classes="upload-image-col"): |
| im_match_in1 = gr.Image( |
| type='filepath', |
| height=320, |
| label="First Image", |
| show_download_button=False |
| ) |
| |
| |
| with gr.Column(scale=1, elem_classes="upload-image-col"): |
| im_match_in2 = gr.Image( |
| type='filepath', |
| height=320, |
| label="Second Image", |
| show_download_button=False |
| ) |
| |
| |
| with gr.Accordion("Try Example Images", open=True): |
| with gr.Row(): |
| gr.Examples( |
| examples=[ |
| "assets/1.jpg", |
| "assets/2.jpg", |
| "assets/3.jpg", |
| "assets/4.jpg", |
| ], |
| inputs=im_match_in1, |
| label="First Image Examples" |
| ) |
| gr.Examples( |
| examples=[ |
| "assets/1-1.jpg", |
| "assets/2-1.jpg", |
| "assets/3-1.jpg", |
| "assets/4-1.jpg", |
| ], |
| inputs=im_match_in2, |
| label="Second Image Examples" |
| ) |
| |
| btn_f_match = gr.Button( |
| "Compare Faces 🚀", |
| variant='primary', |
| elem_classes="button-primary" |
| ) |
| |
| |
| with gr.Column(scale=0.4, elem_classes="result-section"): |
| gr.Markdown("### 2. Comparison Results", elem_classes="section-title") |
| txt_compare_out = gr.HTML( |
| value="<div style='text-align: center; padding: 40px; font-size: 1.1em;'>Results will appear here after comparison</div>" |
| ) |
| |
| |
| btn_f_match.click( |
| face_compare, |
| inputs=[im_match_in1, im_match_in2], |
| outputs=txt_compare_out |
| ) |
| |
| |
| gr.HTML(""" |
| <a href="https://visitorbadge.io/status?path=https%3A%2F%2Fhuggingface.co%2Fspaces%2FMiniAiLive%2FFaceRecognition-LivenessDetection-Demo"> |
| <img src="https://api.visitorbadge.io/api/combined?path=https%3A%2F%2Fhuggingface.co%2Fspaces%2FMiniAiLive%2FFaceRecognition-LivenessDetection-Demo&label=VISITORS&labelColor=%2337d67a&countColor=%23ff8a65&style=plastic&labelStyle=none" /> |
| </a> |
| """) |
|
|
| if __name__ == "__main__": |
| demo.launch( |
| share=False, |
| show_error=True, |
| server_name="0.0.0.0", |
| server_port=7860 |
| ) |