← back to Dw Photo Capture

visual-search/train/download.py

65 lines

#!/usr/bin/env python3
# Resumable, threaded image downloader for the DW CLIP fine-tune.
# Reads manifest.tsv, fetches each catalog image at SMALL size (Shopify CDN width=512 so we pull
# ~20KB not ~1MB), downscales to 256px, writes cache/<id>.jpg. Skips already-cached ids so it
# resumes cleanly. Bandwidth ≈ rows × ~25KB (not × 1MB) thanks to the CDN width rewrite.
import os, io, sys, time, threading, queue, urllib.request, urllib.parse
from PIL import Image

HERE = os.path.dirname(os.path.abspath(__file__))
CACHE = os.path.join(HERE, 'cache'); os.makedirs(CACHE, exist_ok=True)
SIZE = 256; WORKERS = 16
Image.MAX_IMAGE_PIXELS = None

def small_url(u):
    # Shopify CDN honors ?width= — ask for 512 so a 256 thumbnail is crisp but the download stays tiny.
    if 'shopify' in u or 'cdn.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 fetch(idv, url):
    out = os.path.join(CACHE, idv + '.jpg')
    if os.path.exists(out): return 'skip'
    try:
        req = urllib.request.Request(small_url(url), headers={'User-Agent': 'dwtrain/1'})
        data = urllib.request.urlopen(req, timeout=25).read()
        im = Image.open(io.BytesIO(data)).convert('RGB')
        im.thumbnail((SIZE, SIZE))
        im.save(out, 'JPEG', quality=88)
        return 'ok'
    except Exception:
        return 'err'

def main():
    rows = []
    with open(os.path.join(HERE, 'manifest.tsv')) as f:
        for line in f:
            parts = line.rstrip('\n').split('\t')
            if len(parts) >= 2 and parts[0] and parts[1].startswith('http'):
                rows.append((parts[0], parts[1]))
    q = queue.Queue()
    for r in rows: q.put(r)
    total = len(rows)
    counts = {'ok': 0, 'skip': 0, 'err': 0}
    lock = threading.Lock(); t0 = time.time()
    def worker():
        while True:
            try: idv, url = q.get_nowait()
            except queue.Empty: return
            r = fetch(idv, url)
            with lock:
                counts[r] += 1
                done = counts['ok'] + counts['skip'] + counts['err']
                if done % 500 == 0 or done == total:
                    rate = done / max(1e-6, time.time() - t0)
                    print(f"{done}/{total}  ok={counts['ok']} skip={counts['skip']} err={counts['err']}  {rate:.0f}/s", flush=True)
            q.task_done()
    ts = [threading.Thread(target=worker, daemon=True) for _ in range(WORKERS)]
    for t in ts: t.start()
    for t in ts: t.join()
    print(f"DONE  cached={counts['ok']+counts['skip']}  err={counts['err']}", flush=True)

if __name__ == '__main__':
    main()