Fixing Bad Light
Seeing in the dark corners.
It's five o'clock in winter. The sun has dropped, and the far end of the sorting shed has gone dim. AppleEye starts to miss things. Here's a truth that every vision engineer learns the hard way. A better image beats a better model. Before you touch any neural network, make the picture as good as it can be.

- Five o'clock in winter
- The shed goes dim
- A better image beats a better model
Stretching the histogram
We take a photo with only a third of the normal light. The average pixel is about twenty seven out of two hundred and fifty five, which is very dark. Equalize hist spreads the brightness across the full range. But it does that for the entire image at once. Clahe does it in small tiles, and it limits how far each tile can stretch, so that it doesn't blow up the noise. Run it. Global equalisation jumps to one hundred and seventy one, and it turns the belt texture into a harsh glare. Clahe stays gentle, at thirty three, and brings out local detail, without inventing light that wasn't there. For uneven lighting, clahe is nearly always the better choice.
import cv2
import numpy as np
from belt import belt
img, _, _ = belt(light=0.35)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
flat = cv2.equalizeHist(gray)
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)).apply(gray)
print("Average brightness:", int(gray.mean()), "->", int(flat.mean()), "and", int(clahe.mean()))
cv2.imwrite("light.png", np.hstack([gray, flat, clahe]))Average brightness: 27 -> 171 and 33
Also uses belt.py
import numpy as np
import cv2
def belt(n=7, seed=1, size=(480, 640), bruises=2, light=1.0):
"""Draw a simulated conveyor belt of apples. Returns the image and the true boxes."""
rng = np.random.default_rng(seed)
h, w = size
img = np.full((h, w, 3), (70, 75, 80), np.uint8)
img = cv2.add(img, rng.normal(0, 6, img.shape).clip(0, 30).astype(np.uint8))
boxes, spots, taken = [], [], []
while len(boxes) < n:
r = int(rng.integers(32, 48))
x, y = int(rng.integers(r + 5, w - r - 5)), int(rng.integers(r + 5, h - r - 5))
if any((x - a) ** 2 + (y - b) ** 2 < (r + c + 8) ** 2 for a, b, c in taken):
continue
taken.append((x, y, r))
red = int(rng.integers(150, 225))
cv2.circle(img, (x, y), r, (35, 40, red), -1, cv2.LINE_AA)
cv2.circle(img, (x - r // 3, y - r // 3), r // 4, (90, 90, min(255, red + 30)), -1, cv2.LINE_AA)
boxes.append((x - r, y - r, x + r, y + r))
for x, y, r in taken[:bruises]:
cv2.ellipse(img, (x + r // 3, y + r // 4), (r // 3, r // 4), 20, 0, 360, (25, 45, 70), -1, cv2.LINE_AA)
spots.append((x, y, r))
img = cv2.GaussianBlur(img, (3, 3), 0)
return np.clip(img * light, 0, 255).astype(np.uint8), boxes, spotsWhen to use what
So which should you reach for? Global equalisation suits a scan of a document, where everything's equally faded. Clahe suits scenes with shadows, such as medical scans and factory lines. Gamma correction brightens dark tones, without touching bright ones. But remember that none of this adds information. If a sensor captured nothing, then software can't bring it back. The real fix is usually a better lamp.
Summary
Stretch contrast locally, and fix the light if you can. Next, the most common problem of all on a fast belt. Blur.
Key takeaways- Better images beat better models
- CLAHE handles uneven light
- Software can't add information