这是调用代码,windows平台的.
unsigned* fun(const char *path)
{
unsigned *result=new unsigned[75];
int l=strlen(path);
char * pp=new char[l];
for(int i=0;i 一、图像平滑处理简介图像平滑处理属于图像空间滤波的一种,用于模糊处理和降低噪声。模糊处理经常用于图像预处理任务中,例如在(大)目标提取之前去除图像中的一些琐碎细节,以及桥接直线或曲线的缝隙。模糊处理后的图像,可以通过阈值处理、形态处理等方式进行再加工,从而去除一些噪点。平滑滤波器包括线性滤波器和非线性滤波器,平滑线性空间滤波器的输出(响应)是包含在滤波器模板邻域内的像素的简单平均值。平滑线性空间滤波器有时也称为均值滤波器,它们属于低通滤波器。平滑线性滤波器的基本概念非常直观。它使用滤波器模板确定的邻域内像素的平均/加权平均灰度值代替图像中每个像素的值。所有系数都相等(非加权平均)的空间均值滤波器也称为盒状滤波器。非线性滤波器可能有多种,统计排序滤波器是常用的,如中值滤波、最小值滤波(如图像腐蚀)、最大值滤波(如图像膨胀)都属于统计排序滤波器。更多关于图像平滑处理知识的介绍请参考《数字图像处理:线性和非线性滤波的平滑空间滤波器(Smoothing Spatial Filters)》的介绍。二、filter2D介绍2.1、简介filter2D是OpenCV使用卷积核对图像进行卷积运算的函数,该函数能对图像进行任意的线性滤波处理,具体滤波方式由核矩阵确认。该函数其实执行的是相关操作而不是卷积操作,计算公式如下:关于相关和卷积的关系请参考《《数字图像处理》空间滤波学习感悟2:空间相关与卷积的概念、区别及联系》,不过对应系数相等的盒状滤波来说,由于核矩阵的对称性,卷积和相关的处理结果相同。2.2、语法说明语法dst = cv.filter2D( src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]] )参数说明src:输入图像ddepth:目标图像深度(请参考《图像表示的相关概念:图像深度、像素深度、位深的区别和关系》),如果目标图像深度和输入图像深度相同,则传值-1,老猿测试在Python中此时取值None、0效果也一样。针对输入图像对应的目标图像,该参数的可选传值对应关系如下:kernel:卷积核(convolution kernel ),如上概述所述,实际上是相关核(correlation kernel),为一个单通道的浮点数矩阵,如果针对图像不同通道需要使用不同核,则需要将图像进行split拆分成单通道并使用对应核逐个进行处理dst:结果图像anchor:核矩阵的锚点,用于定位核距中与当前处理像素点对齐的点,默认值(-1,-1)表示锚点位于内核中心,否则就是核矩阵锚点位置坐标,锚点位置对卷积处理的结果会有非常大的影响;delta:在将卷积处理后的像素值存储到dst之前,向其添加的可选值,老猿测试验证当有值时,卷积后的像素结果值会与delta相加,得到的结果作为最终输出的像素值,注意这个加法是饱和运算,超过255的被置为255;borderType:当要扩充输入图像矩阵边界时的像素取值方法,当核矩阵锚点与像素重合但核矩阵覆盖范围超出到图像外时,函数可以根据指定的边界模式进行插值运算。可选模式包括:注意:BORDER_WRAP在此不支持;经老猿测试,默认值为BORDER_DEFAULT ,与BORDER_REFLECT_101 、BORDER_REFLECT101相同2.4、返回值返回值为结果图像矩阵,因此输入参数中的dst参数无需输入。三、使用案例下面的案例脱胎于OpenCV帮助文档,代码对输入图像进行均值滤波:import cv2
import numpy as np
import matplotlib.pyplot as plt
def smoothingByOpenCV():
img = imread('f:\\pic\\opencvLogo.JPG')
kernal = np.ones((5, 5), np.float32) / 25
dst = cv2.filter2D(img, None, kernal)
plt.subplot(121), plt.imshow(img), plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(dst), plt.title('Averaging')
plt.xticks([]), plt.yticks([])
plt.show()
return dst
smoothingByOpenCV()结果输出:可以看到输出图像比输入图像变模糊了。上面的代码kernal中各元素相加结果为1,没有改变图像整体的亮度,当将其改为:kernal = np.ones((5, 5), np.float32) / 5此时,输出结果如下:可以看到图像的整体零度提升,并扩展了前景色范围,这是因为卷积过程中卷积核的元素值变大导致卷积结果值相比原值整体变大导致的。如果不改变kernal,而改变delta参数,如: kernal = np.ones((5, 5), np.float32) / 25
dst = cv2.filter2D(img, None, kernal,delta=250)则输出图像为:这是因为delta设置为250后,导致结果图像大部分像素值达到饱和导致的。当然filter2D不只是用于均值滤波,所有线性滤波都可以实现,只需要将核矩阵根据滤波任务预置不同的元素即可。四、小结本文介绍了图像平滑处理及均值滤波等基础概念,并详细介绍了卷积函数filter2D的Python语法及参数,并用之进行了对图像的均值滤波处理,可以看到卷积核元素值以及相关参数如delta等对卷积处理结果的影响。整体卷积过程是将核矩阵和处理图像从左到右、从上到下移动逐一计算像素的卷积结果过程,为了更直观的了解卷积处理,老猿用Python、numpy矩阵运算以及OpenCV-Python的图像基础操作模拟实现了一个卷积程序,其效果与filter2D基本功能完全等价。在这个过程中用到了一些小技巧,有兴趣的同好请参考《卷积处理过程模拟:用Python实现OpenCV函数filter2D等效的卷积功能》一文的介绍。 一、引言最近收到几个网友提供OpenCV中CLAHE的源代码的请求,在此直接将OpenCV4.54版本CLAHE.CPP的源码分享出来。二、OpenCV源代码的下载下载地址:https://sourceforge.net/projects/opencvlibrary/files/有3.4.10–4.5.4的版本,但下载很慢,老猿费了很大的劲,大家可以考虑专门的下载工具下载。如果实在下不下来,请关注老猿Python的微信公号给老猿发消息。三、CLAHE C++源代码/*M///
//
// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
//
// By downloading, copying, installing or using the software you agree to this license.
// If you do not agree to this license, do not download, install,
// copy or use the software.
//
//
// License Agreement
// For Open Source Computer Vision Library
//
// Copyright (C) 2013, NVIDIA Corporation, all rights reserved.
// Copyright (C) 2014, Itseez Inc., all rights reserved.
// Third party copyrights are property of their respective owners.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
// * Redistribution's of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistribution's in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * The name of the copyright holders may not be used to endorse or promote products
// derived from this software without specific prior written permission.
//
// This software is provided by the copyright holders and contributors "as is" and
// any express or implied warranties, including, but not limited to, the implied
// warranties of merchantability and fitness for a particular purpose are disclaimed.
// In no event shall the copyright holders or contributors be liable for any direct,
// indirect, incidental, special, exemplary, or consequential damages
// (including, but not limited to, procurement of substitute goods or services;
// loss of use, data, or profits; or business interruption) however caused
// and on any theory of liability, whether in contract, strict liability,
// or tort (including negligence or otherwise) arising in any way out of
// the use of this software, even if advised of the possibility of such damage.
//
//M*/
#include "precomp.hpp"
#include "opencl_kernels_imgproc.hpp"
// ----------------------------------------------------------------------
// CLAHE
#ifdef HAVE_OPENCL
namespace clahe
{
static bool calcLut(cv::InputArray _src, cv::OutputArray _dst,
const int tilesX, const int tilesY, const cv::Size tileSize,
const int clipLimit, const float lutScale)
{
cv::ocl::Kernel k("calcLut", cv::ocl::imgproc::clahe_oclsrc);
if(k.empty())
return false;
cv::UMat src = _src.getUMat();
_dst.create(tilesX * tilesY, 256, CV_8UC1);
cv::UMat dst = _dst.getUMat();
int tile_size[2];
tile_size[0] = tileSize.width;
tile_size[1] = tileSize.height;
size_t localThreads[3] = { 32, 8, 1 };
size_t globalThreads[3] = { tilesX * localThreads[0], tilesY * localThreads[1], 1 };
int idx = 0;
idx = k.set(idx, cv::ocl::KernelArg::ReadOnlyNoSize(src));
idx = k.set(idx, cv::ocl::KernelArg::WriteOnlyNoSize(dst));
idx = k.set(idx, tile_size);
idx = k.set(idx, tilesX);
idx = k.set(idx, clipLimit);
k.set(idx, lutScale);
return k.run(2, globalThreads, localThreads, false);
}
static bool transform(cv::InputArray _src, cv::OutputArray _dst, cv::InputArray _lut,
const int tilesX, const int tilesY, const cv::Size & tileSize)
{
cv::ocl::Kernel k("transform", cv::ocl::imgproc::clahe_oclsrc);
if(k.empty())
return false;
int tile_size[2];
tile_size[0] = tileSize.width;
tile_size[1] = tileSize.height;
cv::UMat src = _src.getUMat();
_dst.create(src.size(), src.type());
cv::UMat dst = _dst.getUMat();
cv::UMat lut = _lut.getUMat();
size_t localThreads[3] = { 32, 8, 1 };
size_t globalThreads[3] = { (size_t)src.cols, (size_t)src.rows, 1 };
int idx = 0;
idx = k.set(idx, cv::ocl::KernelArg::ReadOnlyNoSize(src));
idx = k.set(idx, cv::ocl::KernelArg::WriteOnlyNoSize(dst));
idx = k.set(idx, cv::ocl::KernelArg::ReadOnlyNoSize(lut));
idx = k.set(idx, src.cols);
idx = k.set(idx, src.rows);
idx = k.set(idx, tile_size);
idx = k.set(idx, tilesX);
k.set(idx, tilesY);
return k.run(2, globalThreads, localThreads, false);
}
}
#endif
namespace
{
template <class T, int histSize, int shift>
class CLAHE_CalcLut_Body : public cv::ParallelLoopBody
{
public:
CLAHE_CalcLut_Body(const cv::Mat& src, const cv::Mat& lut, const cv::Size& tileSize, const int& tilesX, const int& clipLimit, const float& lutScale) :
src_(src), lut_(lut), tileSize_(tileSize), tilesX_(tilesX), clipLimit_(clipLimit), lutScale_(lutScale)
{
}
void operator ()(const cv::Range& range) const CV_OVERRIDE;
private:
cv::Mat src_;
mutable cv::Mat lut_;
cv::Size tileSize_;
int tilesX_;
int clipLimit_;
float lutScale_;
};
template <class T, int histSize, int shift>
void CLAHE_CalcLut_Body<T,histSize,shift>::operator ()(const cv::Range& range) const
{
T* tileLut = lut_.ptr<T>(range.start);
const size_t lut_step = lut_.step / sizeof(T);
for (int k = range.start; k < range.end; ++k, tileLut += lut_step)
{
const int ty = k / tilesX_;
const int tx = k % tilesX_;
// retrieve tile submatrix
cv::Rect tileROI;
tileROI.x = tx * tileSize_.width;
tileROI.y = ty * tileSize_.height;
tileROI.width = tileSize_.width;
tileROI.height = tileSize_.height;
const cv::Mat tile = src_(tileROI);
// calc histogram
cv::AutoBuffer<int> _tileHist(histSize);
int* tileHist = _tileHist.data();
std::fill(tileHist, tileHist + histSize, 0);
int height = tileROI.height;
const size_t sstep = src_.step / sizeof(T);
for (const T* ptr = tile.ptr<T>(0); height--; ptr += sstep)
{
int x = 0;
for (; x <= tileROI.width - 4; x += 4)
{
int t0 = ptr[x], t1 = ptr[x+1];
tileHist[t0 >> shift]++; tileHist[t1 >> shift]++;
t0 = ptr[x+2]; t1 = ptr[x+3];
tileHist[t0 >> shift]++; tileHist[t1 >> shift]++;
}
for (; x < tileROI.width; ++x)
tileHist[ptr[x] >> shift]++;
}
// clip histogram
if (clipLimit_ > 0)
{
// how many pixels were clipped
int clipped = 0;
for (int i = 0; i < histSize; ++i)
{
if (tileHist[i] > clipLimit_)
{
clipped += tileHist[i] - clipLimit_;
tileHist[i] = clipLimit_;
}
}
// redistribute clipped pixels
int redistBatch = clipped / histSize;
int residual = clipped - redistBatch * histSize;
for (int i = 0; i < histSize; ++i)
tileHist[i] += redistBatch;
if (residual != 0)
{
int residualStep = MAX(histSize / residual, 1);
for (int i = 0; i < histSize && residual > 0; i += residualStep, residual--)
tileHist[i]++;
}
}
// calc Lut
int sum = 0;
for (int i = 0; i < histSize; ++i)
{
sum += tileHist[i];
tileLut[i] = cv::saturate_cast<T>(sum * lutScale_);
}
}
}
template <class T, int shift>
class CLAHE_Interpolation_Body : public cv::ParallelLoopBody
{
public:
CLAHE_Interpolation_Body(const cv::Mat& src, const cv::Mat& dst, const cv::Mat& lut, const cv::Size& tileSize, const int& tilesX, const int& tilesY) :
src_(src), dst_(dst), lut_(lut), tileSize_(tileSize), tilesX_(tilesX), tilesY_(tilesY)
{
buf.allocate(src.cols << 2);
ind1_p = buf.data();
ind2_p = ind1_p + src.cols;
xa_p = (float *)(ind2_p + src.cols);
xa1_p = xa_p + src.cols;
int lut_step = static_cast<int>(lut_.step / sizeof(T));
float inv_tw = 1.0f / tileSize_.width;
for (int x = 0; x < src.cols; ++x)
{
float txf = x * inv_tw - 0.5f;
int tx1 = cvFloor(txf);
int tx2 = tx1 + 1;
xa_p[x] = txf - tx1;
xa1_p[x] = 1.0f - xa_p[x];
tx1 = std::max(tx1, 0);
tx2 = std::min(tx2, tilesX_ - 1);
ind1_p[x] = tx1 * lut_step;
ind2_p[x] = tx2 * lut_step;
}
}
void operator ()(const cv::Range& range) const CV_OVERRIDE;
private:
cv::Mat src_;
mutable cv::Mat dst_;
cv::Mat lut_;
cv::Size tileSize_;
int tilesX_;
int tilesY_;
cv::AutoBuffer<int> buf;
int * ind1_p, * ind2_p;
float * xa_p, * xa1_p;
};
template <class T, int shift>
void CLAHE_Interpolation_Body<T, shift>::operator ()(const cv::Range& range) const
{
float inv_th = 1.0f / tileSize_.height;
for (int y = range.start; y < range.end; ++y)
{
const T* srcRow = src_.ptr<T>(y);
T* dstRow = dst_.ptr<T>(y);
float tyf = y * inv_th - 0.5f;
int ty1 = cvFloor(tyf);
int ty2 = ty1 + 1;
float ya = tyf - ty1, ya1 = 1.0f - ya;
ty1 = std::max(ty1, 0);
ty2 = std::min(ty2, tilesY_ - 1);
const T* lutPlane1 = lut_.ptr<T>(ty1 * tilesX_);
const T* lutPlane2 = lut_.ptr<T>(ty2 * tilesX_);
for (int x = 0; x < src_.cols; ++x)
{
int srcVal = srcRow[x] >> shift;
int ind1 = ind1_p[x] + srcVal;
int ind2 = ind2_p[x] + srcVal;
float res = (lutPlane1[ind1] * xa1_p[x] + lutPlane1[ind2] * xa_p[x]) * ya1 +
(lutPlane2[ind1] * xa1_p[x] + lutPlane2[ind2] * xa_p[x]) * ya;
dstRow[x] = cv::saturate_cast<T>(res) << shift;
}
}
}
class CLAHE_Impl CV_FINAL : public cv::CLAHE
{
public:
CLAHE_Impl(double clipLimit = 40.0, int tilesX = 8, int tilesY = 8);
void apply(cv::InputArray src, cv::OutputArray dst) CV_OVERRIDE;
void setClipLimit(double clipLimit) CV_OVERRIDE;
double getClipLimit() const CV_OVERRIDE;
void setTilesGridSize(cv::Size tileGridSize) CV_OVERRIDE;
cv::Size getTilesGridSize() const CV_OVERRIDE;
void collectGarbage() CV_OVERRIDE;
private:
double clipLimit_;
int tilesX_;
int tilesY_;
cv::Mat srcExt_;
cv::Mat lut_;
#ifdef HAVE_OPENCL
cv::UMat usrcExt_;
cv::UMat ulut_;
#endif
};
CLAHE_Impl::CLAHE_Impl(double clipLimit, int tilesX, int tilesY) :
clipLimit_(clipLimit), tilesX_(tilesX), tilesY_(tilesY)
{
}
void CLAHE_Impl::apply(cv::InputArray _src, cv::OutputArray _dst)
{
CV_INSTRUMENT_REGION();
CV_Assert( _src.type() == CV_8UC1 || _src.type() == CV_16UC1 );
#ifdef HAVE_OPENCL
bool useOpenCL = cv::ocl::isOpenCLActivated() && _src.isUMat() && _src.dims()<=2 && _src.type() == CV_8UC1;
#endif
int histSize = _src.type() == CV_8UC1 ? 256 : 65536;
cv::Size tileSize;
cv::_InputArray _srcForLut;
if (_src.size().width % tilesX_ == 0 && _src.size().height % tilesY_ == 0)
{
tileSize = cv::Size(_src.size().width / tilesX_, _src.size().height / tilesY_);
_srcForLut = _src;
}
else
{
#ifdef HAVE_OPENCL
if(useOpenCL)
{
cv::copyMakeBorder(_src, usrcExt_, 0, tilesY_ - (_src.size().height % tilesY_), 0, tilesX_ - (_src.size().width % tilesX_), cv::BORDER_REFLECT_101);
tileSize = cv::Size(usrcExt_.size().width / tilesX_, usrcExt_.size().height / tilesY_);
_srcForLut = usrcExt_;
}
else
#endif
{
cv::copyMakeBorder(_src, srcExt_, 0, tilesY_ - (_src.size().height % tilesY_), 0, tilesX_ - (_src.size().width % tilesX_), cv::BORDER_REFLECT_101);
tileSize = cv::Size(srcExt_.size().width / tilesX_, srcExt_.size().height / tilesY_);
_srcForLut = srcExt_;
}
}
const int tileSizeTotal = tileSize.area();
const float lutScale = static_cast<float>(histSize - 1) / tileSizeTotal;
int clipLimit = 0;
if (clipLimit_ > 0.0)
{
clipLimit = static_cast<int>(clipLimit_ * tileSizeTotal / histSize);
clipLimit = std::max(clipLimit, 1);
}
#ifdef HAVE_OPENCL
if (useOpenCL && clahe::calcLut(_srcForLut, ulut_, tilesX_, tilesY_, tileSize, clipLimit, lutScale) )
if( clahe::transform(_src, _dst, ulut_, tilesX_, tilesY_, tileSize) )
{
CV_IMPL_ADD(CV_IMPL_OCL);
return;
}
#endif
cv::Mat src = _src.getMat();
_dst.create( src.size(), src.type() );
cv::Mat dst = _dst.getMat();
cv::Mat srcForLut = _srcForLut.getMat();
lut_.create(tilesX_ * tilesY_, histSize, _src.type());
cv::Ptr<cv::ParallelLoopBody> calcLutBody;
if (_src.type() == CV_8UC1)
calcLutBody = cv::makePtr<CLAHE_CalcLut_Body<uchar, 256, 0> >(srcForLut, lut_, tileSize, tilesX_, clipLimit, lutScale);
else if (_src.type() == CV_16UC1)
calcLutBody = cv::makePtr<CLAHE_CalcLut_Body<ushort, 65536, 0> >(srcForLut, lut_, tileSize, tilesX_, clipLimit, lutScale);
else
CV_Error( CV_StsBadArg, "Unsupported type" );
cv::parallel_for_(cv::Range(0, tilesX_ * tilesY_), *calcLutBody);
cv::Ptr<cv::ParallelLoopBody> interpolationBody;
if (_src.type() == CV_8UC1)
interpolationBody = cv::makePtr<CLAHE_Interpolation_Body<uchar, 0> >(src, dst, lut_, tileSize, tilesX_, tilesY_);
else if (_src.type() == CV_16UC1)
interpolationBody = cv::makePtr<CLAHE_Interpolation_Body<ushort, 0> >(src, dst, lut_, tileSize, tilesX_, tilesY_);
cv::parallel_for_(cv::Range(0, src.rows), *interpolationBody);
}
void CLAHE_Impl::setClipLimit(double clipLimit)
{
clipLimit_ = clipLimit;
}
double CLAHE_Impl::getClipLimit() const
{
return clipLimit_;
}
void CLAHE_Impl::setTilesGridSize(cv::Size tileGridSize)
{
tilesX_ = tileGridSize.width;
tilesY_ = tileGridSize.height;
}
cv::Size CLAHE_Impl::getTilesGridSize() const
{
return cv::Size(tilesX_, tilesY_);
}
void CLAHE_Impl::collectGarbage()
{
srcExt_.release();
lut_.release();
#ifdef HAVE_OPENCL
usrcExt_.release();
ulut_.release();
#endif
}
}
cv::Ptr<cv::CLAHE> cv::createCLAHE(double clipLimit, cv::Size tileGridSize)
{
return makePtr<CLAHE_Impl>(clipLimit, tileGridSize.width, tileGridSize.height);
} 一、图像平滑处理简介图像平滑处理的基本概念非常直观,它使用滤波器模板确定的邻域内像素的平均/加权平均灰度值代替图像中每个像素的值。平滑线处理滤波器也称均值滤波器,所有系数都相等(非加权平均)的空间均值滤波器也称为盒状滤波器。在《OpenCV-Python 图像平滑处理1:卷积函数filter2D详解及用于均值滤波的案例》介绍了使用filter2D实现图像平滑处理,本文将介绍另外一个OpenCV-Python的函数blur实现平滑处理。二、blur介绍2.1、简介blur是OpenCV用于进行图像模糊处理的函数,该函数使用归一化的盒装滤波器进行均值滤波处理。盒状滤波器的所有元素都相等,其元素为浮点数。blur的核矩阵进行了归一化处理,每个元素值=1/(滤波器核高×核宽),因此核矩阵的所有元素和值为1。对系数相等的盒状滤波来说,由于核矩阵的对称性,卷积和相关的处理结果相同。关于相关和卷积的关系请参考《《数字图像处理》空间滤波学习感悟2:空间相关与卷积的概念、区别及联系》的介绍。2.2、语法说明语法dst = cv.blur( src, ksize[, dst[, anchor[, borderType]]] ) 参数说明src:输入图像,可以是任何通道数的图像,处理时是各通道拆分后单独处理,但图像深度必须是CV_8U, CV_16U, CV_16S, CV_32F 或CV_64F;dst:结果图像,其大小和类型都与输入图像相同;ksize:卷积核(convolution kernel )矩阵大小,如上概述所述,实际上是相关核(correlation kernel),为一个单通道的浮点数矩阵,如果针对图像不同通道需要使用不同核,则需要将图像进行split拆分成单通道并使用对应核逐个进行处理anchor:核矩阵的锚点,用于定位核距中与当前处理像素点对齐的点,默认值(-1,-1),表示锚点位于内核中心,否则就是核矩阵锚点位置坐标,锚点位置对卷积处理的结果会有非常大的影响;borderType:当要扩充输入图像矩阵边界时的像素取值方法,当核矩阵锚点与像素重合但核矩阵覆盖范围超出到图像外时,函数可以根据指定的边界模式进行插值运算。可选模式包括:注意:BORDER_WRAP在此不支持;默认值为BORDER_DEFAULT ,与BORDER_REFLECT_101 、BORDER_REFLECT101相同2.4、返回值返回值为结果图像矩阵,因此输入参数中的dst参数无需输入。从以上介绍可知,blur函数就是在《OpenCV-Python 图像平滑处理1:卷积函数filter2D详解及用于均值滤波的案例》介绍的filter2D的一种用于均值滤波的特定应用。三、使用案例下面的案例脱胎于OpenCV帮助文档,代码对输入图像进行均值滤波:import cv2
import numpy as np
from opencvPublic import cmpMatrix
def smoothingByFiler2D():
img = cv2.imread('f:\\pic\\opencvLogo.JPG')
kernal = np.ones((5, 5), np.float32) / 25
dst = cv2.filter2D(img, None, kernal,delta=0)
return dst
def smoothingByBlur():
img = cv2.imread('f:\\pic\\opencvLogo.JPG')
ksize = (5,5)
dst = cv2.blur(img, ksize)
plt.subplot(121), plt.imshow(img), plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(dst), plt.title('Blurred')
plt.xticks([]), plt.yticks([])
plt.show()
return dst
d1 = smoothingByBlur()
d2 = smoothingByFiler2D()
if(cmpMatrix(d1,d2)): #对比两个结果矩阵是否一致
print('d1==d2')
else: print('d1!=d2')结果输出:文字输出:d1==d2可以看到输出图像比输入图像变模糊了,且blur处理的结果矩阵与filter2D处理的结果完全一样。四、小结本文介绍了图像平滑处理及均值滤波等基础概念,并详细介绍了卷积函数blur的Python语法及参数,并用之进行了对图像的均值滤波平滑处理,可以看到其模糊化处理结果与filter2D完全一样,实际上它是filter2D一种特定场景的应用。 一、引言如今短视频和自媒体大行其道,不会点视频剪辑技能都不好说自己会玩自媒体,音视频剪辑工具大受欢迎,作为万能的编程语言 Python,也早就有了自己的音视频剪辑库 Moviepy。MoviePy 能处理的视频是 ffmpeg 格式的,支持的文件类型包括:*.mp4 *.wmv *.rm *.avi *.flv *.webm *.wav *.rmvb 等 ,可用于进行视频的剪切、拼接、标题插入、视频合成、视频处理或创建高级效果,同时更适合批量进行视频剪辑处理。OpenCV 是一个基于 Apache2.0 许可(开源)发行的跨平台计算机视觉和机器学习软件开源库,可以运行在 Linux、Windows、Android 和 Mac OS 操作系统上。 它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,实现了图像处理和计算机视觉方面的很多通用算法。OpenCV-Python 是 OpenCV 适配 Python 的一个图像处理和计算机视觉处理库。二、一些 Moviepy 无法支持可由 OpenCV 完成的视频处理场景我们知道视频是一帧帧图像和音频构成的,在进行视频处理时,当 Moviepy 无法完成的一些处理,就可以借用 OpenCV 来完成,二者的结合可以制作一些复杂的高级特效。下面老猿列举一些 Moviepy 无法支持可借由 OpenCV 完成的特效:对图像进行灰度变换,例如直方图均衡,以调整视频的对比度以及均衡图像的背景色在视频内容的任意位置增加特定文字或几何图形,如形成弹幕效果对视频内容进行特定的透视变换对彩色视频三色进行分离修复视频背景的噪点进行复杂的背景处理,如增加雪花飘落效果将灰度视频转成彩色视频…只要是图像处理中能用的技术在视频中都可以使用,而不是简单的视频合成。\三、Moviepy 结合 OpenCV-Python 的音视频剪辑开发模式要实现 Moviepy 结合 OpenCV-Python 的音视频剪辑处理,可以按照构建单独图像处理函数、调用 fl_image 进行剪辑的帧图像处理、输出剪辑内容三个步骤来实现。3.1、图像处理函数图像处理函数是用于真正对剪辑的每帧图像进行剪辑加工的函数,当每帧图像的处理模式统一时,可以使用单一的函数来进行图像处理。图像处理函数的名字只要符合 Python 的函数命名要求就行,但该函数只能带一个参数和输出一个结果,输入参数就是要处理图像对应的 numpy 矩阵,输出结果就是加工处理后的结果图像,也是 numpy 矩阵。下面是一给图像加入雪花特效的函数示例:def addSnowEffectToImg(img):
"""
将所有snowObjects中的雪花对象融合放到图像img中,融合时y坐标随机下移一定高度,x坐标左右随机小范围内移动
"""
global snowShapesList,snowObjects
horizontalMaxDistance,verticalMaxDistance = 5,10 #水平方向左右漂移最大值和竖直方向下落最大值
rows,cols = img.shape[:2]
maxObjsPerRow = int(cols/100)
snowObjects += generateOneRowSnows(cols, random.randint(0, maxObjsPerRow))
snowObjectCount = len(snowObjects)
rows,cols = img.shape[0:2]
imgResult = np.array(img)
for index in range(snowObjectCount-1,-1,-1):
imgObj = snowObjects[index] #每个元素为(imgId,x,y)
if imgObj[2]>rows: #如果雪花的起始纵坐标已经超出背景图像的高度(即到达背景图像底部),则该雪花对象需进行失效处理
del(snowObjects[index])
else:
imgSnow = snowShapesList[imgObj[0]]
x,y = imgObj[1:] #取该雪花上次的位置
x = x+random.randint(-1*horizontalMaxDistance,horizontalMaxDistance) #横坐标随机左右移动一定范围
y = y+random.randint(1,verticalMaxDistance) #纵坐标随机下落一定范围
snowObjects[index] = (imgObj[0],x,y) #更新雪花对象信息
imgResult = addImgToLargeImg(imgSnow,imgResult,(x,y),180) #将所有雪花对象图像按照其位置融合到背景图像中
return imgResult 3.2、调用图像处理函数加工剪辑的每帧图像并输出目标剪辑moviepy 音视频剪辑模块的视频剪辑基类 VideoClip 的 fl_image 方法用于进行对剪辑帧数据进行变换。调用语法:fl_image(self, image_func, apply_to=None)。参数说明:image_func:参数 image_func 是对剪辑帧进行图像变换的函数,带一个参数,参数就是要处理的帧图像 numpy 矩阵,image_func 函数的返回值为经过变换后的帧apply_to:apply_to 表示变换是否需要同时作用于剪辑的音频和遮罩,其值可以为’mask’、‘audio’、[‘mask’,‘audio’]在实现剪辑加工处理时,只需要将上面的图像函数作为参数传递给 fl_image,就可以对整个剪辑进行帧图像的变换处理。如下面的示例代码就是调用上面给图像加雪花特效的函数 addSnowEffectToImg:def addVideoSnowEffect(videoFileName,resultFileName):
clip = VideoFileClip(videoFileName)
newclip = clip.fl_image(addSnowEffectToImg, apply_to=['mask'])
newclip.write_videofile(resultFileName)上述代码中,videoFileName 是要处理的视频剪辑文件名,clip 是将该视频文件加载到内存准备剪辑,newclip 就是经过变换后的目标剪辑,resultFileName 是输出的结果视频文件。3.3、对同一个剪辑应用多种不同图像处理前面 2 个步骤介绍的是对一个视频进行统一方式的处理,如果需要针对同一个视频的不同时间段进行不同的视频特效处理,如片头加上文字标题、中间加上弹幕特效、结尾加上鸣谢文字等,则需要区分剪辑的时间位置调用多个图像处理函数,这有多种方法来实现,但是为了模式统一,建议使用 Moviepy 的 subclip 函数将不同时段剪辑单独截取成不同的子剪辑,然后分别对每个子剪辑设置对应的图像处理函数,再针对每个剪辑调用 fl_image 来处理,各个子剪辑处理完成后再拼接成一个剪辑输出即可。四、小结本文简单介绍了 Moviepy 库和 OpenCV-python 库,并讨论了 Moviepy 结合 OpenCV-python 进行视频剪辑的适用的一些场景,同时给出了这种剪辑处理模式的推荐实现方案,对使用 Python 进行视频剪辑处理感兴趣的同仁可以按照该模式去进行尝试。 一、图像平滑处理简介图像平滑处理的基本概念非常直观,它使用滤波器模板确定的邻域内像素的平均/加权平均灰度值代替图像中每个像素的值。平滑线处理滤波器也称均值滤波器,所有系数都相等(非加权平均)的空间均值滤波器也称为盒状滤波器。在《OpenCV-Python 图像平滑处理1:卷积函数filter2D详解及用于均值滤波的案例》介绍了使用filter2D实现图像平滑处理、在《OpenCV-Python 图像平滑处理2:blur函数及滤波案例》介绍了使用blur实现图像模糊处理,本文将介绍另外一个OpenCV-Python的函数boxFilter实现平滑处理。二、boxFilter介绍2.1、简介boxFilter也是OpenCV用于进行图像模糊处理的函数,该函数使用盒装滤波器进行均值滤波平滑处理。盒状滤波器的所有元素都相等,其元素为浮点数。boxFilter的核矩阵的元素取值有两种,与归一化参数normalize 有关。boxFilter对应核矩阵为:这里的α取值规则如下:对系数相等的盒状滤波来说,由于核矩阵的对称性,卷积和相关的处理结果相同。关于相关和卷积的关系请参考《《数字图像处理》空间滤波学习感悟2:空间相关与卷积的概念、区别及联系》的介绍。2.2、语法说明语法1 dst = cv.boxFilter( src, ddepth, ksize[, dst[, anchor[, normalize[, borderType]]]] )参数说明src:输入图像,可以是任何通道数的图像,处理时是各通道拆分后单独处理,但图像深度必须是CV_8U, CV_16U, CV_16S, CV_32F 或CV_64F;ddepth:输出图像深度(请参考《图像表示的相关概念:图像深度、像素深度、位深的区别和关系》),如果目标图像深度和输入图像深度相同,则传值-1,老猿测试在Python中此时取值None、0效果也一样,注意ddepth在这里必须传值,不能使用默认值。dst:结果图像,其大小和类型都与输入图像相同;ksize:卷积核(convolution kernel )矩阵大小,如上概述所述,实际上是相关核(correlation kernel),为一个单通道的浮点数矩阵,如果针对图像不同通道需要使用不同核,则需要将图像进行split拆分成单通道并使用对应核逐个进行处理anchor:核矩阵的锚点,用于定位核距中与当前处理像素点对齐的点,默认值(-1,-1),表示锚点位于内核中心,否则就是核矩阵锚点位置坐标,锚点位置对卷积处理的结果会有非常大的影响normalize:核矩阵是否归一化处理的标记,为True进行归一化处理,否则不进行归一化处理,默认值为TrueborderType:当要扩充输入图像矩阵边界时的像素取值方法,当核矩阵锚点与像素重合但核矩阵覆盖范围超出到图像外时,函数可以根据指定的边界模式进行插值运算。可选模式包括:注意:BORDER_WRAP在此不支持;默认值为BORDER_DEFAULT ,与BORDER_REFLECT_101 、BORDER_REFLECT101相同2.4、返回值返回值为结果图像矩阵,因此输入参数中的dst参数无需输入。从以上介绍可知,boxFilter函数与blur函数一样,就是在《OpenCV-Python 图像平滑处理1:卷积函数filter2D详解及用于均值滤波的案例》介绍的filter2D的一种用于均值滤波的特定应用,而blur函数又是boxFilter函数归一化处理的特例。三、使用案例下面的案例脱胎于OpenCV帮助文档,代码对输入图像进行均值滤波:import cv2
import numpy as np
from opencvPublic import cmpMatrix
def smoothingByBlur():
img = cv2.imread('f:\\pic\\opencvLogo.JPG')
ksize = (5,5)
dst = cv2.blur(img, ksize)
plt.subplot(121), plt.imshow(img), plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(dst), plt.title('Averaging')
plt.xticks([]), plt.yticks([])
plt.show()
return dst
def smoothingByBoxFilter():
img = cv2.imread('f:\\pic\\opencvLogo.JPG')
ksize = (5,5)
dst = cv2.boxFilter(img, ddepth=-1, ksize=ksize,normalize =True)
plt.subplot(121), plt.imshow(img), plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(dst), plt.title('Averaging')
plt.xticks([]), plt.yticks([])
plt.show()
return dst
d1 = smoothingByBlur()
d2 = smoothingByBoxFilter()
if(cmpMatrix(d1,d2)): #对比两个结果矩阵是否一致
print('d1==d2')
else: print('d1!=d2')结果输出:文字输出:d1==d2可以看到输出图像比输入图像变模糊了,且boxFilter归一化的处理的结果矩阵与filter2D、blur处理的结果完全一样。当不进行归一化处理时,图像处理效果如下:四、小结本文介绍了图像平滑处理及均值滤波等基础概念,并详细介绍了卷积函数boxFilter的Python语法及参数,并用之进行了对图像的均值滤波平滑处理,可以看到其归一化的模糊化处理结果与filter2D、blur函数完全一样,实际上它是filter2D一种特定场景的应用,而blur又是boxFilter函数归一化处理的特例 一、引言Moviepy 是一个 Python 的音视频剪辑库,OpenCV 是一个图形处理库,我们知道视频的一帧就是一幅图像,因此在处理视频时可以结合 OpenCV 进行帧处理,将二者结合可以用来进行一些不错的视频特效的处理。老猿在此介绍两个这方面的应用案例供大家参考。二、给视频添加雪花飘落特效2.1、实现原理雪花特效可以给视频增加特殊的效果,要给视频加雪花特效,是基于以下原理来实现的:每个视频都是由一个个视频帧构成,每个视频帧都是一副静态的图像,通过视频帧的连续显示形成动态视频;实现视频雪花飘落,就是在视频的每帧图像中添加雪花,并在前后相连的视频帧中变化雪花的位置,形成雪花下飘带横向移动的效果;雪花的图片本身是一个矩形,矩形内有黑色背景和白色的雪花,在将雪花图片添加到视频帧时,需要确保黑色部分不会遮盖帧图像的内容,只有白色的雪花前景色才可以遮挡帧图像内容。这就需要通过图像的阈值处理确得到雪花图像的二值图,用该二值图及其补图作为掩膜,二值图作为雪花原始图片与自身与运算的掩码来获取雪花图片的前景色,补图作为帧图片雪花对应位置的子图与子图自身与运算的掩膜来获取雪花黑色背景部分对应的帧图像作为背景色;对视频帧调用雪花融合图像的函数进行动态融合雪花的处理;Moviepy 的 fl_image 是视频剪辑基类 VideoClip 的方法,该方法用于对视频的帧图像进行变换,其参数包括一个对帧图像进行变换的函数image_func,具体变换由应用实现对图像进行变换处理的一个函数,然后将该函数作为 image_func 的值参传入fl_image,Moviepy 就会调用该函数完成对视频每帧图像的处理生成新的剪辑2.2、具体实现2.2.1、雪花图片本次实现的案例对应的雪花图片为:文件名为:f:\pic\snow.jpg。上述图片的雪花是标准的雪花图像,但图像比较大,在视频中直接展示这么大的雪花就很假,老猿经过测试,发现将其缩小到原图像的五分之一以内比较象真正的雪花。2.2.2、实现流程2.2.3、关键实现2.2.3.1、初始化雪花图片的雪花是固定大小的,而真正的雪花大小是不同的,为了模拟真正的雪花效果,需要有各种不同大小和角度的雪花,为此每片雪花需要根据图片雪花图像进行随机的大小和角度调整。一帧图像中的雪花数量至少是几十到几百片,如果每次融合图像时,都需要从图片雪花图像进行大小和旋转角度的变换,是非常消耗系统的性能的,影响视频的生成耗时。为了提升处理性能,老猿只在程序开始初始化时一次批量生产各种不同大小、不同旋转角度的各种雪花,后续程序生成雪花时,直接从批量生成的雪花中取一个作为要生成的雪花,而不用每次从基本的雪花图像开始进行变换。生成各种雪花形状的示例代码:# -*- coding: utf-8 -*-
import cv2,random
import numpy as np
from opencvPublic import addImgToLargeImg,readImgFile,rotationImg
snowShapesList = [] #雪花形状列表
snowObjects=[] #图片中要显示的所有雪花对象
def initSnowShapes():
"""
从文件中读入雪花图片,并进行不同尺度的缩小和不同角度的旋转从而生成不同的雪花形状,这些雪花形状保存到全局列表中snowShapesList
"""
global snowShapesList
imgSnow = readImgFile(r'f:\pic\snow.jpg')
imgSnow = cv2.resize(imgSnow, None, fx=0.3, fy=0.3) #图片文件中的雪花比较大,需要缩小才能象自然的雪花形象
minFactor,maxFactor = 50,100 #雪花大小在imgSnow的0.5-1倍之间变化
for factor in range(minFactor,maxFactor,5): #每次增加5%大小
f = factor*0.01
imgSnowSize = cv2.resize(imgSnow, None, fx=f, fy=f)
for ange in range(0,360,5):#雪花0-360之间旋转,每次旋转角度增加5°
imgRotate = rotationImg(imgSnowSize,ange)
snowShapesList.append(imgRotate)
2.2.3.2、产生一排雪花每帧图像除了保留上帧图像中未飘落出图像范围的雪花外,同时还会从顶部生成一排数量随机的雪花,形成生生不息的雪花。下面是从顶部初始化生成一排雪花的代码:def generateOneRowSnows(width,count):
"""
产生一排雪花对象,每个雪花随机从snowShapesList取一个、横坐标位置随机、纵坐标初始为0
:param width: 背景图像宽度
:param count: 希望的雪花数
:y:当前行对应的竖直坐标
:return:一个包含产生的多个雪花对象信息的列表,每个列表的元素代表一个雪花对象,雪花对象包含三个信息,在snowShapesList的索引号、初始x坐标、初始y坐标(才生成固定为0)
"""
global snowShapesList
line = []
picCount = len(snowShapesList)
for loop in range(count):
imgId = random.randint(0,picCount-1)
xPos = random.randint(0,width-1)
line.append((imgId,xPos,0))
return line2.2.3.3、将所有雪花对象融合到背景图像上帧图像中的雪花在当前帧中需要随机下落一定位置,并在一定幅度内横向漂移,当有雪花落到图像底部之下时,需要释放对应对象以节省资源。def addSnowEffectToImg(img):
"""
将所有snowObjects中的雪花对象融合放到图像img中,融合时y坐标随机下移一定高度,x坐标左右随机小范围内移动
"""
global snowShapesList,snowObjects
horizontalMaxDistance,verticalMaxDistance = 5,10 #水平方向左右漂移最大值和竖直方向下落最大值
rows,cols = img.shape[:2]
maxObjsPerRow = int(cols/100)
snowObjects += generateOneRowSnows(cols, random.randint(0, maxObjsPerRow))
snowObjectCount = len(snowObjects)
rows,cols = img.shape[0:2]
imgResult = np.array(img)
for index in range(snowObjectCount-1,-1,-1):
imgObj = snowObjects[index] #每个元素为(imgId,x,y)
if imgObj[2]>rows: #如果雪花的起始纵坐标已经超出背景图像的高度(即到达背景图像底部),则该雪花对象需进行失效处理
del(snowObjects[index])
else:
imgSnow = snowShapesList[imgObj[0]]
x,y = imgObj[1:] #取该雪花上次的位置
x = x+random.randint(-1*horizontalMaxDistance,horizontalMaxDistance) #横坐标随机左右移动一定范围
y = y+random.randint(1,verticalMaxDistance) #纵坐标随机下落一定范围
snowObjects[index] = (imgObj[0],x,y) #更新雪花对象信息
imgResult = addImgToLargeImg(imgSnow,imgResult,(x,y),180) #将所有雪花对象图像按照其位置融合到背景图像中
return imgResult #返回融合图像2.2.3.4、实现视频雪花飘落特效视频合成下面的代码调用 addSnowEffectToImg 实现对《粉丝记事本》视频的雪花飘落特效:from moviepy.editor import *
def addVideoSnowEffect(videoFileName,resultFileName):
clip = VideoFileClip(videoFileName)
newclip = clip.fl_image(addSnowEffectToImg, apply_to=['mask'])
newclip.write_videofile(resultFileName)
if __name__ == '__main__':
addVideoSnowEffect(r'f:\video\fansNote.mp4',r'f:\video\fansNote_snow.mp4')2.2.4、雪花飘落效果三、制作灯光秀短视频3.1、视频内容设计基于多张静态景物叠加特效声光特效可以将静态图片变成有趣的灯光秀视频,老猿为了武汉这个英雄之城解封一周年用 Python 制作了个解封庆祝的灯光秀短视频。老猿是个没有艺术细胞的人,因此这个视频内容只能说仅能代表是个视频而已,对最终的内容表现大家就不需要过多评价。在创作该视频前,老猿对视频进行了简单规划,将创作视频分为片头、视频内容和片尾三部分:片头:5 秒时间,展现一幅黄鹤楼的照片,并带上“武汉重启一周年灯光秀”的标题视频内容:全长 35 秒,每隔 2 秒随机展现一张武汉灯光秀景观图,并在视频中附上向上滚动的文字“热烈庆祝武汉重启一周年!”、“武汉万岁!中国万岁!”,并在视频的左下角和右下角用红绿蓝三色画三条向上晃动的线条表示彩色激光片尾:25 秒,每隔 2 秒随机展现武汉的一张风景照,并展现“制片:老猿 Python”等制作信息。3.2、开发设计3.2.1、视频图片处理视频中用到的图片都来源于互联网,为了减少图片加载的时间和统一初始化,在程序中通过全局变量将片头使用图像、视频内容使用图像、片尾使用图像分别使用了三个全局变量进行保存,其中后两者为列表类型。为了确保视频输出,所有图片都调整到了统一大小。3.2.2、灯光效果处理在视频内容部分,左下角和右下角发射的彩色激光,采用在背景图片中根据时间动态绘制彩色线条,实现彩色激光晃动照射的效果,为了限制晃动范围,设定了激光终点的 x 值的最小值和最大值。激光终点的位置根据时间动态计算,并在到达 x 值的最小值或最大值时自动回扫。3.2.3、帧图像的生成上面介绍的图像处理,全部集中在一个函数中处理,该函数仅带一个参数时间 t,判断时间来决定现在生成的内容是片头、内容还是片尾,然后据此来进行帧图像的生成。生成时,需要判断图像是否切换,因此需要记录上一次切换的时间和切换后的图像,确保未达到切换时间前用上次图像作为帧图像的背景,达到切换时间要求后切换新的图像作为后续帧图像生成的背景。为此在该函数中使用了两个全局变量来记录当前帧图像背景图片和上次切换时间。3.2.4、输出到视频为了将视频输出到文件,通过 VideoWriter_fourcc 指定视频输出文件类型,采用 OpenCV 的 VideoWriter 类来进行视频输出,通过 Open 方法指定输出文件,通过 write 方法将一帧帧视频写入。3.3、具体实现3.3.1、总流程加载片头、视频内容、片尾需要使用的图像;创建新视频文件按照帧率 24、时长 65 秒,构建 24*65 张帧图像,并逐一输出到视频文件。3.3.2、全局变量初始化全局变量主要是视频中需要使用的图片,分成片头使用图片 hhlImg、灯光秀图片列表 lightShowImgList 和片尾风景图片列表 whImgList 。另外 preImg,preTime 用于记录上次切换视频图片的图片和切换时间。hhlImg = cv2.resize(readImgFile(r'f:\pic\武汉\黄鹤楼.jpg'),(800,600))lightShowImgList = [cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_桥.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_武汉江边.jpg'),(800,600)), cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_一桥.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_一桥底部.jpg'),(800,600)), cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_一桥远景.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_远桥.jpg'),(800,600))]whImgList = [cv2.resize(readImgFile(r'f:\pic\武汉\东湖1.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\东湖2.jpg'),(800,600)), cv2.resize(readImgFile(r'f:\pic\武汉\东湖樱园樱花.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\武大牌楼.jpg'),(800,600)), cv2.resize(readImgFile(r'f:\pic\武汉\武大牌楼远观.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\武大樱花2.jpg'),(800,600)), cv2.resize(readImgFile(r'f:\pic\武汉\武大樱园顶高拍照.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\武大樱园门洞.jpg'),(800,600)), cv2.resize(readImgFile(r'f:\pic\武汉\武大樱园入口.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\武大老图书馆.jpg'),(800,600))]preImg,preTime = None,03.3.3、实现给背景图像添加彩色激光照射效果lightShowImg 函数实现给背景图像指定点发射彩色激光的特效,激光发射点固定,由参数 lightStartPos 指定,终点随参数 t 在一定范围内变化,终点 x 坐标受参数 minX, maxX 控制,同一个发射源的三激光之间的间距受参数 distance 控制。def lightShowImg(bg,minX, maxX,distance,lightStartPos,t):
"""
实现在背景图像上添加当前散发彩色激光的处理
:param bg: 背景图像
:param minX:灯光终点的最大x坐标
:param maxX:灯光终点的最小x坐标
:param distance: 不同灯光之间的间距
:param lightStartPos: 灯光发射点
:param t: 时间t
:return: 添加了发射灯光的图像
"""
x = (minX+int(t*200))%(maxX*2) #按时间t计算灯光终点的x坐标,该坐标可以超出背景图像范围
img = np.array(bg)
if x>maxX: #到达最大范围,需要回扫
x = 2*maxX-x
color1,color2,color3 = (255,0,0),(0,255,0),(0,0 ,255 ) #蓝、绿、红三色
cv2.line(img, lightStartPos, (x, 0), color1, 4)
cv2.line(img, lightStartPos, (x + distance, 0), color2, 4)
cv2.line(img, lightStartPos, (x - distance, 0), color3, 4)
return img3.3.4、帧图片生成makeframe 函数实现帧图片生成,带一个参数 t 表示当前帧对应的剪辑时间,在函数内根据剪辑时间来判断是生成片头内容、灯光秀内容还是片尾内容生成 t 时刻对应帧图像返回:对于片头,采用黄鹤楼照片作为背景,并在图片中央显示“武汉重启一周年灯光秀”;对于视频内容,则每 2 秒从灯光秀图片队列中随机取一张图片作为当前背景图像,并调用 lightShowImg 增加左下角和右下角的彩色激光效果,同时动态向上滚动显示“热烈庆祝武汉重启一周年”、“武汉万岁!中国万岁!”等标语;对于片尾,则每 2 秒从武汉风景图片队列中随机取一张图片作为当前背景图像,同时动态向上滚动显示制作信息。def makeframe(t):
#生成t时刻的视频帧图像
global preImg,preTime
if t<5:#5秒片头
img = imgAddText(hhlImg,'武汉重启一周年灯光秀',64,(255,0,0),vRefPos='C')
elif t<40:#5-40秒灯光秀
minX, maxX = 200, 1200 # 灯光横向扫射范围
lightStartPos1 = (0, 600) # 灯光1的发射点坐标设置为左下角
lightStartPos2 = (800, 600) # 灯光2的发射点坐标设置为右下角
if (t-preTime)>2 or preImg is None:
img = np.array(random.choice(lightShowImgList))
preImg,preTime = img,t
else:
img = preImg
img = lightShowImg(img, minX, maxX, 80,lightStartPos1, t)
img = lightShowImg(img, minX-100, maxX+100,48, lightStartPos2, t)
t = int((t-4)*40)%img.shape[0]
img = imgAddText(img,'热烈庆祝武汉重启一周年!',48,(0,0,255),vRefPos=-80-t,)
img = imgAddText(img, '武汉万岁!中国万岁!',48,(255,0,255),vRefPos=-t)
else:#片尾
if (t-preTime)>2 or preImg is None:
img = np.array(random.choice(whImgList))
preImg,preTime = img,t
else:
img = preImg
t = int((t - 39) * 20) % img.shape[0]
img = imgAddText(img,"制片:老猿Python",36,(255,255,255),vRefPos=-120-t)
img = imgAddText(img, "https://www.infoq.cn/u/laoyuanpython/publish",24,(255,255,255),vRefPos= -60-t)
img = imgAddText(img, "2021年4月8日", 24, (255,255,255), vRefPos=-t)
return img3.3.5、制作视频文件函数 buildVideoByCV 用于制作视频文件,按指定格式生成一个新的视频文件,然后循环调用 makeframe 生成一帧帧图像写入视频文件中。def buildVideoByCV():
videoMake = cv2.VideoWriter()
fourcc = cv2.VideoWriter_fourcc(*'MP4V') #https://blog.csdn.net/whudee/article/details/108689420
fps = 12
videoMake.open(r"F:\video\lightShowCV.MP4", fourcc, fps, (800,600))
for t in range(65*fps):
img = makeframe(t*1.0/fps)
videoMake.write(img)
print(f'\r视频制作进度:{(t*100.0)/(66*fps):4.2f}%',end='')
videoMake.release()上述函数构建后只需要调用 buildVideoByCV 函数即可完成视频制作。3.3.6、视频效果四、小结本文介绍了制作视频雪花飘落特效和灯光秀的原理、实现的思想以及流程,并利用 Python+OpenCV+Moviepy 提供了关键的实现代码,可以供大家理解图像融合、图像制作视频、Moviepy 视频变换的完整案例。 一、引言对比度受限的自适应直方图均衡在OpenCV中是通过类CLAHE来提供实现的,老猿没研究过C++中的应用,但OpenCV-Python中应用时与普通的Python类构建对象的机制有所不同,老猿做了相关测试,在此简单介绍一下。二、CLAHE类及方法介绍2.1、简介CLAHE类是OpenCV中进行对比度受限的自适应直方图均衡的基类,其类继承关系如下:其父类和子类老猿没有研究过,在此就不展开介绍。该类有2个重要属性:tilesGridSize:图像被分成称为“tiles”(瓷砖、地砖、小方地毯、片状材料、块状材料)的小块,在OpenCV中,tilesGridSize默认为8x8 ,即整个图像被划分为8纵8横共64块。然后对每一个块进行直方图均衡处理clipLimit:裁剪限制,此值与对比度受限相对应,对比度限制这个参数是用每块的直方图的每个bins的数和整图的平均灰度分布数的比值来限制的。 裁剪则是将每块图像直方图中超过ClipLimit的bins多出的灰度像素数去除超出部分,然后将所有bins超出的像素数累加后平均分配到所有bins。具体算法老猿将在研究清楚后单独介绍。2.2、成员方法简介2.2.1、apply方法apply方法用于对图像应用对比度受限自适应直方图均衡变换处理。调用语法: dst = cv.CLAHE.apply( src[, dst] )参数说明:src:输入图像,图像类型为CV_8UC1 or CV_16UC1,即8位或16位灰度图dst:输出图像,类型同输入图像补充说明:在进行该方法调用前,必须已经设置了对比度受限自适应直方图均衡算法的受限对比度ClipLimit以及图像分块的行数和列数tiles2.2.2、collectGarbage方法collectGarbage方法应该是进行内存垃圾回收的,没有参数和返回值,老猿认为由于Python的内存管理机制,该方法没有什么意义,同时测试没有发现执行该方法起何作用,因此很可能是个无用的方法。2.2.3、getClipLimit方法getClipLimit方法是用于获取当前CLAHE对象设置的ClipLimit值返回。调用语法: retval = cv.CLAHE.getClipLimit()2.2.4、getTilesGridSize方法getClipLimit方法是用于获取当前CLAHE对象设置的tilesGridSize返回。调用语法: retval = cv.CLAHE.getTilesGridSize()2.2.5、setClipLimit方法方法是用于设置当前CLAHE对象的ClipLimit值,无返回值。调用语法: None = cv.CLAHE.setClipLimit( clipLimit )2.2.6、setTilesGridSize方法方法是用于设置当前CLAHE对象的tilesGridSize值,无返回值。调用语法: None = cv.CLAHE.setTilesGridSize( tileGridSize )三、CLAHE对象的构建3.1、CLAHE构造方法研究从上面的方法介绍中,没有看到CLAHE的构造方法,在OpenCV文档中,确实没有这个CLAHE的构造方法,在Python的CLAHE.py模块中,有该类的构造方法的定义:def __init__(self, *args, **kwargs): # real signature unknown
pass没有任何参数的说明,在老猿找到的该类的C++类的构造方法如下:int CLAHE(kz_pixel_t* pImage, unsigned int uiXRes, nsigned int uiYRes, kz_pixel_t Min, kz_pixel_t Max, unsigned int uiNrX, unsigned int uiNrY, unsigned int uiNrBins, float fCliplimit);
老猿以C++构造方法为依据构建了Python中的CLAHE类,构造方法确实返回了CLAHE对象,但以此调用相关方法全部出现代码异常退出。基本上说明OpenCV-Python中CLAHE的构造方法不可用,至于C++中是否能使用,老猿没有去研究。3.2、createCLAHE函数由于OpenCV-Python中CLAHE的构造方法无法使用,同时在OpenCV中,提供了单独的全局函数createCLAHE,因此OpenCV-Python中CLAHE对象的构建必须通过createCLAHE函数。调用语法:retval = cv.createCLAHE( clipLimit=40,tileGridSize=(8,8))参数及返回值说明:clipLimit、tileGridSize请参考前面关于类的介绍,返回值为创建的类对象,该对象的clipLimit、tileGridSize由createCLAHE函数的参数指定。四、示例代码下面的代码使用OpenCV-Python对读入的图像进行对比度受限自适应直方图均衡处理:
import cv2
def testLocalHistEqu():
img = readImgFile(r'f:\pic\valley.png', True)
print('666')
clahe = cv2.createCLAHE(clipLimit=200, tileGridSize=(5, 5))
cl2 = clahe.getClipLimit()
clahe.setClipLimit(20)
cl1 = clahe.getClipLimit()
clahe.setTilesGridSize((8,8))
imgEquA = clahe.apply(img)
上面的代码中创建CLAHE对象后,对对象的属性进行了读写操作,其实这些代码完全没有必要。五、小结本文介绍了OpenCV-Python对比度受限自适应直方图均衡变换的CLAHE类及其方法,并通过代码介绍了相关方法的使用。在单图像的对比度受限自适应直方图均衡时,只需要使用createCLAHE创建CLAHE对象,然后调用该对象apply方法就可以完成对比度受限自适应直方图均衡处理,该类的其他方法用处不大,但如果是要进行多次对比度受限自适应直方图均衡处理,且需要设置不同的分块数和受限阈值,则可以通过提供的方法直接修改对象属性再进行均衡处理即可。 一、引言关于自适应直方图均衡CLAHE的clipLimit的介绍,网上介绍的资料不多,可能对很多大佬来说,这个知识点很简单,但对于没这方面基础知识的,则不好理解,老猿今天结合OpenCV CLAHE源代码中对于clipLimit的赋值处理来解读一下。二、CLAHE涉及clipLimit的关键源代码CLAHE涉及clipLimit的关键源代码摘要如下:CLAHE_Impl::CLAHE_Impl(double clipLimit, int tilesX, int tilesY) :
clipLimit_(clipLimit), tilesX_(tilesX), tilesY_(tilesY)
{
}
void CLAHE_Impl::apply(cv::InputArray _src, cv::OutputArray _dst)
{
...
int histSize = _src.type() == CV_8UC1 ? 256 : 65536;
...
if (_src.size().width % tilesX_ == 0 && _src.size().height % tilesY_ == 0)
{
tileSize = cv::Size(_src.size().width / tilesX_, _src.size().height / tilesY_);
_srcForLut = _src;
}
...
const int tileSizeTotal = tileSize.area();
...
int clipLimit = 0;
if (clipLimit_ > 0.0)
{
clipLimit = static_cast<int>(clipLimit_ * tileSizeTotal / histSize);
clipLimit = std::max(clipLimit, 1);
}
...
}
void CLAHE_Impl::setClipLimit(double clipLimit)
{
clipLimit_ = clipLimit;
}
double CLAHE_Impl::getClipLimit() const
{
return clipLimit_;
}
...三、代码解读以上代码就是OpenCV自适应直方图均衡CLAHE对应源代码中关于clipLimit赋值处理的相关代码,暂不涉及使用。可以看到,类设置方法中对clipLimit设置后,其值会保存在类私有变量clipLimit_ 中,最终进行apply自适应直方图均衡处理时,采用局部变量clipLimit = clipLimit_ * tileSizeTotal / histSize,并取clipLimit 和1中间的最大值。可以看到,我们创建CLAHE对象或调用setClipLimit传入的clipLimit参数,最终被转换为了该值乘以tileSizeTotal (分块像素数)除以histSize(每个分块的直方图组数),这个转换是干什么呢?是得到每个分组的平均像素数量,如果灰度比较平均的话,每种级别(对应直方图分组数)的灰度所对应的像素数应该相等,当用该平均值乘以clipLimit,得到的是超过平均值clipLimit倍的像素数,这个值就是裁剪的限制值,对于超过这个值的分组就得裁剪,具体怎么裁剪我们在下篇博文再介绍。四、小结OpenCV自适应直方图均衡CLAHE中的参数clipLimit,是CLAHE的裁剪限制值,其值表示是各分块图像的直方图分组平均像素的倍数,当一个分组中的像素数超过这个倍数值乘以直方图分组平均像素的值(即限制值),则表示该分组对应灰度的像素数需要裁剪。 一、引言最近一个月来都在研究OpenCV 中CLAHE算法的一些问题,如:图像横向和纵向分块大小与图像的宽和高不能整除怎么处理?CLIP的剪裁是怎么实施的?解决棋盘效应的具体插值处理过程怎样?彩色图像怎么处理?到处找资料,也看了部分博客所谓的源代码,结果还是没有找到答案,看来没有捷径,干脆直接下载了一份OpenCV的源代码来阅读。可惜自从没有亲手做C语言相关开发后,手上的机器连C++运行环境都没有,先直接读代码。本来想等所有问题都有答案时再写博文,不过这一阵子单位和家里事情都很多,没有多少时间,只能慢慢来,解决一个问题就发布一篇博文。二、OpenCV源代码的下载下载地址:https://sourceforge.net/projects/opencvlibrary/files/有3.4.10–4.5.4的版本,但下载很慢,老猿费了很大的劲,大家可以考虑专门的下载工具下载。如果实在下不下来,请关注老猿Python的微信公号给老猿发消息。三、自适应直方图均衡CLAHE图像和分块大小不能整除的处理当图像的宽(或高)不是对应横向(或纵向)分块数的整数倍时,老猿认为对于分块的处理有多种方式:将每个分块横向或纵向多加1个像素,最后一个分块的大小比前面分块小;将每个分块横向或纵向减去1个像素,最后一个分块的大小比前面分块大;将图像裁剪或补齐到可以整除的大小。通过阅读源代码,OpenCV中采用将图像补齐到可以整除的大小,即对于图像的宽(或高)不是对应横向(或纵向)分块数的整数倍时,将对应宽(或高)补齐到可以整除的最少像素素。具体处理的源代码如下: if (_src.size().width % tilesX_ == 0 && _src.size().height % tilesY_ == 0)
{
tileSize = cv::Size(_src.size().width / tilesX_, _src.size().height / tilesY_);
_srcForLut = _src;
}
else
{
{
cv::copyMakeBorder(_src, srcExt_, 0, tilesY_ - (_src.size().height % tilesY_), 0,
tilesX_ - (_src.size().width % tilesX_), cv::BORDER_REFLECT_101);
tileSize = cv::Size(srcExt_.size().width / tilesX_, srcExt_.size().height / tilesY_);
_srcForLut = srcExt_;
}
}小结OpenCV自适应直方图均衡CLAHE图像和分块大小不能整除时,采用的是将图像补齐到能整除大小,补齐是按边界镜像的方式(cv::BORDER_REFLECT_101)补齐。 金某某的算法生活
第一章:OpenCV-Python 图像平滑处理1
金某某的算法生活
第九章:OpenCV自适应直方图均衡CLAHE C++源代码分享
金某某的算法生活
第二章:OpenCV-Python 图像平滑处理2
金某某的算法生活
第四章:Moviepy+OpenCV-python 结合进行音视频剪辑处理的一种建议模式
金某某的算法生活
第三章:OpenCV-Python 图像平滑处理3
金某某的算法生活
第十一章:OpenCV-Python+Moviepy 结合进行视频特效处理
金某某的算法生活
第十三章:OpenCV-Python自适应直方图均衡类CLAHE及方法详解
金某某的算法生活
第六章:OpenCV自适应直方图均衡CLAHE的clipLimit的含义及理解
金某某的算法生活
第十章:OpenCV自适应直方图均衡CLAHE图像和分块大小不能整除的处理