Recipe
Build a document scanner
Use when: a document was photographed at an angle, under uneven real-world lighting, and you need a clean, thresholded, print-ready scan -- not just the flat rectangular warp Chapter 6 §9 stopped at
Goal: a binary (black text, white background) scanned-looking image, corrected for perspective, denoised, contrast-restored, and correctly thresholded despite uneven lighting
Taught in Applied: building a document scanner
Quick recipe
-
1
This step is Chapter 6 Section 9's own Steps 1-4, unchanged. Measure the document's four corners. Assign destination corners. Solve for the homography. Warp with bilinear or bicubic interpolation.
Example
A photographed page's corners solve to a homography. The warped output has straight, rectangular edges. It still keeps uneven lighting and a gray background. That is exactly the gap Chapter 6 §9 named.
Why: every later step in this recipe assumes a flat, rectangular input. Running denoising or thresholding on the original skewed photo first would process the wrong geometry entirely.
-
2
Apply Chapter 4's median filter to remove dust specks and sensor noise before anything else touches the image. Chapter 4 §9's own rule applies here directly: stray bright or dark specks, with everything else untouched, is exactly the salt-and-pepper signature a median filter is chosen for.
Example
A noisy row (180,255,182,179,0,181), median-filtered with a window of 3, becomes (180,182,182,179,179,181). Both speckle outliers, 255 and 0, are gone. The genuine paper values survive untouched.
Why: thresholding an un-denoised image would treat every dust speck as its own tiny "object." Section 8's own connected-component count would then be badly inflated by noise that has nothing to do with the document's real content.
-
3
Apply Chapter 2 §9's linear formula, f(v)=255\cdot\frac{v-v_{\min}}{v_{\max}-v_{\min}}. Use the denoised image's own observed range for v_{\min} and v_{\max}. This undoes a washed-out photo's squeezed contrast.
Example
The denoised row above has observed range v_{\min}=179, v_{\max}=182. That is a narrow, low-contrast band, the kind a photo has and a real scan does not. Stretching v=180 gives f(180)=255\cdot\frac{180-179}{182-179}=255/3\approx85.
Why: adaptive thresholding's own constant C, used in Step 4, is tuned against a full 0-255 range. Feeding it a washed-out, narrow-range image would require re-tuning C for every single photo's own lighting, instead of using one stable value.
-
4
Apply Section 3's local-mean-minus-constant scheme. Do not use Section 1's global threshold or Section 2's Otsu's method. Both of those are still single global values. Chapter 6 §9 named uneven lighting as the specific unsolved problem this step has to fix.
Example
A contrast-stretched paper pixel reads 85 after Step 3. Its own local neighborhood mean is 90, with C=10: T_{\text{local}}=90-10=80. Since 85\le80 is false, the pixel correctly stays background, paper, not falsely flagged as ink.
Why: this is the step that actually closes Chapter 6's own honest gap. A global threshold, Section 1, or even Otsu's optimal global threshold, Section 2, would still misclassify an entire shadowed region. Section 1's own worked example already demonstrated exactly that failure.
-
5
Run a small opening, Chapter 7 §4's erosion followed by dilation, with a small structuring element. This removes any stray foreground specks the thresholding step still produced, without eroding real letter strokes.
Example
A single stray foreground pixel left over from Step 4, surrounded entirely by background, does not survive a 3\times3 opening's erosion pass. It vanishes. A real multi-pixel letter stroke survives intact.
Why: Section 8's own worked examples showed exactly this failure mode. A single stray pixel gets labeled and counted as its own fake "object" the moment connected-component labeling runs on this output.
-
6
Check that the output reads as a genuine binary scan. Confirm straight edges from Step 1, no speckle noise from Steps 2 and 5, full contrast from Step 3, and text correctly split from paper, even under uneven lighting, from Step 4.
Example
This section's own widget above shows the honest limit directly. Row 4, deep shadow, clears correctly. Row 3 sits right at the light-to-dark transition. Its own local window gets pulled brighter by the neighboring lit region. Row 3 still shows a real, if smaller, mistake, compared to the ones global thresholding produced.
Why: naming one real problem that remains, rather than showing this pipeline as flawless, follows the same honest-scope standard Chapter 6 §9's own closing step set for this exact recipe.
Common mistakes
- Running Step 4 (thresholding) before Step 3 (contrast stretching). The constant C then has to be re-tuned for every photograph's own washed-out range, instead of using one stable, once-chosen value.
- Reaching for Section 1's global threshold or Section 2's Otsu's method at Step 4 out of habit. Neither one survives the uneven lighting Chapter 6 §9 explicitly named, which is the whole point of this recipe.
- Skipping Step 5's cleanup and treating Step 4's raw output as the final scan. Section 8's own connected-component count on an uncleaned mask will include every leftover speckle as its own spurious object.
- Presenting the result as a perfect scan, rather than naming Step 6's own honest limitation. A lighting transition, not just a uniformly dark region, still trips up even adaptive thresholding's local window.