OpenCV进阶(9)基于OpenCV的图像着色

在这里插入图片描述
有时技术会增强艺术,有时它会破坏艺术。

为黑白电影着色是一个可以追溯到 1902 年的非常古老的想法。几十年来,许多电影创作者反对为黑白电影着色的想法,并认为这是对他们艺术的破坏。今天,它被认为是对艺术形式的一种改进。

如果算法不使用任何用户输入,那不是很酷吗?

1.定义着色问题

让我们首先根据 CIE Lab 色彩空间来定义着色问题。与 RGB 颜色空间一样,它是一个 3 通道颜色空间,但与 RGB 颜色空间不同的是,颜色信息仅在 a(绿-红分量)和 b(蓝-黄分量)通道中进行编码。 L(亮度)通道仅编码强度信息。

我们想要着色的灰度图像可以看作是图像在Lab颜色空间中的L通道,我们的目标是找到a和b分量。这样得到的Lab图像可以使用标准颜色空间变换转换为RGB颜色空间。例如,在OpenCV中,这可以使用带有COLOR_BGR2Lab选项的cvtColor来实现。

为了简化计算,Lab 颜色空间的 ab 空间被量化为 313 个 bin,如下图所示。而不是为每个像素找到 a 和 b 值,因为这种量化,我们只需要找到一个介于 0 和 312之间的 bin 编号。另一种思考问题的方式是我们已经有了取值从 0 到 255 的 L 通道,我们需要找到取值在 0 到 312 之间的 ab 通道。所以颜色预测任务现在是变成了多分类问题,其中每个灰色像素有 313 个类别可供选择。

在这里插入图片描述

2.用于着色的 CNN 架构

Zhang 等人提出的架构是具有多个卷积块的 VGG 式网络。每个块都有两个或三个卷积层,后跟一个修正线性单元 (ReLU),并终止于一个批量归一化层(BN)。与 VGG 网络不同,它没有池化或全连接层。
在这里插入图片描述
输入图像被重新缩放为 224×224。让我们用 X X X表示这个重新缩放的灰度输入图像。

当它通过上图所示的神经网络时,它会被神经网络转化为 Z ^ \hat Z Z^。在数学上,网络的这种转换可以写成 Z ^ = G ( X ) \hat Z = G(X) Z^=G(X)

Z ^ \hat Z Z^的维度是 H ∗ W ∗ Q H*W*Q HWQ,其中 H ( = 56 ) H(=56) H(=56) W ( = 56 ) W(=56) W(=56)是最后一个卷积层输出的高度和宽度。对于每个 H ∗ W H*W HW像素, Z ^ \hat Z Z^包含一个 Q ( = 313 ) Q(=313) Q(=313)值向量,其中每个值表示像素属于该类的概率。我们的目标是为每个概率分布 Z ^ h , w \hat Z_{h,w} Z^h,w找到一对 ab 通道值。

3.从 Z ^ \hat Z Z^恢复彩色图像

上图所示的 CNN 为我们提供了来自调整大小的输入图像 X X X的分布集合 Z ^ \hat Z Z^。让我们看看如何从 Z ^ \hat Z Z^ 中的每个分布中恢复单个 ab 值对。

您可能会认为我们可以简单地取分布的平均值并选择与最近的量化 bin 中心相对应的 ab 对。不幸的是,这种分布不是高斯分布,分布的均值仅对应于不自然的不饱和颜色。要理解这一点,请想一想天空的颜色——它有时是蓝色的,有时是橙黄色的。天空颜色的分布是双峰的。在为天空着色时,蓝色或黄色都会产生合理的颜色。但是蓝色和黄色的平均值是一种无趣的灰色。

那么为什么不使用分布模式来获得蓝色或黄色的天空呢?当然,作者尝试过,虽然它提供了鲜艳的色彩,但有时会破坏空间一致性。他们的解决方案是在均值和模式估计之间进行插值,以获得称为退火均值的量。使用称为温度 (T) 的参数来控制插值程度。 T=0.38 的最终值用作两个极端之间的权衡。

在这里插入图片描述
使 用 温 度 ( T ) 的 退 火 均 值 用 于 在 分 布 的 均 值 和 众 数 之 间 进 行 插 值 。 使用温度 (T) 的退火均值用于在分布的均值和众数之间进行插值。 使(T)退

对应于 Z ^ \hat Z Z^分布的退火均值的 ab 对表示为 Y ^ h , w \hat Y_{h,w} Y^h,w,可以写成原始分布 Z ^ h , w \hat Z_{h,w} Z^h,w的变换 Y ^ = H ( Z ^ ) \hat Y=H(\hat Z) Y^=H(Z^)

请注意,当图像通过 CNN 时,其大小减小到 56×56。因此,预测的 ab 图像 Y ^ \hat Y Y^也具有 56×56 的维度。为了获得彩色图像,将其上采样到原始图像大小,然后添加到亮度通道 L,以生成最终的彩色图像。

4.具有颜色重新平衡的多项式损失函数

所有神经网络都是通过定义损失函数来训练的。训练过程的目标是最小化训练集的损失。在着色问题中,训练数据由数千张彩色图像及其灰度版本组成。

CNN 的输出是 Z ^ \hat Z Z^,输入图像是 X X X。我们需要将训练集中的所有彩色图像转换为其对应的值。在数学上,我们只是想反转映射 H H H Z = H − 1 ( Y ) Z=H^{-1}(Y) Z=H1(Y)

对于输出图像 Y Y Y的每个像素 Y h , w Y_{h,w} Yh,w,我们可以简单地找到最近的 ab bin 并将 Z h , w Z_{h,w} Zh,w表示为一个独热向量,其中我们将 1 分配给最近的 ab bin,将 0 分配给所有其他 312 个 bin。但为了获得更好的结果,我们考虑了5个最近邻,并使用高斯分布来计算分布 Z h , w Z_{h,w} Zh,w,这取决于与真实值的距离。

如果您之前使用过 CNN,您可能会想使用标准交叉熵损失来比较真实值 Z Z Z和预测值 Z ^ \hat Z Z^
L ( Z ^ , Z ) = − 1 H W ∑ h , w ∑ q Z h , w , q l o g ( Z ^ h , w , q ) L(\hat Z, Z) = -\frac{1}{HW}\sum_{h,w}\sum_qZ_{h,w,q}log(\hat Z_{h,w,q}) L(Z^,Z)=HW1h,wqZh,w,qlog(Z^h,w,q)
不幸的是,上述损失函数会产生非常暗淡的颜色。这是因为 ImageNet 中的颜色分布在灰线周围很重。

5.彩色化的结果

作者分享了两个版本的经过训练的 Caffe 模型 - 有和没有颜色重新平衡。我们尝试了两个版本,并在下图中分享了结果。中间一列显示没有重新平衡颜色的版本,最后一列显示重新平衡的版本。

正如我们所见,色彩重新平衡使许多图像非常生动活泼。它们中的大多数都是似是而非的颜色。另一方面,有时它也会为某些图像添加一些不需要的饱和色块。

请记住,当我们尝试将灰度图像转换为彩色图像时,可能有多种合理的解决方案。因此,评估良好着色的方法不是它与基本事实的匹配程度,而是它在人眼中看起来的可信度和愉悦度。

5.1 动物

该模型在动物图像上表现非常好,尤其是猫和狗。这是因为 ImageNet 包含非常大的这些动物集合。
在这里插入图片描述
在这里插入图片描述

5.2户外场景

该模型在表现蓝天和绿色植被的户外场景方面也做得非常好。另请注意,给定一棵树的轮廓,该模型预测橙色天空表明它已捕捉到日落的概念。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3草图

最后,即使是草图,模型也会产生合理的着色。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.在 OpenCV 中实现着色

作者在此位置的 GitHub 中提供了预训练模型和网络详细信息。下面,我们将回顾 Python 和 C++ 代码,使用这些预训练模型对给定的灰度图像进行着色。我们的代码基于 OpenCV 示例代码。我们使用了 OpenCV 4.5.1 版。我们还提供代码来为给定的灰度视频着色。

链接:https://pan.baidu.com/s/14_my8daL2SxgFVymQy-0Dg 
提取码:1v8m

(1)Python
图像着色代码:

# colorizeImage.py
# Usage 
# python colorizeImage.py --input greyscaleImage.png

import numpy as np
import cv2 as cv
import argparse
import os.path

parser = argparse.ArgumentParser(description='Colorize GreyScale Image')
parser.add_argument('--input', help='Path to image.', default="greyscaleImage.png")
args = parser.parse_args()

if args.input==None:
    print('Please give the input greyscale image name.')
    print('Usage example: python3 colorizeImage.py --input greyscaleImage.png')
    exit()

if os.path.isfile(args.input)==0:
    print('Input file does not exist')
    exit()

# 读取输入图像
frame = cv.imread(args.input)

# 指定 2 个模型文件的路径
protoFile = "./models/colorization_deploy_v2.prototxt"
weightsFile = "./models/colorization_release_v2.caffemodel"
# weightsFile = "./models/colorization_release_v2_norebal.caffemodel"

# 加载聚类中心
pts_in_hull = np.load('./pts_in_hull.npy')

# 将网络读入内存
net = cv.dnn.readNetFromCaffe(protoFile, weightsFile)

# 将聚类中心填充为 1x1 卷积核
pts_in_hull = pts_in_hull.transpose().reshape(2, 313, 1, 1)
net.getLayer(net.getLayerId('class8_ab')).blobs = [pts_in_hull.astype(np.float32)]
net.getLayer(net.getLayerId('conv8_313_rh')).blobs = [np.full([1, 313], 2.606, np.float32)]

#来自 opencv 示例
W_in = 224
H_in = 224

img_rgb = (frame[:,:,[2, 1, 0]] * 1.0 / 255).astype(np.float32)
img_lab = cv.cvtColor(img_rgb, cv.COLOR_RGB2Lab)
img_l = img_lab[:,:,0] # 拉出L通道

# 将亮度通道调整为网络输入大小
img_l_rs = cv.resize(img_l, (W_in, H_in)) #
img_l_rs -= 50 # 减去50中心化

net.setInput(cv.dnn.blobFromImage(img_l_rs))
ab_dec = net.forward()[0,:,:,:].transpose((1,2,0)) # 这是我们的结果

(H_orig,W_orig) = img_rgb.shape[:2] # 原始图像大小
ab_dec_us = cv.resize(ab_dec, (W_orig, H_orig))
img_lab_out = np.concatenate((img_l[:,:,np.newaxis],ab_dec_us),axis=2) # 与原始图像 L 连接
img_bgr_out = np.clip(cv.cvtColor(img_lab_out, cv.COLOR_Lab2BGR), 0, 1)

outputFile = args.input[:-4]+'_colorized.png'
cv.imwrite(outputFile, (img_bgr_out*255).astype(np.uint8))
print('Colorized image saved as '+outputFile)
print('Done !!!')

视频着色代码

# colorizeVideo.py
# Usage
# python colorizeVideo.py --input greyscaleVideo.mp4

import numpy as np
import cv2 as cv
import argparse
import os.path

parser = argparse.ArgumentParser(description='Colorize GreyScale Video')
parser.add_argument('--input', help='Path to video file.', default="greyscaleVideo.mp4")
args = parser.parse_args()

if args.input == None:
    print('Please give the input greyscale video file.')
    print('Usage example: python colorizeVideo.py --input greyscaleVideo.mp4')
    exit()

if os.path.isfile(args.input) == 0:
    print('Input file does not exist')
    exit()

# 读取输入视频
cap = cv.VideoCapture(args.input)
hasFrame, frame = cap.read()

outputFile = args.input[:-4] + '_colorized.avi'
vid_writer = cv.VideoWriter(outputFile, cv.VideoWriter_fourcc('M', 'J', 'P', 'G'), 60, (frame.shape[1], frame.shape[0]))

# 指定 2 个模型文件的路径
protoFile = "./models/colorization_deploy_v2.prototxt"
weightsFile = "./models/colorization_release_v2.caffemodel"
# weightsFile = "./models/colorization_release_v2_norebal.caffemodel"

# 加载聚类中心
pts_in_hull = np.load('./pts_in_hull.npy')

# 将网络读入内存
net = cv.dnn.readNetFromCaffe(protoFile, weightsFile)

# 将聚类中心填充为 1x1 卷积核
pts_in_hull = pts_in_hull.transpose().reshape(2, 313, 1, 1)
net.getLayer(net.getLayerId('class8_ab')).blobs = [pts_in_hull.astype(np.float32)]
net.getLayer(net.getLayerId('conv8_313_rh')).blobs = [np.full([1, 313], 2.606, np.float32)]

# 来自 opencv 示例
W_in = 224
H_in = 224

while cv.waitKey(1):

    hasFrame, frame = cap.read()
    frameCopy = np.copy(frame)
    if not hasFrame:
        break

    img_rgb = (frame[:, :, [2, 1, 0]] * 1.0 / 255).astype(np.float32)
    img_lab = cv.cvtColor(img_rgb, cv.COLOR_RGB2Lab)
    img_l = img_lab[:, :, 0]  # 拉出L通道

    # 将亮度通道调整为网络输入大小
    img_l_rs = cv.resize(img_l, (W_in, H_in))
    img_l_rs -= 50  # 减去50中心化

    net.setInput(cv.dnn.blobFromImage(img_l_rs))
    ab_dec = net.forward()[0, :, :, :].transpose((1, 2, 0))  # 结果

    (H_orig, W_orig) = img_rgb.shape[:2]  # 原始图像尺寸
    ab_dec_us = cv.resize(ab_dec, (W_orig, H_orig))
    img_lab_out = np.concatenate((img_l[:, :, np.newaxis], ab_dec_us), axis=2)  # 与原始 L 通道拼接
    img_bgr_out = np.clip(cv.cvtColor(img_lab_out, cv.COLOR_Lab2BGR), 0, 1)

    vid_writer.write((img_bgr_out * 255).astype(np.uint8))

vid_writer.release()

print('Colorized video saved as ' + outputFile)
print('Done !!!')

(2)C++
图像着色代码

// colorizeImage.cpp
// Usage 
// ./colorizeImage.out greyscaleImage.png

#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace cv::dnn;
using namespace std;

// 来自 pts_in_hull.npy 的 313 个 ab 聚类中心(已经转置)
static float hull_pts[] = {
    -90., -90., -90., -90., -90., -80., -80., -80., -80., -80., -80., -80., -80., -70., -70., -70., -70., -70., -70., -70., -70.,
    -70., -70., -60., -60., -60., -60., -60., -60., -60., -60., -60., -60., -60., -60., -50., -50., -50., -50., -50., -50., -50., -50.,
    -50., -50., -50., -50., -50., -50., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -30.,
    -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -20., -20., -20., -20., -20., -20., -20.,
    -20., -20., -20., -20., -20., -20., -20., -20., -20., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,
    -10., -10., -10., -10., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 10., 10., 10., 10., 10., 10., 10.,
    10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20.,
    20., 20., 20., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 40., 40., 40., 40.,
    40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 50., 50., 50., 50., 50., 50., 50., 50., 50., 50.,
    50., 50., 50., 50., 50., 50., 50., 50., 50., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60.,
    60., 60., 60., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 80., 80., 80.,
    80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 90., 90., 90., 90., 90., 90., 90., 90., 90., 90.,
    90., 90., 90., 90., 90., 90., 90., 90., 90., 100., 100., 100., 100., 100., 100., 100., 100., 100., 100., 50., 60., 70., 80., 90.,
    20., 30., 40., 50., 60., 70., 80., 90., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., -20., -10., 0., 10., 20., 30., 40., 50.,
    60., 70., 80., 90., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., 100., -40., -30., -20., -10., 0., 10., 20.,
    30., 40., 50., 60., 70., 80., 90., 100., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., 100., -50.,
    -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., 100., -60., -50., -40., -30., -20., -10., 0., 10., 20.,
    30., 40., 50., 60., 70., 80., 90., 100., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90.,
    100., -80., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., -80., -70., -60., -50.,
    -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., -90., -80., -70., -60., -50., -40., -30., -20., -10.,
    0., 10., 20., 30., 40., 50., 60., 70., 80., 90., -100., -90., -80., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30.,
    40., 50., 60., 70., 80., 90., -100., -90., -80., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70.,
    80., -110., -100., -90., -80., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., -110., -100.,
    -90., -80., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., -110., -100., -90., -80., -70.,
    -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., -110., -100., -90., -80., -70., -60., -50., -40., -30.,
    -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., -90., -80., -70., -60., -50., -40., -30., -20., -10., 0.
};

int main(int argc, char **argv)
{
    
    string imageFileName;
    // 从命令行获取参数
    if (argc < 2)
    {
        cout << "Please input the greyscale image filename." << endl;
        cout << "Usage example: ./colorizeImage.out greyscaleImage.png" << endl;
        return 1;
    }
    
    imageFileName = argv[1];
    Mat img = imread(imageFileName);
    if (img.empty())
    {
        cout << "Can't read image from file: " << imageFileName << endl;
        return 2;
    }
    
    string protoFile = "./models/colorization_deploy_v2.prototxt";
    string weightsFile = "./models/colorization_release_v2.caffemodel";
    //string weightsFile = "./models/colorization_release_v2_norebal.caffemodel";

    double t = (double) cv::getTickCount();
    
    // 预训练网络的固定输入大小
    const int W_in = 224;
    const int H_in = 224;
    Net net = dnn::readNetFromCaffe(protoFile, weightsFile);
    
    // 设置额外的层:
    int sz[] = {2, 313, 1, 1};
    const Mat pts_in_hull(4, sz, CV_32F, hull_pts);
    Ptr<dnn::Layer> class8_ab = net.getLayer("class8_ab");
    class8_ab->blobs.push_back(pts_in_hull);
    Ptr<dnn::Layer> conv8_313_rh = net.getLayer("conv8_313_rh");
    conv8_313_rh->blobs.push_back(Mat(1, 313, CV_32F, Scalar(2.606)));
    
    // 提取L通道并减去平均值
    Mat lab, L, input;
    img.convertTo(img, CV_32F, 1.0/255);
    cvtColor(img, lab, COLOR_BGR2Lab);
    extractChannel(lab, L, 0);
    resize(L, input, Size(W_in, H_in));
    input -= 50;
    
    // 通过网络运行L通道
    Mat inputBlob = blobFromImage(input);
    net.setInput(inputBlob);
    Mat result = net.forward();
    
    // 从网络输出中检索计算出的 a,b 通道
    Size siz(result.size[2], result.size[3]);
    Mat a = Mat(siz, CV_32F, result.ptr(0,0));
    Mat b = Mat(siz, CV_32F, result.ptr(0,1));
    resize(a, a, img.size());
    resize(b, b, img.size());
    
    // 合并,并转换回 BGR
    Mat color, chn[] = {L, a, b};
    merge(chn, 3, lab);
    cvtColor(lab, color, COLOR_Lab2BGR);

    t = ((double)cv::getTickCount() - t)/cv::getTickFrequency();
    cout << "Time taken : " << t << " secs" << endl;
    
    string str = imageFileName;
    str.replace(str.end()-4, str.end(), "");
    str = str+"_colorized.png";
    
    color = color*255;
    color.convertTo(color, CV_8U);
    imwrite(str, color);

    cout << "Colorized image saved as " << str << endl;
    
    return 0;
}

视频着色代码

// colorizeVideo.cpp
// Usage
// ./colorizeVideo.out greyscaleVideo.mp4

#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace cv::dnn;
using namespace std;

// 来自 pts_in_hull.npy 的 313 个 ab 聚类中心(已经转置)
static float hull_pts[] = {
    -90., -90., -90., -90., -90., -80., -80., -80., -80., -80., -80., -80., -80., -70., -70., -70., -70., -70., -70., -70., -70.,
    -70., -70., -60., -60., -60., -60., -60., -60., -60., -60., -60., -60., -60., -60., -50., -50., -50., -50., -50., -50., -50., -50.,
    -50., -50., -50., -50., -50., -50., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -40., -30.,
    -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -30., -20., -20., -20., -20., -20., -20., -20.,
    -20., -20., -20., -20., -20., -20., -20., -20., -20., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,
    -10., -10., -10., -10., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 10., 10., 10., 10., 10., 10., 10.,
    10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20.,
    20., 20., 20., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 30., 40., 40., 40., 40.,
    40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 40., 50., 50., 50., 50., 50., 50., 50., 50., 50., 50.,
    50., 50., 50., 50., 50., 50., 50., 50., 50., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60., 60.,
    60., 60., 60., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 70., 80., 80., 80.,
    80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 80., 90., 90., 90., 90., 90., 90., 90., 90., 90., 90.,
    90., 90., 90., 90., 90., 90., 90., 90., 90., 100., 100., 100., 100., 100., 100., 100., 100., 100., 100., 50., 60., 70., 80., 90.,
    20., 30., 40., 50., 60., 70., 80., 90., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., -20., -10., 0., 10., 20., 30., 40., 50.,
    60., 70., 80., 90., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., 100., -40., -30., -20., -10., 0., 10., 20.,
    30., 40., 50., 60., 70., 80., 90., 100., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., 100., -50.,
    -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., 100., -60., -50., -40., -30., -20., -10., 0., 10., 20.,
    30., 40., 50., 60., 70., 80., 90., 100., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90.,
    100., -80., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., -80., -70., -60., -50.,
    -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., 90., -90., -80., -70., -60., -50., -40., -30., -20., -10.,
    0., 10., 20., 30., 40., 50., 60., 70., 80., 90., -100., -90., -80., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30.,
    40., 50., 60., 70., 80., 90., -100., -90., -80., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70.,
    80., -110., -100., -90., -80., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., -110., -100.,
    -90., -80., -70., -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., 80., -110., -100., -90., -80., -70.,
    -60., -50., -40., -30., -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., -110., -100., -90., -80., -70., -60., -50., -40., -30.,
    -20., -10., 0., 10., 20., 30., 40., 50., 60., 70., -90., -80., -70., -60., -50., -40., -30., -20., -10., 0.
};

int main(int argc, char **argv)
{
    
    string videoFileName;
    // 从命令行获取参数
    if (argc < 2)
    {
        cout << "Please input the greyscale video filename." << endl;
        cout << "Usage example: ./colorizeVideo.out greyscaleVideo.mp4" << endl;
        return 1;
    }
    videoFileName = argv[1];
    
    cv::VideoCapture cap(videoFileName);
    if (!cap.isOpened())
    {
        cerr << "Unable to open video" << endl;
        return 1;
    }
    
    string protoFile = "./models/colorization_deploy_v2.prototxt";
    string weightsFile = "./models/colorization_release_v2.caffemodel";
    //string weightsFile = "./models/colorization_release_v2_norebal.caffemodel";

    Mat frame, frameCopy;
    int frameWidth = cap.get(CAP_PROP_FRAME_WIDTH);
    int frameHeight = cap.get(CAP_PROP_FRAME_HEIGHT);
    
    string str = videoFileName;
    str.replace(str.end()-4, str.end(), "");
    string outVideoFileName = str+"_colorized.avi";
    VideoWriter video(outVideoFileName, VideoWriter::fourcc('M','J','P','G'), 60, Size(frameWidth,frameHeight));

    // 预训练网络的固定输入大小
    const int W_in = 224;
    const int H_in = 224;
    Net net = dnn::readNetFromCaffe(protoFile, weightsFile);

    // 设置附加层
    int sz[] = {2, 313, 1, 1};
    const Mat pts_in_hull(4, sz, CV_32F, hull_pts);
    Ptr<dnn::Layer> class8_ab = net.getLayer("class8_ab");
    class8_ab->blobs.push_back(pts_in_hull);
    Ptr<dnn::Layer> conv8_313_rh = net.getLayer("conv8_313_rh");
    conv8_313_rh->blobs.push_back(Mat(1, 313, CV_32F, Scalar(2.606)));

    for(;;)
    {

        cap >> frame;
        if (frame.empty()) break;
        
        frameCopy = frame.clone();
        
        // 提取L通道并减去平均值
        Mat lab, L, input;
        frame.convertTo(frame, CV_32F, 1.0/255);
        cvtColor(frame, lab, COLOR_BGR2Lab);
        extractChannel(lab, L, 0);
        resize(L, input, Size(W_in, H_in));
        input -= 50;
        
        // 通过网络运行L通道
        Mat inputBlob = blobFromImage(input);
        net.setInput(inputBlob);
        Mat result = net.forward();
        
        // 从网络输出中提取计算出的 a,b 通道
        Size siz(result.size[2], result.size[3]);
        Mat a = Mat(siz, CV_32F, result.ptr(0,0));
        Mat b = Mat(siz, CV_32F, result.ptr(0,1));
        
        resize(a, a, frame.size());
        resize(b, b, frame.size());
        
        // 合并,并转换回 BGR
        Mat coloredFrame, chn[] = {L, a, b};
        merge(chn, 3, lab);
        cvtColor(lab, coloredFrame, COLOR_Lab2BGR);
        
        coloredFrame = coloredFrame*255;
        coloredFrame.convertTo(coloredFrame, CV_8U);
        video.write(coloredFrame);

    }
    cout << "Colorized video saved as " << outVideoFileName << endl << "Done !!!" << endl;
    cap.release();
    video.release();

    return 0;
}

7.代码解析

7.1读取模型

我们在代码中提供了protoFile和weightsFile的路径。选择合适的模型,这取决于你是否想要使用颜色平衡。我们默认使用颜色再平衡模型。读取输入图像并定义网络的输入大小为224×224。将网络读入内存。
python

# 指定模型文件的路径
protoFile = "./models/colorization_deploy_v2.prototxt"
weightsFile = "./models/colorization_release_v2.caffemodel"
#weightsFile = "./models/colorization_release_v2_norebal.caffemodel";

# 读取输入图像
frame = cv.imread("./dog-greyscale.png")

W_in = 224
H_in = 224

# 将网络读入内存
net = cv.dnn.readNetFromCaffe(protoFile, weightsFile)

C++

// 指定 2 个文件的路径
string protoFile = "./models/colorization_deploy_v2.prototxt";
string weightsFile = "./models/colorization_release_v2.caffemodel";
//string weightsFile = "./models/colorization_release_v2_norebal.caffemodel";

Mat img = imread(imageFile);
const int W_in = 224;
const int H_in = 224;

// 将网络读入内存
Net net = readNetFromCaffe(protoFile, weightsFile);

7.1加载量化的 bin 聚类中心

接下来,我们加载量化的 bin 聚类中心。然后我们为313个bin聚类中心中的每个聚类中心分配1x1核,并将它们分配到网络中相应的层。最后,我们添加一个非零值的缩放层。
python

# 加载 bin 聚类中心
pts_in_hull = np.load('./pts_in_hull.npy')

# 将聚类中心填充为 1x1 卷积核
pts_in_hull = pts_in_hull.transpose().reshape(2, 313, 1, 1)
net.getLayer(net.getLayerId('class8_ab')).blobs = [pts_in_hull.astype(np.float32)]
net.getLayer(net.getLayerId('conv8_313_rh')).blobs = [np.full([1, 313], 2.606, np.float32)]

C++

// 将聚类中心填充为 1x1 卷积核
int sz[] = {2, 313, 1, 1};
const Mat pts_in_hull(4, sz, CV_32F, hull_pts);
Ptr<dnn::Layer> class8_ab = net.getLayer("class8_ab");
class8_ab->blobs.push_back(pts_in_hull);
Ptr<dnn::Layer> conv8_313_rh = net.getLayer("conv8_313_rh");
conv8_313_rh->blobs.push_back(Mat(1, 313, CV_32F, Scalar(2.606)));

7.3将图像转换为 CIE Lab 色彩空间

对输入的RGB图像进行缩放,使其值在0-1的范围内,然后将其转换为Lab颜色空间并提取出亮度通道。
python

# 将输入图像的 rgb 值转换为 0 到 1 的范围
img_rgb = (frame[:,:,[2, 1, 0]] * 1.0 / 255).astype(np.float32)
img_lab = cv.cvtColor(img_rgb, cv.COLOR_RGB2Lab)
img_l = img_lab[:,:,0] # 拉出L通道

C++

Mat lab, L, input;
img.convertTo(img, CV_32F, 1.0/255);
cvtColor(img, lab, COLOR_BGR2Lab);
extractChannel(lab, L, 0);

原始图像中的亮度通道被调整为网络输入大小,在这种情况下为 (224,224)。通常,亮度通道的范围是 0 到 100。所以我们减去 50 使其以 0 为中心。
python

# 将亮度通道调整为网络输入大小
img_l_rs = cv.resize(img_l, (W_in, H_in)) 
img_l_rs -= 50 # 中心化

C++

Mat lab, L, input;
img.convertTo(img, CV_32F, 1.0/255);
cvtColor(img, lab, COLOR_BGR2Lab);
extractChannel(lab, L, 0);

然后我们将缩放后的均值中心亮度通道作为前向传递的输入提供给网络。前向传递的输出是图像的预测 ab 通道。将其缩放回原始图像大小,然后与原始大小的亮度图像(在原始分辨率中较早提取)合并以获得输出 Lab 图像。然后将其转换为 RGB 颜色空间以获得最终的彩色图像。然后我们可以保存输出图像。
python

net.setInput(cv.dnn.blobFromImage(img_l_rs))
ab_dec = net.forward()[0,:,:,:].transpose((1,2,0)) # 结果

(H_orig,W_orig) = img_rgb.shape[:2] # 原始图像尺寸
ab_dec_us = cv.resize(ab_dec, (W_orig, H_orig))
img_lab_out = np.concatenate((img_l[:,:,np.newaxis],ab_dec_us),axis=2) # 与原始图像 L 拼接
img_bgr_out = np.clip(cv.cvtColor(img_lab_out, cv.COLOR_Lab2BGR), 0, 1)

cv.imwrite('dog_colorized.png', cv.resize(img_bgr_out*255, imshowSize))

C++

// 通过网络运行L通道
Mat inputBlob = blobFromImage(input);
net.setInput(inputBlob);
Mat result = net.forward();

// 从网络输出中提取计算出的 a,b 通道并将它们调整为原始图像大小
Size siz(result.size[2], result.size[3]);
Mat a = Mat(siz, CV_32F, result.ptr(0,0));
Mat b = Mat(siz, CV_32F, result.ptr(0,1));
resize(a, a, img.size());
resize(b, b, img.size());

// 合并并转换回 BGR
Mat color, chn[] = {L, a, b};
merge(chn, 3, lab);
cvtColor(lab, color, COLOR_Lab2BGR);

结果展示

在这里插入图片描述
参考目录
https://learnopencv.com/convolutional-neural-network-based-image-colorization-using-opencv/
https://github.com/richzhang/colorization/blob/caffe/colorization/demo/colorization_demo_v2.ipynb

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值