HardnessTester API Reference¶
The HardnessTester class provides segment-based reading of 7-segment LCD displays. Instead of using traditional OCR, it detects which segments are active in each digit and matches the pattern against a lookup table.
Overview¶
HardnessTester is designed for:
- Reading 7-segment LCD displays with low contrast
- Detecting active segments using image processing
- Recognizing digits via pattern matching
- Calibrating digit positions for accurate reading
- Working without OCR dependencies
This approach is more reliable than OCR for LCD displays because it's not affected by font variations, requires no training data, and works with low-contrast displays.
Class Reference¶
Constructor¶
Initialize the LCD reader.
Parameters:
num_digits(int, optional): Number of digits in the display. Default: 4cam_id(int, optional): Camera device ID. Default: 0exposure_time(int, optional): Fixed exposure time in microseconds (Picamera2 only). None = auto. Default: Nonegain(float, optional): Fixed gain value (Picamera2 only). None = auto. Default: None
Returns: HardnessTester instance
Example:
# Basic initialization
reader = HardnessTester(num_digits=4)
# With locked camera settings (Raspberry Pi)
reader = HardnessTester(
num_digits=4,
exposure_time=10000,
gain=1.0
)
Class Attributes¶
DIGITS_LOOKUP¶
Lookup table mapping 7-bit segment patterns to digit strings.
Pattern order: (top, top_left, top_right, middle, bottom_left, bottom_right, bottom)
Example:
# Pattern for digit '0': all segments except middle
(1, 1, 1, 0, 1, 1, 1): '0'
# Pattern for digit '1': only right segments
(0, 0, 1, 0, 0, 1, 0): '1'
Core Methods¶
capture_image¶
Capture an image from the camera.
Parameters:
save(bool, optional): Whether to save the captured image. Default: Falseoutput_path(str, optional): Path to save the image. Default: 'lcd_capture.jpg'
Returns: BGR numpy array or None if capture failed
Example:
frame = reader.capture_image(save=True, output_path='test.jpg')
if frame is not None:
print("Image captured successfully")
preprocess_frame¶
Phase 1: Image Acquisition & Advanced Preprocessing
Converts frame to LAB color space, extracts b-channel, and applies CLAHE to enhance LCD segment contrast.
Parameters:
frame(np.ndarray): Input BGR frame from cameradebug(bool, optional): Whether to save debug images. Default: Falsedebug_prefix(str, optional): Prefix for debug image filenames. Default: "debug"
Returns: Binary image with enhanced LCD segments
Raises: ValueError if input frame is None
Example:
frame = reader.capture_image()
binary = reader.preprocess_frame(frame, debug=True, debug_prefix="test")
Debug Output:
When debug=True, saves:
- {prefix}_step1_original.png - Original BGR frame
- {prefix}_step2_lab.png - LAB color space conversion
- {prefix}_step3_b_channel.png - Extracted b-channel
- {prefix}_step4_clahe.png - After CLAHE enhancement
- {prefix}_step5_binary.png - Binary thresholded image
- {prefix}_step6_cleaned.png - After morphological cleaning
read_display¶
Complete pipeline: Read all digits from LCD display.
Parameters:
frame(np.ndarray, optional): Input BGR frame. If None, captures from camera. Default: Nonedebug(bool, optional): Whether to save debug images. Default: Falsedebug_prefix(str, optional): Prefix for debug image filenames. Default: "debug"
Returns: String with recognized digits (e.g., "1234") or None if reading failed
Example:
# Capture and read in one call
result = reader.read_display()
print(f"LCD shows: {result}")
# Use existing frame
frame = reader.capture_image()
result = reader.read_display(frame=frame, debug=True)
# Check for errors
if result is None:
print("Reading failed")
elif '?' in result:
print(f"Unclear reading: {result}")
else:
print(f"Valid reading: {result}")
Calibration Methods¶
calibrate¶
Interactive calibration to manually set digit ROIs.
Parameters:
frame(np.ndarray, optional): Input BGR frame. If None, captures from camera. Default: Nonesave_calibration(bool, optional): Whether to save calibration to file. Default: True
Returns: True if calibration successful
Example:
reader = HardnessTester(num_digits=4)
frame = reader.capture_image(save=True)
reader.calibrate(frame=frame)
Calibration Process:
- Preprocesses frame and saves debug images
- Prompts for pixel coordinates of each digit (x1, y1, x2, y2)
- Saves calibration to
lcd_calibration.json - Tests the calibration immediately
load_calibration¶
Load calibration from file.
Parameters:
filepath(str, optional): Path to calibration JSON file. Default: 'lcd_calibration.json'
Returns: True if loaded successfully
Example:
if reader.load_calibration('lcd_calibration.json'):
print("Calibration loaded")
else:
print("Failed to load calibration")
Calibration File Format:
set_digit_rois¶
Set the ROI boundaries for each digit.
Parameters:
rois(List[Tuple[int, int, int, int]]): List of tuples[(x1, y1, x2, y2), ...]for each digit
Raises: ValueError if number of ROIs doesn't match num_digits
Example:
# Manually set digit ROIs
rois = [
(50, 100, 90, 180), # Digit 0
(100, 100, 140, 180), # Digit 1
(150, 100, 190, 180), # Digit 2
(200, 100, 240, 180) # Digit 3
]
reader.set_digit_rois(rois)
auto_detect_digit_rois¶
Attempt to automatically detect digit ROIs using contour detection.
Parameters:
binary_frame(np.ndarray): Preprocessed binary image
Returns: List of digit ROIs [(x1, y1, x2, y2), ...] or None if detection failed
Example:
frame = reader.capture_image()
binary = reader.preprocess_frame(frame)
rois = reader.auto_detect_digit_rois(binary)
if rois:
reader.set_digit_rois(rois)
print(f"Detected {len(rois)} digit ROIs")
else:
print("Auto-detection failed - use manual calibration")
Note: Auto-detection works best with clear digit separation and good contrast. Manual calibration is more reliable.
Digit Analysis Methods¶
extract_digit_roi¶
Extract a single digit ROI from the binary frame.
Parameters:
binary_frame(np.ndarray): Preprocessed binary imagedigit_idx(int): Index of the digit to extract
Returns: Cropped digit image or None
Example:
analyze_segment¶
Phase 3: Segment Analysis Logic
Analyzes a single segment within a digit ROI to determine if it's active.
Parameters:
digit_roi(np.ndarray): Binary image of a single digitsegment_name(str): Name of the segment ('top', 'top_left', 'top_right', 'middle', 'bottom_left', 'bottom_right', 'bottom')
Returns: 1 if segment is active (ON), 0 if inactive (OFF)
Example:
digit_roi = reader.extract_digit_roi(binary, 0)
top_active = reader.analyze_segment(digit_roi, 'top')
middle_active = reader.analyze_segment(digit_roi, 'middle')
recognize_digit¶
Phase 4: Recognition via Lookup Table
Recognizes a single digit by analyzing all 7 segments.
Parameters:
digit_roi(np.ndarray): Binary image of a single digitdebug(bool, optional): Whether to print debug information. Default: False
Returns: Recognized digit as string or '?' if not recognized
Example:
digit_roi = reader.extract_digit_roi(binary, 0)
digit = reader.recognize_digit(digit_roi, debug=True)
print(f"Recognized: {digit}")
Debug Output:
When debug=True, prints:
Instance Attributes¶
num_digits¶
Number of digits in the display.
digit_rois¶
ROI boundaries for each digit. Format: [(x1, y1, x2, y2), ...]
Set via calibration, load_calibration(), or set_digit_rois().
segment_rois¶
Segment ROI boundaries relative to digit ROI. Proportions (0.0 to 1.0).
Default values:
{
'top': (0.2, 0.0, 0.8, 0.2),
'top_left': (0.0, 0.0, 0.3, 0.5),
'top_right': (0.7, 0.0, 1.0, 0.5),
'middle': (0.2, 0.4, 0.8, 0.6),
'bottom_left': (0.0, 0.5, 0.3, 1.0),
'bottom_right': (0.7, 0.5, 1.0, 1.0),
'bottom': (0.2, 0.8, 0.8, 1.0),
}
Can be customized for different display layouts.
segment_threshold¶
Threshold for segment detection (proportion of pixels that must be active). Default: 0.5
Adjust for sensitivity: - Lower (0.3): More sensitive, detects dimmer segments - Higher (0.7): Less sensitive, only bright segments
Example:
picamera / cv2_camera¶
Camera objects (internal use). One will be initialized based on availability.
Usage Examples¶
Basic Reading with Calibration¶
from src.HardnessTester import HardnessTester
# Initialize and load calibration
reader = HardnessTester(num_digits=4)
reader.load_calibration('lcd_calibration.json')
# Read display
result = reader.read_display()
print(f"LCD reading: {result}")
First-Time Setup¶
from src.HardnessTester import HardnessTester
# Initialize
reader = HardnessTester(num_digits=4)
# Capture test image
frame = reader.capture_image(save=True, output_path='calibration.jpg')
# Run calibration
reader.calibrate(frame=frame, save_calibration=True)
# Now ready for production use
result = reader.read_display()
Reading from Static Image¶
from src.HardnessTester import test_with_image
import cv2
# Using helper function
result = test_with_image('lcd_photo.jpg', 'lcd_calibration.json')
# Or manually
reader = HardnessTester(num_digits=4)
reader.load_calibration('lcd_calibration.json')
frame = cv2.imread('lcd_photo.jpg')
result = reader.read_display(frame=frame)
Advanced Debugging¶
reader = HardnessTester(num_digits=4)
reader.load_calibration('lcd_calibration.json')
# Capture and preprocess
frame = reader.capture_image()
binary = reader.preprocess_frame(frame, debug=True, debug_prefix="debug")
# Analyze each digit
for i in range(reader.num_digits):
digit_roi = reader.extract_digit_roi(binary, i)
# Check each segment
segments = {
'top': reader.analyze_segment(digit_roi, 'top'),
'top_left': reader.analyze_segment(digit_roi, 'top_left'),
'top_right': reader.analyze_segment(digit_roi, 'top_right'),
'middle': reader.analyze_segment(digit_roi, 'middle'),
'bottom_left': reader.analyze_segment(digit_roi, 'bottom_left'),
'bottom_right': reader.analyze_segment(digit_roi, 'bottom_right'),
'bottom': reader.analyze_segment(digit_roi, 'bottom'),
}
# Recognize
digit = reader.recognize_digit(digit_roi)
print(f"Digit {i}: {segments} → {digit}")
Adjusting for Different Displays¶
reader = HardnessTester(num_digits=4)
# Custom segment positions (if default doesn't work)
reader.segment_rois = {
'top': (0.15, 0.0, 0.85, 0.15),
'top_left': (0.0, 0.0, 0.25, 0.45),
'top_right': (0.75, 0.0, 1.0, 0.45),
'middle': (0.15, 0.45, 0.85, 0.55),
'bottom_left': (0.0, 0.55, 0.25, 1.0),
'bottom_right': (0.75, 0.55, 1.0, 1.0),
'bottom': (0.15, 0.85, 0.85, 1.0),
}
# Adjust sensitivity
reader.segment_threshold = 0.4
# Load calibration
reader.load_calibration('lcd_calibration.json')
Helper Functions¶
test_with_image¶
Simple test function to read LCD from a single image.
Parameters:
image_path(str): Path to LCD imagecalibration_file(str, optional): Path to calibration file. Default: 'lcd_calibration.json'
Returns: String with recognized digits or None
Example:
from src.HardnessTester import test_with_image
result = test_with_image('lcd_image.jpg')
print(f"Result: {result}")
Design Notes¶
Why Not OCR?¶
Traditional OCR (Tesseract, TrOCR, EasyOCR) struggles with: - Low-contrast LCD displays - Varying lighting conditions - Font variations in 7-segment displays - Hollow or outlined digits
Segment-based detection advantages:
- Specifically designed for 7-segment displays
- More reliable with low contrast
- No training data required
- Fast and lightweight (no ML models)
- Easy to debug (visual segment patterns)
Preprocessing Strategy¶
The preprocessing pipeline is optimized for LCD displays:
- LAB Color Space: The b-channel (blue-yellow axis) provides best contrast for LCD segments with greenish or gray backgrounds
- CLAHE: Enhances contrast in local 8×8 tiles rather than globally, handling shadows and glare
- Adaptive Thresholding: Otsu's method automatically finds the best threshold value
Segment Detection¶
Each segment is detected by:
1. Extracting segment ROI (proportional to digit size)
2. Counting white pixels with cv2.countNonZero()
3. Comparing ratio to threshold (default 50%)
This is more robust than edge detection or contour analysis.
Calibration Trade-offs¶
Manual Calibration:
- More accurate
- Works with any display layout
- Requires user input
- One-time setup per display
Auto-Detection:
- No user input
- Quick setup
- May fail with complex layouts
- Less accurate
Recommendation: Use auto-detection for testing, manual calibration for production.
Limitations¶
Current implementation supports:
- Recognizes digits 0-9
Current implementation does not support:
- Decimal point detection
- Negative sign detection
- Unit symbols
- 14-segment or dot matrix displays
Workarounds: - Decimal points: Know position and post-process (e.g., "1234" → 12.34) - Negative signs: Check separate ROI or use additional processing - Units: Read separately or ignore
Performance¶
Typical performance on Raspberry Pi 4: - Initialization: < 1 second - Calibration: 5-10 seconds (one-time) - Single reading: 100-200ms - With debug output: 300-500ms
On desktop PC: - Single reading: 20-50ms
See Also¶
- Reading LCD Displays Guide - User-facing tutorial
- Segment Layout Guide - Technical segment details
- Architecture Overview - System design