title: Automatic contrast and brightness adjustment of a color photo of a sheet of paper with OpenCV
body: <p>When photographing a sheet of paper (e.g. with phone camera), I get the following result (left image) (jpg download <a href="https://i.stack.imgur.com/mY7ep.jpg" rel="noreferrer">here</a>). The desired result (processed manually with an image editing software) is on the right:</p>
  
  <p><img src="https://i.stack.imgur.com/mY7epm.jpg" alt="">
  <img src="https://i.stack.imgur.com/se8hCm.jpg" alt=""></p>
  
  <p><strong>I would like to process the original image with openCV to get a better brightness/contrast <em>automatically</em> (so that the background is more white)</strong>.</p>
  
  <p>Assumption: the image has an A4 portrait format (we don't need to perspective-warp it in this topic here), and the sheet of paper is white with possibly text/images in black or colors.</p>
  
  <p>What I've tried so far:</p>
  
  <ol>
  <li><p>Various <strong>adaptive thresholding</strong> methods such as Gaussian, OTSU (see OpenCV doc <a href="https://docs.opencv.org/3.4.0/d7/d4d/tutorial_py_thresholding.html" rel="noreferrer">Image Thresholding</a>). It usually works well with OTSU:</p>
  
  <pre><code>ret, gray = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY)
  </code></pre>
  
  <p><strong>but it only works for grayscale images</strong> and not directly for color images. Moreover, <strong>the output is binary (white or black), which I don't want</strong>: I prefer to keep a color non-binary image as output</p></li>
  <li><p><a href="https://en.wikipedia.org/wiki/Histogram_equalization" rel="noreferrer">Histogram equalization</a> </p>
  
  <ul>
  <li>applied on Y (after RGB => YUV transform) </li>
  <li>or applied on V (after RGB => HSV transform),</li>
  </ul>
  
  <p>as suggested by this <a href="https://stackoverflow.com/a/15009815/1422096">answer</a> (<a href="https://stackoverflow.com/questions/15007304/histogram-equalization-not-working-on-color-image-opencv">Histogram equalization not working on color image - OpenCV</a>) or this <a href="https://stackoverflow.com/a/38312281/1422096">one</a> (<a href="https://stackoverflow.com/questions/31998428/opencv-python-equalizehist-colored-image">OpenCV Python equalizeHist colored image</a>):</p>
  
  <pre><code>img3 = cv2.imread(f)
  img_transf = cv2.cvtColor(img3, cv2.COLOR_BGR2YUV)
  img_transf[:,:,0] = cv2.equalizeHist(img_transf[:,:,0])
  img4 = cv2.cvtColor(img_transf, cv2.COLOR_YUV2BGR)
  cv2.imwrite('test.jpg', img4)
  </code></pre>
  
  <p>or with HSV:</p>
  
  <pre><code>img_transf = cv2.cvtColor(img3, cv2.COLOR_BGR2HSV)
  img_transf[:,:,2] = cv2.equalizeHist(img_transf[:,:,2])
  img4 = cv2.cvtColor(img_transf, cv2.COLOR_HSV2BGR)
  </code></pre>
  
  <p>Unfortunately, the result is quite bad since it creates awful micro contrasts locally (?):</p>
  
  <p><img src="https://i.stack.imgur.com/sfOJdm.jpg" alt=""></p>
  
  <p>I also tried YCbCr instead, and it was similar.</p></li>
  <li><p>I also tried <a href="https://docs.opencv.org/3.1.0/d5/daf/tutorial_py_histogram_equalization.html" rel="noreferrer">CLAHE (Contrast Limited Adaptive Histogram Equalization)</a> with various <code>tileGridSize</code> from <code>1</code> to <code>1000</code>:</p>
  
  <pre><code>img3 = cv2.imread(f)
  img_transf = cv2.cvtColor(img3, cv2.COLOR_BGR2HSV)
  clahe = cv2.createCLAHE(tileGridSize=(100,100))
  img_transf[:,:,2] = clahe.apply(img_transf[:,:,2])
  img4 = cv2.cvtColor(img_transf, cv2.COLOR_HSV2BGR)
  cv2.imwrite('test.jpg', img4)
  </code></pre>
  
  <p>but the result was equally awful too.</p></li>
  <li><p>Doing this CLAHE method with LAB color space, as suggested in the question <a href="https://stackoverflow.com/questions/25008458/how-to-apply-clahe-on-rgb-color-images">How to apply CLAHE on RGB color images</a>:</p>
  
  <pre><code>import cv2, numpy as np
  bgr = cv2.imread('_example.jpg')
  lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB)
  lab_planes = cv2.split(lab)
  clahe = cv2.createCLAHE(clipLimit=2.0,tileGridSize=(100,100))
  lab_planes[0] = clahe.apply(lab_planes[0])
  lab = cv2.merge(lab_planes)
  bgr = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
  cv2.imwrite('_example111.jpg', bgr)
  </code></pre>
  
  <p>gave bad result too. Output image:</p>
  
  <p><img src="https://i.stack.imgur.com/fi7Bcm.jpg" alt=""></p></li>
  <li><p>Do an adaptive thresholding or histogram equalization <strong>separately on each channel</strong> (R, G, B) is not an option since it would mess with the color balance, as explained <a href="https://stackoverflow.com/questions/15007304/histogram-equalization-not-working-on-color-image-opencv/15009815">here</a>.</p></li>
  <li><p><strong>"Contrast strechting"</strong> method from <code>scikit-image</code>'s tutorial on <a href="https://scikit-image.org/docs/dev/auto_examples/color_exposure/plot_equalize.html" rel="noreferrer">Histogram Equalization</a>:</p>
  
  <blockquote>
    <p>the image is rescaled to include all intensities that fall within the 2nd and 98th percentiles</p>
  </blockquote>
  
  <p>is a little bit better, but still far from the desired result (see image on top of this question).</p></li>
  </ol>
  
  <hr>
  
  <p><strong>TL;DR: how to get an automatic brightness/contrast optimization of a color photo of a sheet of paper with OpenCV/Python?</strong> What kind of thresholding/histogram equalization/other technique could be used?</p>