Brightness, Invert & Threshold
Pixel-value edits with vectorised maths: brightness and contrast with np.clip, avoiding uint8 overflow, inverting (negatives), normalising and thresholding to black-and-white.
Watch the lesson
1 video on CodeWithMunnaX
The previous topic moved pixels around. This one changes the values of pixels: make a dark night-match photo brighter, create a negative, or turn a scanned scoresheet into pure black-and-white. Every one of these is a single vectorised expression — the operation is applied to every pixel at once.
Definition
Brightness adjustment adds (or subtracts) a constant to every pixel; contrast adjustment multiplies the distance of every pixel from mid-grey. Results must be clipped back into the valid range 0–255.
Inverting (a negative image) replaces every value
vwith255 − v, so black becomes white and vice-versa.
Thresholding converts a grayscale image into a binary image: pixels above a threshold
Tbecome 255 (white), all others 0 (black).
- Clipping —
np.clip(a, 0, 255)forces values below 0 up to 0 and above 255 down to 255. - uint8 overflow (wrap-around) — uint8 can only hold 0–255.
250 + 10does not become 260; it wraps to260 − 256 = 4. This silently ruins images. - Normalising — dividing by 255 maps pixels to floats in
[0, 1], the format most machine-learning models expect. - Mask — the boolean array
img > Titself; useful for counting or selecting pixels.
Syntax
python
np.clip(a, a_min, a_max)
np.where(condition, x, y)
255 - img # invert (uint8)
img.astype(np.int16) # widen the type before risky maths| Function | Parameter | Meaning | Default |
|---|---|---|---|
np.clip | a | Input array | — required |
np.clip | a_min, a_max | Lowest / highest allowed value (None = no limit) | — required |
np.where | condition | Boolean array | — required |
np.where | x, y | Value where condition is True / False | — required |
.astype | dtype | New type, e.g. np.int16, np.float64, np.uint8 | — required |
Example — brightness: the overflow trap and the fix
python
import numpy as np
img = np.array([[ 10, 80, 150],
[200, 230, 250]], dtype=np.uint8)
print("wrong (wraps around):")
print(img + 50)
brighter = np.clip(img.astype(np.int16) + 50, 0, 255).astype(np.uint8)
print("brighter:")
print(brighter)
darker = np.clip(img.astype(np.int16) - 100, 0, 255).astype(np.uint8)
print("darker:")
print(darker)text
wrong (wraps around):
[[ 60 130 200]
[250 24 44]]
brighter:
[[ 60 130 200]
[250 255 255]]
darker:
[[ 0 0 50]
[100 130 150]]How it works: in the "wrong" result 230 + 50 = 280 wrapped to 24 and 250 + 50 = 300 wrapped to 44 — the brightest pixels turned nearly black! The fix is a 3-step pattern you should memorise:
- Widen the type:
astype(np.int16)can hold −32768…32767, so 280 fits (and negatives too). - Clip into 0–255.
- Narrow back with
astype(np.uint8).
Example — invert (negative)
python
img = np.array([[ 10, 80, 150],
[200, 230, 250]], dtype=np.uint8)
print(255 - img)
print(np.invert(img))text
[[245 175 105]
[ 55 25 5]]
[[245 175 105]
[ 55 25 5]]255 - img can never leave the 0–255 range, so no clipping is needed. For uint8, np.invert (bitwise NOT) gives the same result.
Example — thresholding to black and white
python
img = np.array([[ 12, 200, 45, 180],
[220, 30, 160, 90],
[ 60, 140, 250, 10]], dtype=np.uint8)
mask = img > 127
print(mask)
binary = np.where(img > 127, 255, 0).astype(np.uint8)
print(binary)
print("bright pixels:", mask.sum(), "of", img.size)text
[[False True False True]
[ True False True False]
[False True True False]]
[[ 0 255 0 255]
[255 0 255 0]
[ 0 255 255 0]]
bright pixels: 6 of 12How it works: the comparison makes a boolean mask; np.where paints 255 where it is True and 0 elsewhere. mask.sum() counts Trues — handy for "what fraction of the frame is sky / ball / pitch?". Thresholding is the first step of document scanning, OCR and simple object detection.
Example — contrast and normalising
python
img = np.array([[ 10, 80, 150],
[200, 230, 250]], dtype=np.uint8)
contrast = np.clip((img.astype(np.float64) - 128) * 1.5 + 128, 0, 255).astype(np.uint8)
print(contrast)
norm = img / 255.0
print(norm.round(2))text
[[ 0 56 161]
[236 255 255]]
[[0.04 0.31 0.59]
[0.78 0.9 0.98]]Contrast stretches values away from mid-grey (128): darks get darker, brights brighter. img / 255.0 gives floats in [0, 1] — the standard input for neural networks. (astype(np.uint8) truncates decimals; use .round() first if you need proper rounding.)
Comparison — pixel operations at a glance
| Operation | Code (safe version) | Needs clip? |
|---|---|---|
Brighten by b | np.clip(img.astype(np.int16) + b, 0, 255).astype(np.uint8) | yes |
Darken by b | np.clip(img.astype(np.int16) - b, 0, 255).astype(np.uint8) | yes |
Contrast × c | np.clip((img.astype(float) - 128) * c + 128, 0, 255).astype(np.uint8) | yes |
| Invert | 255 - img | no |
Threshold at T | np.where(img > T, 255, 0).astype(np.uint8) | no |
| Normalise to [0, 1] | img / 255.0 | no |
| Back to 0–255 | (norm * 255).round().astype(np.uint8) | no |
Common mistakes
- ❌
img + 50on uint8 — silent wrap-around. ✅ Widen, clip, narrow. - ❌
np.clip(img + 50, 0, 255)— too late, the overflow already happened insideimg + 50. ✅ Convert toint16/floatbefore adding. - ❌ Writing a double
forloop over every pixel. ✅ One vectorised expression — thousands of times faster on a real 12-megapixel photo. - ❌ Forgetting
.astype(np.uint8)at the end — image libraries expect uint8.
Interview tip
Q: Why does adding 50 to a uint8 image make some bright pixels dark? A: uint8 stores only 0–255 and arithmetic wraps modulo 256 (e.g. 230 + 50 = 280 → 24). Convert to a wider type, add, clip to 0–255, and convert back.
Key points to remember
- uint8 arithmetic wraps around: 230 + 50 becomes 24, silently corrupting images.
- Safe pattern: widen (astype int16/float) → compute → np.clip(…, 0, 255) → astype(np.uint8).
- Inverting is 255 - img; it never leaves the valid range.
- Thresholding: np.where(img > T, 255, 0) turns grayscale into a binary image.
- The boolean mask img > T can be summed to count bright pixels.
- img / 255.0 normalises pixels to [0, 1] for machine learning.
- Every pixel operation is one vectorised expression — never loop over pixels.
Try it yourself
Brightness with and without the overflow bug
Invert and threshold
ASCII art from a threshold
Practice: auto-brighten a dark photo
Revision Flashcards
Tap a card to flip it and see the answer.
