← back to Dw Photo Capture

visual-search/train/robustness_test.py

74 lines

#!/usr/bin/env python3
# Real-world-ish validation of the fine-tuned visual-ID: does a DEGRADED "phone-like" photo of a
# catalog item still retrieve the CORRECT product from the live FT index — and do the confidence
# thresholds (>=0.92 high, >=0.85 medium) mean anything on that query distribution?
# We can't shoot real samples autonomously, so we simulate handheld capture: downscale, JPEG-recompress,
# blur, brightness/glare, small rotate+crop — then query the LIVE /api/identify-multi endpoint.
import os, io, sys, json, base64, subprocess, urllib.request, urllib.parse, ssl
from PIL import Image, ImageFilter, ImageEnhance

APP = "https://photo.designerwallcoverings.com/api/identify-multi"
AUTH = base64.b64encode(b"admin:DW2024!").decode()
N = 40
ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE

# pull N random rows that ARE embedded on KAMATERA (the live FT index) — so the correct answer is in it
remote_sql = ("select e.dw_sku, e.image_url from image_embeddings e where e.embedding is not null "
              "and left(e.image_url,4)='http' order by random() limit " + str(N))
cmd = ('DB="$(grep ^DW_UNIFIED_DB= /root/public-projects/dwphoto/.env|cut -d= -f2-)"; '
       'psql "$DB" -F "|" -tA -c "' + remote_sql + '"')
out = subprocess.check_output(['ssh','root@45.61.58.125', cmd]).decode()
rows = [l.split('|',1) for l in out.splitlines() if '|' in l]
print(f"pulled {len(rows)} live-indexed rows", flush=True)

def small(u):
    if 'shopify' in u:
        pr=urllib.parse.urlparse(u);q=urllib.parse.parse_qs(pr.query);q['width']=['512']
        return urllib.parse.urlunparse(pr._replace(query=urllib.parse.urlencode(q,doseq=True)))
    return u

def degrade(im):
    # mimic a handheld iPhone shot of a physical sample
    w,h = im.size
    im = im.rotate(4, expand=False, fillcolor=(240,240,240))        # slight tilt
    im = im.crop((int(w*0.06), int(h*0.06), int(w*0.94), int(h*0.94)))  # off-center crop
    im = im.resize((max(64,im.size[0]//2), max(64,im.size[1]//2)))   # lower res (distance)
    im = im.filter(ImageFilter.GaussianBlur(1.2))                    # hand shake / focus
    im = ImageEnhance.Brightness(im).enhance(1.18)                   # glare/overexposure
    im = ImageEnhance.Contrast(im).enhance(0.92)
    b=io.BytesIO(); im.save(b,'JPEG',quality=55); b.seek(0)          # phone JPEG compression
    return Image.open(b).convert('RGB')

def query(im):
    bio=io.BytesIO(); im.save(bio,'JPEG',quality=80)
    b64='data:image/jpeg;base64,'+base64.b64encode(bio.getvalue()).decode()
    req=urllib.request.Request(APP, data=json.dumps({'front':b64}).encode(),
        headers={'Content-Type':'application/json','Authorization':'Basic '+AUTH,'User-Agent':'rt/1'})
    return json.load(urllib.request.urlopen(req, timeout=30, context=ctx))

top1=0; top5=0; conf={'high':0,'medium':0,'low':0}; scored=[]; n=0
for dw_sku,url in rows:
    try:
        data=urllib.request.urlopen(urllib.request.Request(small(url),headers={'User-Agent':'rt/1'}),timeout=20).read()
        q=degrade(Image.open(io.BytesIO(data)).convert('RGB'))
        r=query(q); vis=r.get('visual') or []
        if not vis: continue
        n+=1
        skus=[str(v.get('dw_sku')) for v in vis]
        if skus and skus[0]==dw_sku: top1+=1
        if dw_sku in skus[:5]: top5+=1
        conf[r.get('confidence','low')] = conf.get(r.get('confidence','low'),0)+1
        scored.append((dw_sku, round(vis[0].get('score',0),3), skus[0]==dw_sku, r.get('confidence')))
    except Exception as e:
        print('skip',e, flush=True)

print(f"\n=== DEGRADED (phone-like) image→image retrieval on the LIVE FT index, n={n} ===")
print(f"top-1 correct: {top1}/{n} ({100*top1/max(1,n):.0f}%)   top-5: {top5}/{n} ({100*top5/max(1,n):.0f}%)")
print(f"confidence buckets: {conf}")
correct_scores=[s for _,s,c,_ in scored if c]; wrong_scores=[s for _,s,c,_ in scored if not c]
if correct_scores: print(f"top score when CORRECT: min={min(correct_scores):.3f} avg={sum(correct_scores)/len(correct_scores):.3f}")
if wrong_scores:   print(f"top score when WRONG:   max={max(wrong_scores):.3f} avg={sum(wrong_scores)/len(wrong_scores):.3f}")
# threshold sanity: of matches labeled high (>=0.92), how many were actually correct?
high=[c for _,s,c,cf in scored if cf=='high'];
if high: print(f"of 'high'-confidence results, {sum(high)}/{len(high)} were the CORRECT product ({100*sum(high)/len(high):.0f}%)")