Ai_Inspection_System / report_generator.py
RehamAAhmed's picture
Upload 18 files
a0554a9 verified
Raw
History Blame Contribute Delete
16 kB
import os
import base64
from pathlib import Path
def generate_combined_report(results, out_dir):
"""
results is a list of dicts, each containing:
- 'before_path': Path or None
- 'after_path': Path or None
- 'status': 'INTACT' | 'DAMAGED' | 'MISSING' | 'ADDED'
- 'cls_score': float
- 'patch_score': float
- 'geo_inliers': int
- 'confidence': str
- 'stage_used': int
- 'damage_description': str
- 'target_phrases_list': list of str
- 'masked_after_path': Path or None
"""
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
def b64img(path):
if not path or not os.path.exists(path):
return ""
try:
p = Path(path)
data = base64.b64encode(p.read_bytes()).decode()
ext = p.suffix.lstrip(".") or "jpeg"
return f"data:image/{ext};base64,{data}"
except Exception as e:
print(f"Error encoding image {path}: {e}")
return ""
cards = ""
for r in results:
status = r['status']
# Determine styling based on status
if status == 'INTACT':
color = "#10b981" # Emerald
bg_color = "rgba(16, 185, 129, 0.05)"
border_c = "rgba(16, 185, 129, 0.2)"
icon = "✅"
label = "INTACT / سليم"
elif status == 'DAMAGED':
color = "#f59e0b" # Amber
bg_color = "rgba(245, 158, 11, 0.05)"
border_c = "rgba(245, 158, 11, 0.25)"
icon = "⚠️"
label = "DAMAGED / تالف"
elif status == 'MISSING':
color = "#ef4444" # Red
bg_color = "rgba(239, 68, 68, 0.05)"
border_c = "rgba(239, 68, 68, 0.2)"
icon = "❌"
label = "MISSING / مفقود"
else: # ADDED
color = "#3b82f6" # Blue
bg_color = "rgba(59, 130, 246, 0.05)"
border_c = "rgba(59, 130, 246, 0.25)"
icon = "➕"
label = "ADDED / مضاف"
before_html = ""
if r['before_path']:
before_src = b64img(r['before_path'])
before_name = Path(r['before_path']).name
before_html = f"""
<div class="img-box">
<div class="box-label">BEFORE IMAGE / الصورة قبل</div>
<img src="{before_src}" alt="{before_name}"/>
<div class="fname">{before_name}</div>
</div>"""
after_html = ""
if r['after_path']:
after_src = b64img(r['after_path'])
after_name = Path(r['after_path']).name
after_html = f"""
{"" if status == "ADDED" else '<div class="arrow">→</div>'}
<div class="img-box">
<div class="box-label">AFTER IMAGE / الصورة بعد</div>
<img src="{after_src}" alt="{after_name}"/>
<div class="fname">{after_name}</div>
</div>"""
mask_html = ""
if status == 'DAMAGED' and r['masked_after_path']:
mask_src = b64img(r['masked_after_path'])
mask_name = Path(r['masked_after_path']).name
mask_html = f"""
<div class="arrow">→</div>
<div class="img-box highlighted">
<div class="box-label" style="color:#f59e0b">🎯 DETECTED DAMAGE (SAM MASK) / الضرر المكتشف (قناع SAM)</div>
<img src="{mask_src}" alt="{mask_name}" style="border-color:#f59e0b"/>
<div class="fname" style="color:#f59e0b">{mask_name}</div>
</div>"""
stage_labels = {1: "DINOv2 CLS", 2: "DINOv2 Patch", 3: "SIFT+RANSAC"}
stage_str = stage_labels.get(r.get('stage_used', 0), "None")
# Matching metrics block
scores_html = ""
if status in ['INTACT', 'DAMAGED'] and r['after_path']:
scores_html = f"""
<div class="metric-chip">Match / مطابقة: <b>{stage_str}</b></div>
<div class="metric-chip">CLS score / درجة CLS: <b>{r['cls_score']:.3f}</b></div>
"""
if r['patch_score']:
scores_html += f'<div class="metric-chip">Patch score / درجة الرقعة: <b>{r["patch_score"]:.3f}</b></div>'
if r['geo_inliers']:
scores_html += f'<div class="metric-chip">Geo Inliers / مطابقة هندسية: <b>{r["geo_inliers"]}</b></div>'
if r.get('confidence'):
scores_html += f'<div class="metric-chip">Confidence / ثقة: <b>{r["confidence"]}</b></div>'
# Damage Report Text Block
report_details_html = ""
if status == 'DAMAGED':
phrases_badges = "".join([f'<span class="phrase-tag">{p}</span>' for p in r['target_phrases_list']])
report_details_html = f"""
<div class="damage-report-box">
<h4>📝 AI Damage Assessment Report / تقرير فحص التلفيات بالذكاء الاصطناعي</h4>
<p class="desc">{r['damage_description']}</p>
<div class="phrases-container">
<strong>Segmented features / الأجزاء المحددة:</strong>
<div class="phrase-tags-row">{phrases_badges}</div>
</div>
</div>
"""
elif status == 'INTACT':
report_details_html = f"""
<div class="intact-report-box">
<p>✨ AI analysis confirms this item is intact. No major damage or negative changes were detected. / يؤكد تحليل الذكاء الاصطناعي أن هذا العنصر سليم، ولم يتم رصد أي تلفيات أو تغييرات سلبية.</p>
</div>
"""
elif status == 'MISSING':
report_details_html = f"""
<div class="missing-report-box">
<p>🔍 This item from the before inventory could not be matched with any item in the after inventory. It may have been moved, stolen, or removed. / لم يتم العثور على مطابقة لهذا العنصر في صور البعد، قد يكون تم نقله، سرقته أو إزالته.</p>
</div>
"""
else: # ADDED
report_details_html = f"""
<div class="added-report-box">
<p>➕ New item added in the after inventory that was not present in the before inventory. / تم رصد عنصر جديد مضاف في صور البعد لم يكن موجوداً في صور القبل.</p>
</div>
"""
cards += f"""
<div class="card" data-status="{status}" style="background:{bg_color};border-color:{border_c}">
<div class="card-header">
<div class="badge" style="background:{color}">{icon} {label}</div>
<div class="metrics-row">{scores_html}</div>
</div>
<div class="img-row">
{before_html}
{after_html}
{mask_html}
</div>
{report_details_html}
</div>"""
n_total = len(results)
n_intact = sum(1 for r in results if r['status'] == 'INTACT')
n_damaged = sum(1 for r in results if r['status'] == 'DAMAGED')
n_missing = sum(1 for r in results if r['status'] == 'MISSING')
n_added = sum(1 for r in results if r['status'] == 'ADDED')
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Inventory & Damage Report</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Cairo:wght@300;400;600;700;800&display=swap');
* {{
box-sizing: border-box;
margin: 0;
padding: 0;
}}
body {{
font-family: 'Inter', 'Cairo', -apple-system, sans-serif;
background: #0b0f19;
color: #f1f5f9;
padding: 2.5rem 1.5rem;
line-height: 1.5;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
}}
header {{
text-align: center;
margin-bottom: 2.5rem;
}}
h1 {{
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 0.5rem;
letter-spacing: -0.025em;
background: linear-gradient(135deg, #60a5fa, #34d399);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}}
.sub {{
color: #94a3b8;
font-size: 1rem;
font-weight: 400;
}}
.summary {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1.25rem;
margin: 2rem 0;
}}
.stat {{
background: #1e293b;
border-radius: 16px;
padding: 1.25rem;
text-align: center;
border: 1px solid #334155;
transition: transform 0.2s, box-shadow 0.2s;
}}
.stat:hover {{
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
}}
.stat .num {{
font-size: 2.25rem;
font-weight: 800;
line-height: 1.2;
}}
.stat .lbl {{
font-size: 0.85rem;
color: #94a3b8;
font-weight: 500;
margin-top: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}}
.filters {{
text-align: center;
margin-bottom: 2rem;
}}
.fbtn {{
background: #1e293b;
border: 1px solid #334155;
color: #cbd5e1;
padding: 0.5rem 1.5rem;
border-radius: 9999px;
cursor: pointer;
margin: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.2s;
}}
.fbtn:hover, .fbtn.active {{
background: #3b82f6;
color: #ffffff;
border-color: #3b82f6;
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.4);
}}
.card {{
border-radius: 20px;
padding: 1.75rem;
margin-bottom: 2rem;
border: 1px solid;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
background: #111827;
transition: transform 0.2s;
}}
.card:hover {{
transform: scale(1.005);
}}
.card-header {{
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 1rem;
}}
.badge {{
display: inline-block;
padding: 0.35rem 1.1rem;
border-radius: 9999px;
font-weight: 700;
color: #ffffff;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.025em;
}}
.metrics-row {{
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}}
.metric-chip {{
background: #1e293b;
border: 1px solid #334155;
padding: 0.25rem 0.75rem;
border-radius: 8px;
font-size: 0.75rem;
color: #94a3b8;
}}
.metric-chip b {{
color: #f1f5f9;
}}
.img-row {{
display: flex;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}}
.img-box {{
flex: 1;
min-width: 260px;
text-align: center;
background: #0f172a;
padding: 1rem;
border-radius: 12px;
border: 1px solid #1e293b;
}}
.img-box img {{
max-width: 100%;
max-height: 280px;
border-radius: 8px;
object-fit: contain;
border: 2px solid #334155;
margin-top: 0.5rem;
background: #0b0f19;
}}
.box-label {{
font-size: 0.75rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.08em;
}}
.fname {{
font-size: 0.75rem;
color: #475569;
margin-top: 0.5rem;
word-break: break-all;
font-family: monospace;
}}
.arrow {{
font-size: 2rem;
color: #475569;
font-weight: 800;
flex-shrink: 0;
}}
.damage-report-box {{
background: rgba(245, 158, 11, 0.03);
border: 1px dashed rgba(245, 158, 11, 0.2);
border-radius: 12px;
padding: 1.25rem;
margin-top: 1rem;
}}
.damage-report-box h4 {{
color: #f59e0b;
font-size: 0.95rem;
font-weight: 700;
margin-bottom: 0.5rem;
}}
.damage-report-box .desc {{
font-size: 0.9rem;
color: #e2e8f0;
margin-bottom: 0.75rem;
}}
.phrases-container {{
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
font-size: 0.8rem;
color: #94a3b8;
}}
.phrase-tags-row {{
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}}
.phrase-tag {{
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
border: 1px solid rgba(245, 158, 11, 0.2);
padding: 0.15rem 0.5rem;
border-radius: 4px;
font-weight: 600;
font-size: 0.75rem;
}}
.intact-report-box {{
background: rgba(16, 185, 129, 0.03);
border: 1px solid rgba(16, 185, 129, 0.15);
border-radius: 12px;
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: #a7f3d0;
}}
.missing-report-box {{
background: rgba(239, 68, 68, 0.03);
border: 1px solid rgba(239, 68, 68, 0.15);
border-radius: 12px;
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: #fca5a5;
}}
.added-report-box {{
background: rgba(59, 130, 246, 0.03);
border: 1px solid rgba(59, 130, 246, 0.15);
border-radius: 12px;
padding: 1rem 1.25rem;
font-size: 0.9rem;
color: #93c5fd;
}}
@media (max-width: 768px) {{
.arrow {{
display: none;
}}
.img-row {{
flex-direction: column;
}}
.img-box {{
width: 100%;
}}
.card-header {{
flex-direction: column;
align-items: flex-start;
}}
}}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🏠 Property Inventory & Damage Report / تقرير جرد وتحديد تلفيات الممتلكات</h1>
<p class="sub">AI Matching & Inspection System · نظام المطابقة والفحص الذكي بالذكاء الاصطناعي (DINOv2 + Grounded-SAM + MLLM)</p>
</header>
<div class="summary">
<div class="stat">
<div class="num" style="color:#22c55e">{n_intact}</div>
<div class="lbl">✅ Intact / سليم</div>
</div>
<div class="stat">
<div class="num" style="color:#f59e0b">{n_damaged}</div>
<div class="lbl">⚠️ Damaged / تالف</div>
</div>
<div class="stat">
<div class="num" style="color:#ef4444">{n_missing}</div>
<div class="lbl">❌ Missing / مفقود</div>
</div>
<div class="stat">
<div class="num" style="color:#3b82f6">{n_added}</div>
<div class="lbl">➕ Added / مضاف</div>
</div>
<div class="stat">
<div class="num" style="color:#a855f7">{n_total}</div>
<div class="lbl">Total Items / إجمالي العناصر</div>
</div>
</div>
<div class="filters">
<button class="fbtn active" onclick="filt('ALL', this)">All Items / كل العناصر</button>
<button class="fbtn" onclick="filt('INTACT', this)">✅ Intact / سليم</button>
<button class="fbtn" onclick="filt('DAMAGED', this)">⚠️ Damaged / تالف</button>
<button class="fbtn" onclick="filt('MISSING', this)">❌ Missing / مفقود</button>
<button class="fbtn" onclick="filt('ADDED', this)">➕ Added / مضاف</button>
</div>
<div id="cards-container">
{cards}
</div>
</div>
<script>
function filt(status, btn) {{
document.querySelectorAll('.fbtn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.card').forEach(c => {{
const cardStatus = c.getAttribute('data-status');
if (status === 'ALL') {{
c.style.display = 'block';
}} else {{
c.style.display = cardStatus === status ? 'block' : 'none';
}}
}});
}}
</script>
</body>
</html>"""
# Escape curly braces in css and scripts properly (using %% for format double escape or format string)
report_file = out / "inventory_damage_report.html"
report_file.write_text(html, encoding="utf-8")
return str(report_file)