如何正确修改图像饱和度与明度通道?解决Python肤色调整异常
Let's break down exactly why you're seeing those weird black/red pixels and red overflow, then fix it with practical, artifact-free solutions.
Root Cause of the Issues
Your current code modifies sat and val directly as 8-bit unsigned integers (uint8) without restricting values to the valid [0, 255] range. Here's what's happening:
- When you do
sat[sat>0]+=60, any saturation value above 195 will exceed 255. Sinceuint8wraps around (e.g., 250 + 10 = 4), this creates garbage pixel values like red overflow. val[val<255]-=100makes values below 100 wrap to negative-equivalentuint8values (e.g., 50 - 100 = 206), causing those black outline artifacts.
Solution 1: Clamp Values to Valid Range
First, convert your HSV channels to a higher-bit integer type (like int32) before modifying them, then clamp results back to [0, 255] and convert back to uint8. This eliminates overflow entirely.
Here's the fixed core adjustment code:
import cv2 import numpy as np import matplotlib.pyplot as plt import os for img in list_im: ori_image = cv2.imread(img) # Convert to HSV_FULL (H mapped 0-360 → 0-255; S/V 0-255) hsv_image = cv2.cvtColor(ori_image, cv2.COLOR_BGR2HSV_FULL) # Split channels and upgrade to int32 to avoid overflow h = hsv_image[:, :, 0].astype(np.int32) sat = hsv_image[:, :, 1].astype(np.int32) val = hsv_image[:, :, 2].astype(np.int32) # Adjust with clamping (choose one option) # Option A: Fixed offset (your original intent) sat = np.clip(sat + 60, 0, 255) # Lock to 0-255 val = np.clip(val - 100, 0, 255) # Option B: Proportional adjustment (more natural across images) # sat = np.clip(sat * 1.6, 0, 255) ~60% saturation boost # val = np.clip(val * 0.6, 0, 255) ~40% brightness reduction # Merge channels and convert back to uint8 adjusted_hsv = np.dstack((h, sat, val)).astype(np.uint8) im = cv2.cvtColor(adjusted_hsv, cv2.COLOR_HSV2RGB_FULL) # Your existing plotting code (minor cleanup) fig = plt.figure(figsize=(18.5, 10.5)) ax1 = fig.add_subplot(5, 1, 1) ax1.imshow(cv2.cvtColor(ori_image, cv2.COLOR_BGR2RGB)) ax1.set_title('Original') ax2 = fig.add_subplot(5, 1, 2) ax2.imshow(im) ax2.set_title('Artifact-Free Adjustment') plt.subplots_adjust(top=2) save_path = os.path.join('/home/cgal/color_harmonization/', 'Result_saturation', os.path.basename(img)) plt.savefig(save_path, bbox_inches="tight") plt.show()
Solution 2: Apply Adjustments Only to Masked Areas
Your mask function is great for excluding eyes/mouth, but you need to ensure adjustments only affect the jawline region. Here's how to integrate the mask properly:
First, update your mask function to return a 3-channel mask (matches color image dimensions):
from PIL import Image, ImageDraw def getmask(img, jawline, eyes, mouth): height, width = img.shape[:2] # Create mask (white = apply adjustment; black = exclude) mask_im = Image.new('L', (width, height), 0) draw = ImageDraw.Draw(mask_im) # Draw jawline (target area) jawline_poly = jawline.flatten().tolist() draw.polygon(jawline_poly, fill='white') # Draw eyes (exclude) right_eyes = eyes[0:6].flatten().tolist() draw.polygon(right_eyes, fill='black') left_eyes = eyes[6:].flatten().tolist() draw.polygon(left_eyes, fill='black') # Draw mouth (exclude) mouth_poly = mouth.flatten().tolist() draw.polygon(mouth_poly, fill='black') # Convert to 3-channel normalized mask (0-1 range) mask = np.array(mask_im) mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2) / 255.0 return mask_3ch
Then, integrate it into your main workflow to apply adjustments selectively:
for img in list_im: ori_image = cv2.imread(img) ori_rgb = cv2.cvtColor(ori_image, cv2.COLOR_BGR2RGB) hsv_image = cv2.cvtColor(ori_image, cv2.COLOR_BGR2HSV_FULL) # Get mask (replace jawline/eyes/mouth with your actual landmark data) mask_3ch = getmask(ori_image, jawline, eyes, mouth) # Adjust HSV channels with clamping h = hsv_image[:, :, 0].astype(np.int32) sat = hsv_image[:, :, 1].astype(np.int32) val = hsv_image[:, :, 2].astype(np.int32) sat_adjusted = np.clip(sat + 60, 0, 255) val_adjusted = np.clip(val - 100, 0, 255) # Convert back to RGB and apply mask adjusted_hsv = np.dstack((h, sat_adjusted, val_adjusted)).astype(np.uint8) adjusted_rgb = cv2.cvtColor(adjusted_hsv, cv2.COLOR_HSV2RGB_FULL) # Blend original and adjusted image using mask final_image = (ori_rgb * (1 - mask_3ch) + adjusted_rgb * mask_3ch).astype(np.uint8) # Plotting code fig = plt.figure(figsize=(18.5, 10.5)) ax1 = fig.add_subplot(5, 1, 1) ax1.imshow(ori_rgb) ax1.set_title('Original') ax2 = fig.add_subplot(5, 1, 2) ax2.imshow(final_image) ax2.set_title('Masked Artifact-Free Adjustment') plt.subplots_adjust(top=2) save_path = os.path.join('/home/cgal/color_harmonization/', 'Result_saturation', os.path.basename(img)) plt.savefig(save_path, bbox_inches="tight") plt.show()
Key Takeaways
- Always clamp values: 8-bit image channels can never go outside [0,255]. Using
np.clipor upgrading to a higher-bit type first eliminates overflow artifacts. - Proportional adjustments are better: Multiplying saturation/value by a factor (e.g.,
sat * 1.6) gives more consistent, natural results across different images than fixed offsets. - Align masks correctly: Ensure your mask matches the image dimensions, and use 3-channel masks for color images to avoid channel mismatches.
内容的提问来源于stack exchange,提问作者Wanttobepro

