Forked from HViktorTsoi/ShadowHighlightCorrection.py
Created
September 21, 2023 01:32
-
-
Save zxmarcos/23885f392b2e00088645b05a519c74ac to your computer and use it in GitHub Desktop.
Image shadow and highlight correction/adujstment with opencv.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import numpy as np | |
import cv2 | |
def correction( | |
img, | |
shadow_amount_percent, shadow_tone_percent, shadow_radius, | |
highlight_amount_percent, highlight_tone_percent, highlight_radius, | |
color_percent | |
): | |
""" | |
Image Shadow / Highlight Correction. The same function as it in Photoshop / GIMP | |
:param img: input RGB image numpy array of shape (height, width, 3) | |
:param shadow_amount_percent [0.0 ~ 1.0]: Controls (separately for the highlight and shadow values in the image) how much of a correction to make. | |
:param shadow_tone_percent [0.0 ~ 1.0]: Controls the range of tones in the shadows or highlights that are modified. | |
:param shadow_radius [>0]: Controls the size of the local neighborhood around each pixel | |
:param highlight_amount_percent [0.0 ~ 1.0]: Controls (separately for the highlight and shadow values in the image) how much of a correction to make. | |
:param highlight_tone_percent [0.0 ~ 1.0]: Controls the range of tones in the shadows or highlights that are modified. | |
:param highlight_radius [>0]: Controls the size of the local neighborhood around each pixel | |
:param color_percent [-1.0 ~ 1.0]: | |
:return: | |
""" | |
shadow_tone = shadow_tone_percent * 255 | |
highlight_tone = 255 - highlight_tone_percent * 255 | |
shadow_gain = 1 + shadow_amount_percent * 6 | |
highlight_gain = 1 + highlight_amount_percent * 6 | |
# extract RGB channel | |
height, width = img.shape[:2] | |
img = img.astype(np.float) | |
img_R, img_G, img_B = img[..., 2].reshape(-1), img[..., 1].reshape(-1), img[..., 0].reshape(-1) | |
# The entire correction process is carried out in YUV space, | |
# adjust highlights/shadows in Y space, and adjust colors in UV space | |
# convert to Y channel (grey intensity) and UV channel (color) | |
img_Y = .3 * img_R + .59 * img_G + .11 * img_B | |
img_U = -img_R * .168736 - img_G * .331264 + img_B * .5 | |
img_V = img_R * .5 - img_G * .418688 - img_B * .081312 | |
# extract shadow / highlight | |
shadow_map = 255 - img_Y * 255 / shadow_tone | |
shadow_map[np.where(img_Y >= shadow_tone)] = 0 | |
highlight_map = 255 - (255 - img_Y) * 255 / (255 - highlight_tone) | |
highlight_map[np.where(img_Y <= highlight_tone)] = 0 | |
# // Gaussian blur on tone map, for smoother transition | |
if shadow_amount_percent * shadow_radius > 0: | |
# shadow_map = cv2.GaussianBlur(shadow_map.reshape(height, width), ksize=(shadow_radius, shadow_radius), sigmaX=0).reshape(-1) | |
shadow_map = cv2.blur(shadow_map.reshape(height, width), ksize=(shadow_radius, shadow_radius)).reshape(-1) | |
if highlight_amount_percent * highlight_radius > 0: | |
# highlight_map = cv2.GaussianBlur(highlight_map.reshape(height, width), ksize=(highlight_radius, highlight_radius), sigmaX=0).reshape(-1) | |
highlight_map = cv2.blur(highlight_map.reshape(height, width), ksize=(highlight_radius, highlight_radius)).reshape(-1) | |
# Tone LUT | |
t = np.arange(256) | |
LUT_shadow = (1 - np.power(1 - t * (1 / 255), shadow_gain)) * 255 | |
LUT_shadow = np.maximum(0, np.minimum(255, np.int_(LUT_shadow + .5))) | |
LUT_highlight = np.power(t * (1 / 255), highlight_gain) * 255 | |
LUT_highlight = np.maximum(0, np.minimum(255, np.int_(LUT_highlight + .5))) | |
# adjust tone | |
shadow_map = shadow_map * (1 / 255) | |
highlight_map = highlight_map * (1 / 255) | |
iH = (1 - shadow_map) * img_Y + shadow_map * LUT_shadow[np.int_(img_Y)] | |
iH = (1 - highlight_map) * iH + highlight_map * LUT_highlight[np.int_(iH)] | |
img_Y = iH | |
# adjust color | |
if color_percent != 0: | |
# color LUT | |
if color_percent > 0: | |
LUT = (1 - np.sqrt(np.arange(32768)) * (1 / 128)) * color_percent + 1 | |
else: | |
LUT = np.sqrt(np.arange(32768)) * (1 / 128) * color_percent + 1 | |
# adjust color saturation adaptively according to highlights/shadows | |
color_gain = LUT[np.int_(img_U ** 2 + img_V ** 2 + .5)] | |
w = 1 - np.minimum(2 - (shadow_map + highlight_map), 1) | |
img_U = w * img_U + (1 - w) * img_U * color_gain | |
img_V = w * img_V + (1 - w) * img_V * color_gain | |
# re convert to RGB channel | |
output_R = np.int_(img_Y + 1.402 * img_V + .5) | |
output_G = np.int_(img_Y - .34414 * img_U - .71414 * img_V + .5) | |
output_B = np.int_(img_Y + 1.772 * img_U + .5) | |
output = np.row_stack([output_B, output_G, output_R]).T.reshape(height, width, 3) | |
output = np.minimum(output, 255).astype(np.uint8) | |
return output |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment