本文介绍了 PS 中色阶的实现原理及公式,并用 Python 实现,自测与 PS 的色阶调整效果基本完全一样(使用和 PS 中色阶相同的参数对比效果,包括各极限值,本文只实现了 RGB 整体色阶的处理,对各个通道的处理逻辑公式是一样的,实际使用会用 OpenGL 实现)
如下图是 PS 中色阶调整的操作面板,可以对 R、G、B 单独的通道或 RGB 整体通道做色阶调整。每个通道或整体有五个参数(红色箭头所指的五个滑块,调整时也可以直接输入值):
inputShadows: 输入图像的黑场阈值,作用是将输入图像中低于该阈值的全变成 0
inputHighlit: 输入图像的白场阈值,作用是将输入图像中高于该阈值的全变成 255
midtone: 中间调,范围是 [0.01, 9.99],默认值是 1.0 在中间,[9.99 – 1.0 – 0.01],从中间往左调,即[1.0, 9.99] 加灰降对比度,往右调减灰加对比度(RGB通道)
outputShadows: 输出图像的黑场阈值,输出图像的最低值为该阈值
outputHighlight: 输出图像的白场阈值,输出图像的最高值为该阈值
色阶调整的处理转换公式如下:
分输入色阶映射、中间调调整、输出色阶映射共三步处理,上一步处理的输出做为下一步的输入(以下公式用 MarkDown Latex 语法编辑)。
输入色阶映射公式:
V o u t = 255 ∗ V i n − i n S h a d o w s i n H i g h l i g h t s − i n S h a d o w s Vout = 255 * \frac{Vin - inShadows}{inHighlights - inShadows} Vout=255∗inHighlights−inShadowsVin−inShadows
V o u t = { 0 if V o u t < 0 255 if V o u t > 255 Vout = \left\{ \begin{array}{ll} 0 & \textrm{if $Vout<0$}\\ 255 & \textrm{if $Vout>255$}\\ \end{array} \right. Vout={0255if Vout<0if Vout>255
中间调调整:
V o u t = 255 ∗ ( V i n 255.0 ) 1 m i d t o n e s Vout = 255 * (\frac{Vin}{255.0})^\frac{1}{midtones} Vout=255∗(255.0Vin)midtones1
输出色阶映射:
V o u t = V i n 255.0 ∗ ( o u t L i g h h i g h t s − o u t S h a d o w s ) + o u t S h a d o w s Vout = \frac{Vin}{255.0} * (outLighhights - outShadows) + outShadows Vout=255.0Vin∗(outLighhights−outShadows)+outShadows
V o u t = { 0 if V o u t < 0 255 if V o u t > 255 Vout = \left\{ \begin{array}{ll} 0 & \textrm{if $Vout<0$}\\ 255 & \textrm{if $Vout>255$}\\ \end{array} \right. Vout={0255if Vout<0if Vout>255
# -*- coding: utf-8 -*-
# @Time : 2021-02-24 16:45
# @Author : AlanWang4523
# @FileName: ps_levels.pyimport os
import sys
import cv2
import numpy as npclass Levels:"""@Author : AlanWang4523色阶调整类,根据输入参数调整图片色阶,并输出处理后的图片"""def __init__(self):self.channel = 0self.input_shadows = 0self.input_highlights = 255self.midtones = 1.0self.output_shadows = 0self.output_highlights = 255def adjust_image(self, img):print("Levels Params:")print(" channel:", self.channel)print(" input_shadows:", self.input_shadows)print(" input_highlights:", self.input_highlights)print(" midtones:", self.midtones)print(" output_shadows:", self.output_shadows)print("output_highlights:", self.output_highlights)print("")img = img.astype(np.float)# 输入色阶映射img = 255 * ((img - self.input_shadows) / (self.input_highlights - self.input_shadows))img[img < 0] = 0img[img > 255] = 255# 中间调处理img = 255 * np.power(img / 255.0, 1.0 / self.midtones)# 输出色阶映射img = (img / 255) * (self.output_highlights - self.output_shadows) + self.output_shadowsimg[img < 0] = 0img[img > 255] = 255img = img.astype(np.uint8)return imgdef level_adjust_and_save_img(origin_image):levels.input_shadows = 40levels.input_highlights = 240levels.midtones = 0.60levels.output_shadows = 30levels.output_highlights = 220image = levels.adjust_image(origin_image)cv2.imwrite('py_test_out.png', image)def level_adjust(path):"""色阶调整"""origin_image = cv2.imread(path)levels = Levels()def update_input_shadows(x):if (x < levels.input_highlights):levels.input_shadows = xdef update_input_highlights(x):if (x > levels.input_shadows):levels.input_highlights = xdef update_midtones(x):# 由于 midtones 的调整范围是 [9.99, 0.01],Python 滑杆无法自定义显示小数,因此将滑杆的 [0, 100] 映射到 [9.99, 0.01]midtones = 1.0if (x < 50):midtones = 1 + 9 * ((50.0 - x) / 50.0)elif (x > 50):midtones = 1 - (x - 50) / 50.0levels.midtones = np.clip(midtones, 0.01, 9.99)# levels.midtones = 0.6 # 直接测试某个参数值def update_output_shadows(x):if (x < levels.output_highlights):levels.output_shadows = xdef update_output_highlights(x):if (x > levels.output_shadows):levels.output_highlights = x# 创建图片显示窗口title = "Levels"cv2.namedWindow(title, cv2.WINDOW_NORMAL) cv2.resizeWindow(title, 800, 600)cv2.moveWindow(title, 0, 0)# 创建色阶操作窗口option_title = "Option"cv2.namedWindow(option_title, cv2.WINDOW_NORMAL) cv2.resizeWindow(option_title, 400, 200)cv2.moveWindow(option_title, 800, 0)cv2.createTrackbar(' input_shadows', option_title, levels.input_shadows, 255, update_input_shadows)cv2.createTrackbar(' input_highlights', option_title, levels.input_highlights, 255, update_input_highlights)cv2.createTrackbar(' midtones', option_title, 50, 100, update_midtones)cv2.createTrackbar(' output_shadows', option_title, levels.output_shadows, 255, update_output_shadows)cv2.createTrackbar('output_highlights', option_title, levels.output_highlights, 255, update_output_highlights)while True:image = levels.adjust_image(origin_image)cv2.imshow(title, image)if cv2.waitKey(1) == ord('q'):breakcv2.destroyAllWindows() if __name__ == '__main__':'''Author: AlanWang4523运行环境:Python 3执行:python3 ps_levels.py <图片路径>如:python3 ps_levels.py test.jpg'''if len(sys.argv) == 1:print("参数错误:未传入图片路径!")sys.exit(-1)img_path = sys.argv[1]print("img_path Params:", img_path)level_adjust(img_path)
def update_midtones(x):# 由于 midtones 的调整范围是 [9.99, 0.01],Python 滑杆无法自定义显示小数,因此将滑杆的 [0, 100] 映射到 [9.99, 0.01]midtones = 1.0if (x < 50):midtones = 1 + 9 * ((50.0 - x) / 50.0)elif (x > 50):midtones = 1 - (x - 50) / 50.0levels.midtones = np.clip(midtones, 0.01, 9.99)# levels.midtones = 0.6 # 直接测试某个参数值
使用和 PS 相同的参数,效果和 PS 上的完全一致,各参数下和 PS 对比效果如下:
(截图上半部部分为用 Python 使用与 PS 相同参数的效果,下半部分为 PS 的效果)
① Python 和 PS 都使用如下参数:
inputShadows: 50
inputHighlit: 52
midtone: 0.50 (根据上面的映射公式,python 实现的 midtones 的滑块在 75 的位置)
outputShadows: 50
outputHighlight: 200
效果如下:
② Python 和 PS 都使用如下参数:
inputShadows: 0
inputHighlit: 255
midtone: 0.10
outputShadows: 0
outputHighlight: 255
效果如下:
参考文档:
Adobe 官网的 Levels Adjustment
Algorithm for adjustment of image levels