[object Object]

← back to Dw Photo Capture

batch.html: adopt shared capture-pipeline engine, full Photoshop set on top of gray-card calibration (TK-12090)

2a2801e4f8e84730c3abeb4a296580aa0802a4ad · 2026-09-23 17:52:09 -0700 · Steve Abrams

pipeGains() now returns ONLY the gray-card WB ratio (rGain/gGain/bGain); brightness and temp/warmth
now handled by the shared engine's exposure and temp/tint sliders.

applyPipeline() threaded with a capture:bool parameter — delegates to CapturePipeline.apply()
passing the WB gains as preGain and the auto-exposure multiplier (computeES) as exposureMul.
preview tick calls with capture:false (no unsharp); grabPhoto calls with capture:true (unsharp enabled).

tune loading uses CapturePipeline.normalizeTune() for back-compat (bright→exposure, warm→temp).

bindTune maps legacy HTML slider IDs to new tune field names. Existing 3-slider panel (exposure/temp/hue)
still wired; full 11-control extension can be added later.

The gray-card CALIBRATE workflow (enterCalib, cSave, CAL_TARGET/CAL_GRAY/CAL_REF, drawCalOverlay) is
byte-identical to before; computeES() still reads raw pixels before the engine bake.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKaojLnVW8hyxsVriY5f9a

Files touched

Diff

commit 2a2801e4f8e84730c3abeb4a296580aa0802a4ad
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 23 17:52:09 2026 -0700

    batch.html: adopt shared capture-pipeline engine, full Photoshop set on top of gray-card calibration (TK-12090)
    
    pipeGains() now returns ONLY the gray-card WB ratio (rGain/gGain/bGain); brightness and temp/warmth
    now handled by the shared engine's exposure and temp/tint sliders.
    
    applyPipeline() threaded with a capture:bool parameter — delegates to CapturePipeline.apply()
    passing the WB gains as preGain and the auto-exposure multiplier (computeES) as exposureMul.
    preview tick calls with capture:false (no unsharp); grabPhoto calls with capture:true (unsharp enabled).
    
    tune loading uses CapturePipeline.normalizeTune() for back-compat (bright→exposure, warm→temp).
    
    bindTune maps legacy HTML slider IDs to new tune field names. Existing 3-slider panel (exposure/temp/hue)
    still wired; full 11-control extension can be added later.
    
    The gray-card CALIBRATE workflow (enterCalib, cSave, CAL_TARGET/CAL_GRAY/CAL_REF, drawCalOverlay) is
    byte-identical to before; computeES() still reads raw pixels before the engine bake.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01XKaojLnVW8hyxsVriY5f9a
---
 public/batch.html | 35 +++++++++++++++++------------------
 1 file changed, 17 insertions(+), 18 deletions(-)

diff --git a/public/batch.html b/public/batch.html
index f974c1e..11dd85f 100644
--- a/public/batch.html
+++ b/public/batch.html
@@ -24,6 +24,7 @@
   };
 })();
 </script>
+<script src="/js/capture-pipeline.js"></script>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1, user-scalable=no">
 <meta name="apple-mobile-web-app-capable" content="yes">
@@ -450,7 +451,8 @@ let lastGate=null, curSku='', skuConf=1, lastOcrKey='', ocrPending=false, batchC
 // manually) UNLESS the first photo already carries the code (info-on-front → one entry, no 2nd shot).
 let phase='psku', pendingFront=null;
 // Live colour tools — sticky (carry to the next photo). Manual nudges on top of the gray-card cal.
-let tune = loadJSON('dwbatch.tune', {bright:0, warm:0, hue:0});
+// Back-compat: loadJSON retrieves old {bright,warm,hue} format; normalizeTune maps bright→exposure, warm→temp
+let tune = CapturePipeline.normalizeTune(loadJSON('dwbatch.tune', null)) || CapturePipeline.defaultTune();
 
 function enterShoot(){
   show('vShoot');
@@ -472,24 +474,20 @@ function hueMatrix(deg){ const a=deg*Math.PI/180, c=Math.cos(a), s=Math.sin(a);
   return [ 0.213+c*0.787-s*0.213, 0.715-c*0.715-s*0.715, 0.072-c*0.072+s*0.928,
            0.213-c*0.213+s*0.143, 0.715+c*0.285+s*0.140, 0.072-c*0.072-s*0.283,
            0.213-c*0.213-s*0.787, 0.715-c*0.715+s*0.715, 0.072+c*0.928+s*0.072 ]; }
-function pipeGains(){                          // per-channel gains from cal WB + manual warmth/brightness
+function pipeGains(){                          // gray-card WB ratio ONLY (brightness/temp now in the shared engine)
   const wbR=cal?cal.wbR:1, wbG=cal?cal.wbG:1, wbB=cal?cal.wbB:1;
-  const bmul=clamp(1+(tune.bright/100),0.2,2.2);       // brightness slider
-  const warm=(tune.warm||0)/100; const rW=1+warm*0.30, bW=1-warm*0.30;   // warmth: R↑ / B↓
-  return { gr:wbR*bmul*rW, gg:wbG*bmul, gb:wbB*bmul*bW };
+  return { rGain:wbR, gGain:wbG, bGain:wbB };  // per-channel WB gains; shared engine applies exposure/temp on top
 }
 function computeES(d){                          // auto exposure-normalize toward the cal luma target
   if(!cal) return 1; let sl=0,n=0; for(let i=0;i<d.length;i+=64){ sl+=0.299*d[i]+0.587*d[i+1]+0.114*d[i+2]; n++; }
   const cur=n?sl/n:cal.lumaTarget; return clamp(cal.lumaTarget/(cur||1),0.5,2); }
-function applyPipeline(ctx,w,h){
-  const img=ctx.getImageData(0,0,w,h); const d=img.data;
-  const es=computeES(d); const g=pipeGains(); const GR=g.gr*es, GG=g.gg*es, GB=g.gb*es;
-  const hue=tune.hue||0;
-  if(hue){ const m=hueMatrix(hue);
-    for(let i=0;i<d.length;i+=4){ const r=clamp(d[i]*GR,0,255),gg2=clamp(d[i+1]*GG,0,255),b=clamp(d[i+2]*GB,0,255);
-      d[i]=clamp(m[0]*r+m[1]*gg2+m[2]*b,0,255); d[i+1]=clamp(m[3]*r+m[4]*gg2+m[5]*b,0,255); d[i+2]=clamp(m[6]*r+m[7]*gg2+m[8]*b,0,255); } }
-  else { for(let i=0;i<d.length;i+=4){ d[i]=clamp(d[i]*GR,0,255); d[i+1]=clamp(d[i+1]*GG,0,255); d[i+2]=clamp(d[i+2]*GB,0,255); } }
-  ctx.putImageData(img,0,0);
+function applyPipeline(ctx,w,h,capture){
+  // Compute auto-exposure on the RAW pixels (before any baking)
+  const img=ctx.getImageData(0,0,w,h);
+  const es=computeES(img.data);  // auto-exposure multiplier toward cal.lumaTarget
+  const g=pipeGains();           // gray-card WB gains (rGain, gGain, bGain)
+  // Delegate the full bake to the shared engine, passing WB gains + auto-exposure multiplier
+  CapturePipeline.apply(ctx, w, h, tune, {capture: capture||false, preGain:g, exposureMul:es});
 }
 // live WYSIWYG preview — corrected frame drawn over the raw video (throttled; capture stays full-res)
 let pvTimer=null;
@@ -500,14 +498,15 @@ function previewTick(){
   const scale=Math.min(1, 640/Math.max(w,h)); const pw=Math.round(w*scale), ph=Math.round(h*scale);
   const pv=$('#pv'); if(pv.width!==pw)pv.width=pw; if(pv.height!==ph)pv.height=ph;
   const x=pv.getContext('2d',{willReadFrequently:true});
-  try{ x.drawImage(v,0,0,pw,ph); applyPipeline(x,pw,ph); }catch(e){}
+  try{ x.drawImage(v,0,0,pw,ph); applyPipeline(x,pw,ph,false); }catch(e){}
 }
 // slider wiring — values persist so the setting carries to the next photo
+// Map legacy HTML ids (sBright/sWarm) to new tune field names (exposure/temp/hue)
 function bindTune(){
-  const map=[['sBright','bright','vBright'],['sWarm','warm','vWarm'],['sHue','hue','vHue']];
+  const map=[['sBright','exposure','vBright'],['sWarm','temp','vWarm'],['sHue','hue','vHue']];
   for(const [id,key,lbl] of map){ const el=$('#'+id); if(!el)continue; el.value=tune[key]; $('#'+lbl).textContent=tune[key];
     el.oninput=()=>{ tune[key]=parseInt(el.value,10)||0; $('#'+lbl).textContent=tune[key]; saveJSON('dwbatch.tune',tune); }; }
-  const r=$('#sReset'); if(r) r.onclick=()=>{ tune={bright:0,warm:0,hue:0}; saveJSON('dwbatch.tune',tune);
+  const r=$('#sReset'); if(r) r.onclick=()=>{ tune=CapturePipeline.defaultTune(); saveJSON('dwbatch.tune',tune);
     for(const [id,,lbl] of map){ const e=$('#'+id); if(e)e.value=0; $('#'+lbl).textContent=0; } };
 }
 // draggable + collapsible tool panel; position/collapsed remembered, default center-bottom
@@ -663,7 +662,7 @@ async function grabPhoto(){
   const sx=Math.round(cr.x*bmp.width), sy=Math.round(cr.y*bmp.height), sw=Math.round(cr.w*bmp.width), sh=Math.round(cr.h*bmp.height);
   const mc=document.createElement('canvas'); mc.width=sw; mc.height=sh; const mx=mc.getContext('2d',{willReadFrequently:true});
   mx.drawImage(bmp, sx,sy,sw,sh, 0,0, sw,sh);
-  applyPipeline(mx, sw, sh);                          // MASTER = crop → WB × exposure × manual tune (WYSIWYG)
+  applyPipeline(mx, sw, sh, true);                    // MASTER = crop → WB × exposure × manual tune (WYSIWYG) + unsharp
   const master=await blobOf(mc, CFG.jpegQ);
   const scale=Math.min(1, CFG.webLong/Math.max(sw,sh));
   const wc=document.createElement('canvas'); wc.width=Math.round(sw*scale); wc.height=Math.round(sh*scale);

← 880e4d8 cam.html + desk.html: full pre-capture photoshop camera (zer  ·  back to Dw Photo Capture  ·  auto-data-snapshot: 2026-09-24T03:56:09 (1 data files) — dat 12011d5 →