fix: use best-contrast color channel for VIN preprocessing (refs #113)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 36s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 1m7s
Deploy to Staging / Verify Staging (pull_request) Successful in 10s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 9s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 36s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 1m7s
Deploy to Staging / Verify Staging (pull_request) Successful in 10s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 9s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
White text on green VIN stickers has only ~12% contrast in standard grayscale conversion because the green channel dominates luminance. The new _best_contrast_channel method evaluates each RGB channel's standard deviation and selects the one with highest contrast, giving ~2x improvement for green-tinted VIN stickers. Falls back to standard grayscale for neutral-colored images. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -86,9 +86,9 @@ class VinPreprocessor:
|
|||||||
if len(cv_image.shape) == 3:
|
if len(cv_image.shape) == 3:
|
||||||
cv_image = cv2.cvtColor(cv_image, cv2.COLOR_RGB2BGR)
|
cv_image = cv2.cvtColor(cv_image, cv2.COLOR_RGB2BGR)
|
||||||
|
|
||||||
# Convert to grayscale
|
# Convert to grayscale using best-contrast channel selection
|
||||||
if len(cv_image.shape) == 3:
|
if len(cv_image.shape) == 3:
|
||||||
gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
|
gray = self._best_contrast_channel(cv_image)
|
||||||
else:
|
else:
|
||||||
gray = cv_image
|
gray = cv_image
|
||||||
steps_applied.append("grayscale")
|
steps_applied.append("grayscale")
|
||||||
@@ -150,6 +150,46 @@ class VinPreprocessor:
|
|||||||
logger.debug(f"Upscaled image from {width}x{height} to {new_width}x{new_height}")
|
logger.debug(f"Upscaled image from {width}x{height} to {new_width}x{new_height}")
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
def _best_contrast_channel(self, bgr_image: np.ndarray) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Select the single color channel with the highest contrast.
|
||||||
|
|
||||||
|
Standard grayscale conversion (0.299R + 0.587G + 0.114B) averages
|
||||||
|
channels, which destroys contrast when text and background differ
|
||||||
|
primarily in one channel. For example, white text on a green VIN
|
||||||
|
sticker has almost identical luminance, but the blue and red channels
|
||||||
|
show strong contrast.
|
||||||
|
|
||||||
|
This method evaluates each BGR channel by its standard deviation
|
||||||
|
(a proxy for contrast) and returns the one with the highest value.
|
||||||
|
Falls back to standard grayscale when all channels are similar.
|
||||||
|
"""
|
||||||
|
b_channel, g_channel, r_channel = cv2.split(bgr_image)
|
||||||
|
|
||||||
|
stds = [
|
||||||
|
float(np.std(b_channel)),
|
||||||
|
float(np.std(g_channel)),
|
||||||
|
float(np.std(r_channel)),
|
||||||
|
]
|
||||||
|
channels = [b_channel, g_channel, r_channel]
|
||||||
|
channel_names = ["blue", "green", "red"]
|
||||||
|
|
||||||
|
best_idx = int(np.argmax(stds))
|
||||||
|
max_std = stds[best_idx]
|
||||||
|
min_std = min(stds)
|
||||||
|
|
||||||
|
# Only use single-channel extraction when one channel is notably
|
||||||
|
# better (>20% higher std than the weakest). Otherwise, standard
|
||||||
|
# grayscale is fine and more robust for neutral-colored images.
|
||||||
|
if max_std > 0 and (max_std - min_std) / max_std > 0.20:
|
||||||
|
logger.debug(
|
||||||
|
"Using %s channel (std=%.1f) over grayscale (stds: B=%.1f G=%.1f R=%.1f)",
|
||||||
|
channel_names[best_idx], max_std, stds[0], stds[1], stds[2],
|
||||||
|
)
|
||||||
|
return channels[best_idx]
|
||||||
|
|
||||||
|
return cv2.cvtColor(bgr_image, cv2.COLOR_BGR2GRAY)
|
||||||
|
|
||||||
def _apply_clahe(self, image: np.ndarray) -> np.ndarray:
|
def _apply_clahe(self, image: np.ndarray) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Apply CLAHE (Contrast Limited Adaptive Histogram Equalization).
|
Apply CLAHE (Contrast Limited Adaptive Histogram Equalization).
|
||||||
@@ -306,7 +346,7 @@ class VinPreprocessor:
|
|||||||
cv_image = cv2.cvtColor(cv_image, cv2.COLOR_RGB2BGR)
|
cv_image = cv2.cvtColor(cv_image, cv2.COLOR_RGB2BGR)
|
||||||
|
|
||||||
if len(cv_image.shape) == 3:
|
if len(cv_image.shape) == 3:
|
||||||
gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
|
gray = self._best_contrast_channel(cv_image)
|
||||||
else:
|
else:
|
||||||
gray = cv_image
|
gray = cv_image
|
||||||
steps_applied.append("grayscale")
|
steps_applied.append("grayscale")
|
||||||
|
|||||||
Reference in New Issue
Block a user