|
|
import gradio as gr |
|
|
import os |
|
|
import requests |
|
|
from PIL import Image |
|
|
import json |
|
|
|
|
|
|
|
|
API_BASE_URL = os.getenv("API_BASE_URL") |
|
|
API_TOKEN = os.getenv("API_TOKEN") |
|
|
|
|
|
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['file1'] = open(frame1, 'rb') |
|
|
if frame2: |
|
|
files['file2'] = open(frame2, 'rb') |
|
|
|
|
|
if not files: |
|
|
return "<div class='error-message'>Please upload both images</div>" |
|
|
|
|
|
|
|
|
headers = { |
|
|
"Authorization": f"Bearer {API_TOKEN}" |
|
|
} |
|
|
|
|
|
|
|
|
response = requests.post(url=url, files=files, headers=headers) |
|
|
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</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> |
|
|
<div class="matches-table"> |
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>First Face</th> |
|
|
<th>Second 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 """ |
|
|
|
|
|
/* 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: 10px; |
|
|
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: 80px; |
|
|
height: auto; |
|
|
} |
|
|
|
|
|
.header-text { |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.header-text h1 { |
|
|
font-size: 2.4em !important; |
|
|
font-weight: 700; |
|
|
color: var(--body-text-color); |
|
|
} |
|
|
|
|
|
.header-text p { |
|
|
font-size: 1.3em !important; |
|
|
color: var(--body-text-color); |
|
|
opacity: 0.8; |
|
|
} |
|
|
|
|
|
/* Main content layout */ |
|
|
.main-content-row { |
|
|
display: flex; |
|
|
gap: 25px; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
.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: 6px 12px !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-content { |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
/* Detection cards */ |
|
|
.detections-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); |
|
|
gap: 15px; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.detection-card { |
|
|
background: var(--background-fill-secondary); |
|
|
padding: 4px; |
|
|
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; |
|
|
} |
|
|
|
|
|
/* Matching table - NEW STYLING */ |
|
|
.matches-table { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
width: 100%; |
|
|
overflow-x: auto; |
|
|
} |
|
|
|
|
|
.matches-table table { |
|
|
width: 100%; |
|
|
border-collapse: collapse; |
|
|
font-size: 1em !important; |
|
|
min-width: 450px; |
|
|
} |
|
|
|
|
|
.matches-table th { |
|
|
background: var(--background-fill-secondary); |
|
|
color: var(--body-text-color); |
|
|
padding: 4px 2px !important; |
|
|
text-align: center; |
|
|
font-size: 1em !important; |
|
|
font-weight: 700; |
|
|
border-bottom: 2px solid var(--border-color-primary); |
|
|
} |
|
|
|
|
|
.matches-table td { |
|
|
padding: 4px 2px !important; |
|
|
border-bottom: 1px solid var(--border-color-primary); |
|
|
text-align: center; |
|
|
font-size: 0.95em !important; |
|
|
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; |
|
|
} |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
title="MiniAiLive - Face Recognition WebAPI Playground", |
|
|
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/2025/11/logo_new.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.Row(elem_classes="main-content-row"): |
|
|
|
|
|
with gr.Column(scale=0.6, elem_classes="upload-section"): |
|
|
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=380, |
|
|
label="First Image", |
|
|
show_download_button=False |
|
|
) |
|
|
gr.Examples( |
|
|
examples=[ |
|
|
"assets/1.jpg", |
|
|
"assets/2.jpg", |
|
|
"assets/3.jpg", |
|
|
"assets/4.jpg", |
|
|
], |
|
|
inputs=im_match_in1, |
|
|
label="First Image Examples" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Column(scale=1, elem_classes="upload-image-col"): |
|
|
im_match_in2 = gr.Image( |
|
|
type='filepath', |
|
|
height=380, |
|
|
label="Second Image", |
|
|
show_download_button=False |
|
|
) |
|
|
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"): |
|
|
txt_compare_out = gr.HTML( |
|
|
value="<div style='text-align: center; padding: 10px; 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 |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch( |
|
|
share=False, |
|
|
show_api=False, |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860 |
|
|
) |