PS 色阶调整之算法公式原理详解及 Python 实现(色阶原理)

PS 色阶调整之算法公式原理详解及 Python 实现(色阶原理)

本文介绍了 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={0255​if 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={0255​if Vout<0if Vout>255​

完整 python 代码如下:

# -*- coding: utf-8 -*-

# @Time : 2021-02-24 16:45

# @Author : AlanWang4523

# @FileName: ps_levels.py

import os

import sys

import cv2

import numpy as np

class Levels:

"""

@Author : AlanWang4523

色阶调整类,根据输入参数调整图片色阶,并输出处理后的图片

"""

def __init__(self):

self.channel = 0

self.input_shadows = 0

self.input_highlights = 255

self.midtones = 1.0

self.output_shadows = 0

self.output_highlights = 255

def 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] = 0

img[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_shadows

img[img < 0] = 0

img[img > 255] = 255

img = img.astype(np.uint8)

return img

def level_adjust_and_save_img(origin_image):

levels.input_shadows = 40

levels.input_highlights = 240

levels.midtones = 0.60

levels.output_shadows = 30

levels.output_highlights = 220

image = 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 = x

def update_input_highlights(x):

if (x > levels.input_shadows):

levels.input_highlights = x

def update_midtones(x):

# 由于 midtones 的调整范围是 [9.99, 0.01],Python 滑杆无法自定义显示小数,因此将滑杆的 [0, 100] 映射到 [9.99, 0.01]

midtones = 1.0

if (x < 50):

midtones = 1 + 9 * ((50.0 - x) / 50.0)

elif (x > 50):

midtones = 1 - (x - 50) / 50.0

levels.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 = x

def 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'):

break

cv2.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.0

if (x < 50):

midtones = 1 + 9 * ((50.0 - x) / 50.0)

elif (x > 50):

midtones = 1 - (x - 50) / 50.0

levels.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

相关推荐