OpenCV2
imwrite
black image
computer vision
image processing

OpenCV2 imwrite is writing a black image

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When cv2.imwrite() saves a completely black image, the problem is almost always that the pixel values in your array are effectively zero. The most common causes are: the image failed to load (returning None or an empty array), the pixel values are floating-point numbers in the 0.0–1.0 range that get truncated to 0 when saved as 8-bit integers, the image was processed with operations that zeroed out the data, or the array dtype is wrong. Debugging involves checking the array shape, dtype, and value range before calling imwrite.

Check If the Image Loaded

python
1import cv2
2
3img = cv2.imread('photo.jpg')
4
5# imread returns None if the file doesn't exist or can't be decoded
6if img is None:
7    print("Failed to load image!")
8else:
9    print(f"Shape: {img.shape}, dtype: {img.dtype}")
10    cv2.imwrite('output.jpg', img)
11
12# Common path issues
13img = cv2.imread('~/photos/test.jpg')     # BROKEN — OpenCV doesn't expand ~
14img = cv2.imread('/home/user/photos/test.jpg')  # FIX — use absolute path
15
16import os
17img = cv2.imread(os.path.expanduser('~/photos/test.jpg'))  # Also works

cv2.imread silently returns None on failure — no exception, no warning. Always check the return value.

Float Images (0.0–1.0 Range)

python
1import cv2
2import numpy as np
3
4img = cv2.imread('photo.jpg')
5# img.dtype is uint8, values 0-255
6
7# Convert to float for processing
8img_float = img.astype(np.float64) / 255.0
9# img_float values are 0.0 to 1.0
10
11# BROKEN — imwrite truncates float 0.0-1.0 to integer 0
12cv2.imwrite('output.jpg', img_float)
13# Result: completely black image
14
15# FIX — convert back to uint8 before saving
16cv2.imwrite('output.jpg', (img_float * 255).astype(np.uint8))
17
18# FIX — or use np.clip to handle overflow
19result = np.clip(img_float * 255, 0, 255).astype(np.uint8)
20cv2.imwrite('output.jpg', result)

imwrite expects uint8 (0–255) or uint16 (0–65535) arrays. Float values between 0.0 and 1.0 are truncated to 0 (black).

Debugging the Array

python
1import cv2
2import numpy as np
3
4img = cv2.imread('photo.jpg')
5
6# Always check these before imwrite
7print(f"Type: {type(img)}")           # Should be numpy.ndarray
8print(f"Shape: {img.shape}")          # (height, width, channels)
9print(f"Dtype: {img.dtype}")          # Should be uint8
10print(f"Min: {img.min()}")            # Should be > 0 for non-black
11print(f"Max: {img.max()}")            # Should be > 0
12print(f"Mean: {img.mean():.2f}")      # Rough brightness check
13
14# If max is 0 — your image is all black
15# If dtype is float and max is ~1.0 — need to multiply by 255
16# If shape has 0 in any dimension — empty array

Normalization Issues

python
1import cv2
2import numpy as np
3
4img = cv2.imread('photo.jpg', cv2.IMREAD_GRAYSCALE)
5
6# After some processing, values might be outside 0-255
7processed = img.astype(np.float64)
8processed = processed - processed.mean()  # Now has negative values
9# Min might be -128, Max might be +127
10
11# BROKEN — negative values become 0, >255 wraps around
12cv2.imwrite('output.jpg', processed)
13
14# FIX — normalize to 0-255 range
15normalized = cv2.normalize(processed, None, 0, 255, cv2.NORM_MINMAX)
16cv2.imwrite('output.jpg', normalized.astype(np.uint8))
17
18# FIX — manual normalization
19p_min, p_max = processed.min(), processed.max()
20if p_max > p_min:
21    normalized = ((processed - p_min) / (p_max - p_min) * 255).astype(np.uint8)
22    cv2.imwrite('output.jpg', normalized)

Color Space Issues

python
1import cv2
2import numpy as np
3
4# OpenCV uses BGR by default, not RGB
5img_bgr = cv2.imread('photo.jpg')  # BGR order
6
7# If you got the image from PIL/matplotlib (RGB order)
8from PIL import Image
9img_pil = np.array(Image.open('photo.jpg'))  # RGB order
10
11# Saving RGB image with imwrite — colors are swapped, not black
12# but sometimes channel operations cause black output
13cv2.imwrite('output.jpg', img_pil)  # Colors wrong
14
15# FIX — convert RGB to BGR
16cv2.imwrite('output.jpg', cv2.cvtColor(img_pil, cv2.COLOR_RGB2BGR))
17
18# Grayscale: make sure the shape is (H, W), not (H, W, 1)
19gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
20print(gray.shape)  # (480, 640) — correct for grayscale
21cv2.imwrite('gray.jpg', gray)

Incorrect Operations Zeroing the Image

python
1import cv2
2import numpy as np
3
4img = cv2.imread('photo.jpg')
5
6# Threshold that's too high — everything becomes 0
7_, thresh = cv2.threshold(img, 254, 255, cv2.THRESH_BINARY)
8# If no pixels are above 254, result is all black
9
10# Mask with wrong dimensions
11mask = np.zeros(img.shape[:2], dtype=np.uint8)
12# Forgot to draw on the mask — it's all zeros
13result = cv2.bitwise_and(img, img, mask=mask)
14# Result: all black (mask blocks everything)
15
16# FIX — verify mask has non-zero pixels
17print(f"Mask non-zero pixels: {cv2.countNonZero(mask)}")

Common Pitfalls

  • Not checking imread return value: cv2.imread returns None when the file path is wrong, the file is corrupted, or the codec is missing. Passing None to imwrite raises an error, but passing an empty or zeroed array writes a black image silently. Always check if img is None immediately after imread.
  • Saving float images without converting to uint8: Processing pipelines often convert images to float32/float64 with values in 0.0–1.0. imwrite treats these as integer values, truncating 0.7 to 0. Always multiply by 255 and cast to uint8 before saving.
  • Using tilde (~) in file paths: OpenCV does not expand ~ to the home directory. cv2.imread('~/photo.jpg') fails silently and returns None. Use os.path.expanduser() or provide the full absolute path.
  • Operations producing out-of-range values: Arithmetic on uint8 arrays wraps around (255 + 1 = 0). Mathematical operations can produce negative values or values above 255. Use np.clip() or cv2.normalize() to bring values back into the valid range before saving.
  • Empty mask in bitwise operations: cv2.bitwise_and(img, img, mask=mask) with an all-zero mask produces an all-black result. Verify your mask has non-zero pixels with cv2.countNonZero(mask) before applying it.

Summary

  • Check if img is None after every cv2.imread call — it fails silently
  • Print shape, dtype, min, and max of your array before calling imwrite to diagnose issues
  • Convert float images (0.0–1.0) back to uint8 (0–255) with (img * 255).astype(np.uint8)
  • Use cv2.normalize() for arrays with values outside the 0–255 range
  • Verify masks have non-zero pixels and paths are absolute (no ~ expansion)

Course illustration
Course illustration

All Rights Reserved.