个性化阅读
专注于IT技术分析

计算机图形学中的Python图像处理简介

本文概述

计算摄影是通过计算来增强摄影过程。虽然我们通常倾向于认为这仅适用于对最终结果进行后处理(类似于照片编辑), 但其可能性要丰富得多, 因为可以在摄影过程的每个步骤中启用计算-从场景照明开始, 然后到镜头, 甚至在拍摄的图像显示时也是如此。

这很重要, 因为与普通相机相比, 它可以做的事情更多, 而且方式也不同。这一点也很重要, 因为当今最流行的相机(即移动相机)与其较大的同级相机(DSLR)相比并没有特别强大, 但它可以利用设备上可用的计算能力来出色地完成工作。

我们将看两个例子, 其中计算可以增强摄影效果-更确切地说, 我们将看到在两种情况下, 简单地拍摄更多照片并使用一些Python进行组合就可以在没有移动相机硬件的情况下产生不错的效果真正的光芒-低光和高动态范围。

低光摄影

假设我们要为场景拍摄低光, 但相机的光圈(镜头)较小且曝光时间有限。对于手机相机来说, 这是典型的情况, 在低光照的情况下, 可能会产生这样的图像(使用iPhone 6相机拍摄):

在昏暗的环境中几个玩具的形象

如果我们尝试改善对比度, 则结果如下, 这也很糟糕:

与上述相同的图像,亮度更高,但视觉干扰较小

怎么了?这些噪音从何而来?

答案是噪声来自传感器, 该传感器试图确定光线何时照射到光线以及光线强度。然而, 在弱光下, 它必须大大提高其灵敏度才能记录任何东西, 而高灵敏度意味着它也开始检测假阳性-光子根本就不存在。 (请注意, 此问题不仅会影响设备, 还会影响我们的人类:下次当你在黑暗的房间里时, 请花一点时间注意视野中的噪音。)

成像设备中始终会存在一定数量的噪音;但是, 如果信号(有用信息)强度高, 则噪声可以忽略不计(信噪比高)。当信号很低时(例如在光线不足的情况下), 噪声会突出(信噪比低)。

尽管如此, 即使在所有相机限制下, 我们也可以克服噪点问题, 以获得比上述相机更好的拍摄效果。

为此, 我们需要考虑一段时间后会发生什么:信号将保持不变(同一场景, 并且我们假定它是静态的), 而噪声将完全是随机的。这意味着, 如果我们对场景进行多次拍摄, 它们将具有不同版本的噪点, 但具有相同的有用信息。

因此, 如果我们将一段时间内拍摄的许多图像平均化, 则噪声将被抵消, 而信号将不受影响。

下图显示了一个简化的示例:我们有一个受噪声影响的信号(三角形), 并且我们尝试通过平均受不同噪声影响的同一信号的多个实例来恢复该信号。

该三角形的四面板演示,代表带有附加噪声的三角形的散布图像,一种锯齿状的三角形,代表平均50个实例,平均1000个实例,看起来与原始三角形几乎相同。

我们看到, 尽管噪声足够强大, 可以在任何单个实例中完全使信号失真, 但通过平均可以逐渐降低噪声, 并恢复原始信号。

让我们看看该原理如何应用于图像:首先, 我们需要以相机允许的最大曝光量对被摄对象进行多次拍摄。为了获得最佳效果, 请使用允许手动拍摄的应用程序。从同一位置拍摄照片很重要, 因此(简易的)三脚架会有所帮助。

拍摄更多照片通常意味着更好的质量, 但是确切的数量取决于情况:有多少光线, 相机有多灵敏等等。一个好的范围可能在10到100之间。

一旦获得了这些图像(如果可能, 则为原始格式), 我们就可以在Python中读取和处理它们。

对于不熟悉Python图像处理的用户, 应该提到图像表示为字节值(0-255)的2D数组, 即单色或灰度图像。彩色图像可以看作是三个这样的图像的集合, 每个颜色通道一个图像(R, G, B), 或者实际上是一个由垂直位置, 水平位置和颜色通道(0、1、2)索引的3D阵列。 。

我们将使用两个库:NumPy(http://www.numpy.org/)和OpenCV(https://opencv.org/)。第一个允许我们非常有效地对数组执行计算(使用出奇的短代码), 而OpenCV在这种情况下可以处理图像文件的读/写, 但是功能更强大, 可以提供许多高级图形处理程序, 其中一些我们将在本文后面使用。

import os
import numpy as np
import cv2

folder = 'source_folder'

# We get all the image files from the source folder
files = list([os.path.join(folder, f) for f in os.listdir(folder)])

# We compute the average by adding up the images
# Start from an explicitly set as floating point, in order to force the
# conversion of the 8-bit values from the images, which would otherwise overflow
average = cv2.imread(files[0]).astype(np.float)
for file in files[1:]:
    image = cv2.imread(file)
    # NumPy adds two images element wise, so pixel by pixel / channel by channel
    average += image
 
# Divide by count (again each pixel/channel is divided)
average /= len(files)

# Normalize the image, to spread the pixel intensities across 0..255
# This will brighten the image without losing information
output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX)

# Save the output
cv2.imwrite('output.png', output)

结果(应用自动对比度)表明噪声消失了, 与原始图像相比有了很大的改进。

玩具的原始照片,这次更明亮,更清晰,几乎看不到噪音

但是, 我们仍然注意到一些奇怪的伪像, 例如绿色框和网格状图案。这次, 它不是随机噪声, 而是固定模式噪声。发生了什么?

上图左上角的特写

左上角的特写镜头, 显示绿色框和网格图案

同样, 我们可以将其归咎于传感器。在这种情况下, 我们看到传感器的不同部分对光的反应不同, 从而导致可见图案。这些图案的某些元素是规则的, 并且很可能与传感器基板(金属/硅)及其反射/吸收入射光子的方式有关。其他元素(例如白色像素)只是有缺陷的传感器像素, 可能对光过于敏感或过于不敏感。

幸运的是, 也有一种方法可以消除这种类型的噪音。这称为暗框减法。

为此, 我们需要图案噪声本身的图像, 如果我们拍摄黑暗, 则可以得到图像。是的, 没错-只需遮盖相机孔并以最大曝光时间和ISO值拍摄很多照片(例如100张), 然后如上所述进行处理即可。

当平均许多黑色帧(由于随机噪声而实际上不是黑色)时, 我们将得到固定的图案噪声。我们可以假定此固定噪声将保持恒定, 因此仅需执行一次此步骤:生成的图像可用于将来的所有弱光拍摄。

这是iPhone 6图案右上角部分(调整对比度)的样子:

上一幅图像中显示的帧部分的图案噪声

再次, 我们注意到类似网格的纹理, 甚至是看起来像是白​​色像素滞留的东西。

一旦获得了暗帧噪声的值(在average_noise变量中), 就可以在归一化之前, 从到目前为止的镜头中简单地减去它:

average -= average_noise

output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX)
cv2.imwrite('output.png', output)

这是我们的最终照片:

照片的另一张图像,这次完全没有证据表明是在弱光下拍摄的

高动态范围

小型(移动)相机的另一个限制是其动态范围小, 这意味着它可以捕获细节的光强度范围很小。

换句话说, 相机只能从场景中捕获一小段光强;该波段下方的强度显示为纯黑色, 而上方波段的强度显示为纯白色, 并且这些区域中的所有细节都丢失了。

但是, 相机(或摄影师)可以使用一个技巧, 那就是调整曝光时间(传感器暴露于光线的时间), 以便有效地控制到达传感器的光线总量。向上或向下移动范围, 以捕获给定场景的最合适范围。

但这是一个折衷。许多细节未能将其纳入最终照片。在下面的两张图像中, 我们看到以不同的曝光时间拍摄的同一场景:非常短的曝光(1/1000秒), 中等曝光(1/50秒)和长时间曝光(1/4秒)。

三种相同花朵图案的照片,一种太暗以至于大多数照片都是黑色的,一种看起来很正常,尽管光线有些许不幸,而第三种则把光摇得很高,以致于很难看到花朵前景

如你所见, 这三个图像都无法捕获所有可用的细节:灯丝仅在第一张照片中可见, 而一些花的细节在中间或最后一张照片中可见, 但不可见都。

好消息是, 我们可以做一些事情, 而且还涉及使用一些Python代码在多个镜头上进行构建。

我们将采用的方法基于Paul Debevec等人的工作, Paul Debevec等人在本文中介绍了该方法。该方法如下所示:

首先, 它需要对同一场景(固定)进行多次拍摄, 但曝光时间不同。再次, 与前面的情况一样, 我们需要一个三脚架或支撑架以确保相机完全不动。我们还需要一个手动拍摄应用程序(如果使用手机), 以便我们可以控制曝光时间并防止相机自动调整。所需的拍摄数量取决于图像中出现的强度范围(从三个以上), 并且曝光时间应在该范围内隔开, 以便我们感兴趣保存的细节至少出现在一张照片中。

接下来, 基于在不同曝光时间下相同像素的颜色, 使用一种算法来重建相机的响应曲线。基本上, 这使我们可以在点的真实场景亮度, 曝光时间和所捕获图像中相应像素将具有的值之间建立映射。我们将使用OpenCV库中Debevec方法的实现。

# Read all the files with OpenCV
files = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg']
images = list([cv2.imread(f) for f in files])
# Compute the exposure times in seconds
exposures = np.float32([1. / t for t in [1000, 500, 100, 50, 10]])

# Compute the response curve
calibration = cv2.createCalibrateDebevec()
response = calibration.process(images, exposures)

响应曲线如下所示:

以像素值相对于像素值的曝光量(对数)的形式显示响应曲线的图

在垂直轴上, 我们具有点的场景亮度和曝光时间的累积影响, 而在水平轴上, 我们具有对应像素将具有的值(每个通道0到255)。

这条曲线允许我们执行反向操作(这是该过程的下一步)-鉴于像素值和曝光时间, 我们可以计算场景中每个点的真实亮度。该亮度值称为辐照度, 它测量落在传感器区域单位上的光能量。与图像数据不同, 它使用浮点数表示, 因为它反映的值范围更广(因此, 动态范围较高)。获得辐照度图像(HDR图像)后, 我们可以简单地保存它:

# Compute the HDR image
merge = cv2.createMergeDebevec()
hdr = merge.process(images, exposures, response)

# Save it to disk
cv2.imwrite('hdr_image.hdr', hdr)

对于我们这些幸运的拥有HDR显示器(越来越常见)的人来说, 有可能直接在其所有荣耀中可视化该图像。不幸的是, HDR标准仍处于起步阶段, 因此针对不同的显示器, 其执行过程可能有所不同。

对于我们其他人来说, 好消息是, 尽管正常显示要求图像具有字节值(0-255)通道, 但我们仍然可以利用此数据。尽管我们需要放弃一些辐照度图, 但是至少我们可以控制辐照度。

此过程称为色调映射, 它涉及将浮点辐照度图(具有较大的值范围)转换为标准字节值图像。有一些技术可以做到, 以便保留许多额外的细节。只是为你提供一个示例, 说明如何将浮点范围压缩为字节值, 然后增强(锐化)HDR图像中存在的边缘。增强这些边缘将有助于在低动态范围图像中保留它们(并隐式提供它们的细节)。

OpenCV提供了一组这些音调映射运算符, 例如Drago, Durand, Mantiuk或Reinhardt。这是一个示例, 说明如何使用这些运算符之一(Durand)及其产生的结果。

durand = cv2.createTonemapDurand(gamma=2.5)
ldr = durand.process(hdr)

# Tonemap operators create floating point images with values in the 0..1 range
# This is why we multiply the image with 255 before saving
cv2.imwrite('durand_image.png', ldr * 255)
以上计算结果显示为图像

如果你需要对过程进行更多控制, 则还可以使用Python创建自己的运算符。例如, 这是使用自定义运算符获得的结果, 该运算符在将值范围缩小到8位之前删除了以很少像素表示的强度(随后是自动对比度步骤):

通过上述过程得到的图像

这是上述运算符的代码:

def countTonemap(hdr, min_fraction=0.0005):
	counts, ranges = np.histogram(hdr, 256)
	min_count = min_fraction * hdr.size
	delta_range = ranges[1] - ranges[0]

	image = hdr.copy()
	for i in range(len(counts)):
    	if counts[i] < min_count:
        	image[image >= ranges[i + 1]] -= delta_range
        	ranges -= delta_range

	return cv2.normalize(image, None, 0, 1, cv2.NORM_MINMAX)

总结

我们已经了解了如何使用一些Python和几个支持库来突破物理相机的极限, 以改善最终结果。我们讨论的两个示例都使用了多个低质量的镜头来创造更好的效果, 但是还有许多其他方法可以解决不同的问题和局限性。

尽管许多照相手机都具有解决这些特定示例的存储或内置应用程序, 但显然手工编写这些程序并享受可以得到的更高级别的控制和理解并不困难。

如果你对移动设备上的图像计算感兴趣, 请参阅srcminier和精英OpenCV开发人员Altaibayar Tseveenbayar撰写的OpenCV教程:使用iOS中的MSER进行实时对象检测。

赞(0)
未经允许不得转载:srcmini » 计算机图形学中的Python图像处理简介

评论 抢沙发

评论前必须登录!