【数字图像处理】数字图像处理中的直方图相关操作

news2024/11/26 9:31:48

文章目录

  • 前言
  • 一、直方图为什么可以进行图像处理?
  • 二、直方图处理怎么实现?
    • 直方图均衡化
    • 直方图匹配-规定化
    • 局部直方图处理
    • 直方图统计量增强图像
  • 三、OpenCv提供的直方图基础操作
    • 直方图均衡化
    • OpenCv中直方图的表示
    • 从数据创建直方图:cv::calcHist()
    • 显示直方图
    • 直方图归一化
    • 直方图二值化
    • 找出最显著的区间
    • 比较两个直方图
  • 四、OpenCv提供的一些复杂的直方图操作
    • EMD距离
    • 反向投影
    • 模板匹配
  • 五、 MATLAB中的直方图操作
    • 计算并显示图像直方图
    • 直方图归一化
    • 直方图均衡化
    • 正比例变换


前言

直方图操作是图像处理中的一个基本工具,直方图计算简单,适合于快速硬件的实现,基于直方图的技术成为实时图像处理的一个流行工具。

在了解直方图进行图像处理前先明确几个概念:

  • 直方图容器:细分的灰度级称为直方图容器,个人理解:容器顾名思义是容纳一类事物的东西,直方图容器就是容纳灰度级的;
  • 直方图或图像直方图:通常把处理的归一化直方图简单称为直方图或图像直方图

一、直方图为什么可以进行图像处理?

直方图的形状与图像的外观有关。

如下图显示了具有4个基本灰度特性的几幅图像:暗图像(左上)、亮图像(右上)、低对比度图像(左下)和高对比度图像(右下)。

在这里插入图片描述

下图分别是这几幅图像的归一化直方图:

在这里插入图片描述

从归一化直方图中我们可以得出这几个结论:

  • 暗图像的直方图中,大多数直方图容器集中在灰度级的低端/暗部;
  • 亮图像的直方图中,大多数直方图容器集中在灰度级的高端/亮部;
  • 低对比度图像的直方图中,直方图容器基本位于灰度级的中间;对于单色图像来说,这表示图像具有灰色的外观;
  • 高对比度图像的直方图中,直方图容器覆盖了较宽范围的灰度级,并且像素的分布也基本上是均匀的,直方图容器的高度基本相同。

可以简单的得出结论:像素占据整个灰度级范围并且均匀分布的图像,将具有高对比度的外观和多种灰色调。最终呈现的图像效果将是显示了大量灰度细节并具有高动态范围。

另外,直方图可以表达更多的信息,比如,物体的颜色分布、物体的边缘梯度模板或是以概率分布的形式表达当前对物体位置的估计。

直方图可以说是计算机视觉领域中的经典工具之一。

例如,通过判断帧与帧之间边缘和颜色的统计量是否出现巨大变化,来检测视频中场景的变换。

通过使用兴趣点邻域内的特征组成的直方图,来辨识兴趣点。若将边缘、颜色、角点等等的直方图作为特征,可以使用分类器来进行目标识别。

提取视频中的颜色或边缘直方图序列,可以用来判断视频是否拷贝自网络。

直方图是一种用来揭示数据分布的统计特性的工具。


二、直方图处理怎么实现?

基础的直方图处理有:直方图均衡化、直方图规定化、局部直方图处理、直方图统计量增强图像等。

直方图均衡化

具体理论基础参考《数字图像处理(第四版)》冈萨雷兹版3.3.1节P87。

直方图均衡化的计算公式:
s k = T ( r k ) = ( L − 1 ) ∑ j = 0 k p r ( r j ) = L − 1 M N ∑ j = 0 k n j s_k = T(r_k) = (L-1) \sum_{j=0}^{k} p_r(r_j) = \frac {L-1} {MN} \sum_{j=0}^{k} n_j sk=T(rk)=(L1)j=0kpr(rj)=MNL1j=0knj
其中 r k r_k rk是输入图像的灰度值,通常 r k = k r_k=k rk=k k = 0 , 1 , 2 , . . . L − 1 k=0,1,2,...L-1 k=0,1,2,...L1 s k s_k sk r k r_k rk对应的输出图像灰度值; L L L是图像的灰度级,比如 8 b i t 8bit 8bit图像灰度级为 255 255 255 M N MN MN是图像像素总数; n j n_j nj表示灰度值为 r k r_k rk的像素数量。

直方图均衡化视会常事产生一幅具有均匀直方图的输出图像,希望自动增强图像时,直方图均衡化是很好的选择。


直方图匹配-规定化

具体理论基础参考《数字图像处理(第四版)》冈萨雷兹版3.3.2节P92。

用于生成具有规定直方图的图像的方法,称为直方图匹配或直方图规定化。

直方图规定化需要有一幅图像,这幅图像的直方图即为规定的直方图,将输入图像经过某种转换,得到输出图像,使得输出图像的直方图与规定的直方图一致或者大致相同。

离散直方图规定化的过程总结如下:

  1. 计算输入图像的直方图,并将利用式将(1)输入图像中的灰度映射到直方图均衡化后的图像中的灰度,将得到的值 s k s_k sk四舍五入到整数区间 [ 0 , L − 1 ] [0, L-1] [0,L1]
  2. 由规定的直方图值 p z p_z pz,用式 G ( z q ) = ( L − 1 ) ∑ i = 0 q p z ( z i ) G(z_q) = (L-1) \sum_{i=0}^{q} p_z(z_i) G(zq)=(L1)i=0qpz(zi) 计算 G ( z q ) G(z_q) G(zq)的所有值,将得到的值四舍五入到区间 [ 0 , L − 1 ] [0, L-1] [0,L1]内的整数,将四舍五入后的 G ( z q ) G(z_q) G(zq)值存储到一个查找表中;
  3. 找到 s k s_k sk G ( z q ) G(z_q) G(zq)最接近时对应的 z q z_q zq值,存储从 s s s z z z的映射;当多个 z q z_q zq值有相同的匹配时,按照约定选择最小值;
  4. 根据步骤3中找到的映射,将每个均衡化后的像素 s k s_k sk值映射到直方图规定化图像中值为 z q z_q zq的对应像素,形成直方图规定化后的图像。

如下以一个简单的示例展示了直方图均衡化和直方图规定化的原理。

在这里插入图片描述

上图的表格中展示了直方图均衡化以及直方图规定化过程中的计算数据;表格上下文字是对过程的描述;右上图表中展示了规定直方图与规定化实际计算的直方图,从这里可以看出实际计算的结果与规定直方图并不完全匹配,但是实现了将灰度向灰度级的高端移动的一般趋势。

具体文件链接:
链接1;
链接2 提取码:ci06;


局部直方图处理

当需要增强图像中几个小区域的细节时,通常采用基于像素邻域的灰度分布的变换函数进行局部增强。

局部直方图处理的过程大致是:定义一个邻域,并将其中心在水平方向或垂直方向上从一个像素移动到另一个像素;在每个位置,计算邻域中的各点的直方图,得到直方图均衡化或直方图规定化变换函数;这个函数用于映射邻域中心像素的灰度;然后将邻域的中心移到一个相邻像素位置,并重复这个过程。


直方图统计量增强图像

直方图统计量增强图像的主要思路是计算图像的均值和方差,根据邻域像素的均值和方差判断中心像素是否需要增强,如果需要,则根据给定的灰度变换函数增强该像素。

均值是平均灰度的测度,方差是图像对比度的测度。

使用局部均值和方差处理图像的一个重要优点是,根据这些与图像外观紧密相关的统计测度可以开发处简单且强大的增强规则;

如下是根据直方图统计量增强图像的一种方法:

  1. 确定某个区域在点 ( x , y ) (x,y) (x,y)处是相对较亮还是较暗的方法:将局部灰度均值 m s x y m_{s_{xy}} msxy与全局灰度均值 m G m_G mG进行比较,如果 k 0 m G ≤ m s x y ≤ k 1 m G k_0 m_G \le m_{s_{xy}} \le k_1 m_G k0mGmsxyk1mG,则将 ( x , y ) (x,y) (x,y)处的像素作为待处理的候选像素;其中 k 0 k_0 k0 k 1 k_1 k1是非负常数,且 k 0 ≤ k 1 k_0 \le k_1 k0k1。比如,如果关注区域是比平均灰度的 1 / 4 1/4 1/4更暗的区域,那么选择 k 0 = 0 k_0 = 0 k0=0 k 1 = 0.25 k_1 = 0.25 k1=0.25

  2. 增强低对比度的区域,确定某个区域的对比度是否使得它是一个增强的候选区域的方法:将局部灰度方差 σ s x y \sigma_{s_{xy}} σsxy与全局灰度方差 σ G \sigma _G σG比较,若 k 2 σ G ≤ σ s x y ≤ k 3 σ G k_2 \sigma _G \le \sigma_{s_{xy}} \le k_3 \sigma _ G k2σGσsxyk3σG,则考虑将 ( x , y ) (x,y) (x,y)处的像素作为候选像素;其中 k 2 k_2 k2 k 2 k_2 k2是非负常数,且 k 2 < k 3 k_2 < k_3 k2<k3。比如,要增强对比度较低的一个暗色区域,可以选择 k 2 = 0 k_2 = 0 k2=0 k 3 = 0.1 k_3=0.1 k3=0.1

  3. 满足上面所有局部增强的候选像素按照如下方式处理:乘以一个规定常数 C C C,增大或减小其相对于图像剩下部分的灰度值,不满足增强条件的像素则保持不变;

上面方法总结为公式如下:
g ( x , y ) = { C f ( x , y ) , k 0 m G ≤ m s x y ≤ k 1 m G A N D k 2 σ G ≤ σ s x y ≤ k 3 σ G f ( x , y ) 其他 g(x,y) = \left\{\begin{matrix} Cf(x,y), & k_0 m_G \le m_{s_{xy}} \le k_1 m_G AND k_2 \sigma _G \le \sigma _{s_{xy}} \le k_3 \sigma _G \\ f(x,y) & 其他 \end{matrix}\right. g(x,y)={Cf(x,y),f(x,y)k0mGmsxyk1mGANDk2σGσsxyk3σG其他
其中, x = 0 , 1 , 2 , . . . , M − 1 x=0,1,2,...,M-1 x=0,1,2,...,M1 y = 0 , 1 , 2 , . . . N − 1 y=0,1,2,...N-1 y=0,1,2,...N1 C 、 k 0 、 k 1 、 k 2 和 k 3 C、k_0、k_1、k_2和k_3 Ck0k1k2k3是规定的常数 , , m G m_G mG是输入图像的全局均值, σ G \sigma _G σG是输入图像的标准差,参数 m s x y m_{s_{xy}} msxy σ s x y \sigma _{s_{xy}} σsxy分别是局部均值和局部标准差,在每个 ( x , y ) (x,y) (x,y)位置都会变化, M M M N N N是输入图像中的行数和列数。

从上面的叙述中可以知道, k 0 、 k 1 、 k 2 、 k 3 、 C k_0、k_1、k_2、k_3、C k0k1k2k3C等参数的选择很重要,


三、OpenCv提供的直方图基础操作

直方图均衡化

扩大图像的动态范围以增强对比度最常用的技术是直方图均衡化。

解决扩展分布值的问题的一个较好的方法是重映射函数是累积分布函数,这也是直方图均衡化的原理。

OpenCv提供函数cv::equalizeHist()用于直方图均衡处理,要注意,该函数只能处理 8 b i t 8bit 8bit一维图像,并且输出也是 8 b i t 8bit 8bit一维图像;如果要处理的是彩色图像,需要分通道逐一处理。

函数声明:

void cv::equalizaHist(
    const cv::InputArray src,
    cv::OutputArray dst
);

OpenCv中的直方图均衡化操作实际并没有用到直方图,仅仅将原始图像与结果图像在直方图上直观展示时用到了直方图操作。


OpenCv提供一种数据类型来表达直方图,这个数据类型可以表达一维至多维的直方图,并包括必要的数据以支持均匀或非均匀的组宽,并配备各种有用的函数方便对直方图进行各种操作。

OpenCv中直方图的表示

在C++ API中,OpenCv使用数组表示直方图,可以使用cv::Matvector<>或稀疏矩阵cv::SparseMat表示。

当把一个数组视为直方图时,这个数据和普通数据的含义完全不同。

如果一个n维数组被视为直方图,那么数组中的每个元素表示该n维直方图对应组的计数结果,直方图中组的编号即数组某维与该组对应的元素的下标,下标通常是整数。

直方图中组的实际含义不同于该组的编号,当使用直方图时,可能需要在组的实际含义和组的编号之间转换。比如,有一个表达人群体重的直方图,其中的组可能被设计为 20 − 40 20-40 2040 40 − 60 40 - 60 4060 60 − 80 60 - 80 6080 80 − 100 80 - 100 80100,单位是 k g kg kg,表示为数组时这些组的编号是 0 0 0 1 1 1 2 2 2 3 3 3

OpenCv也提供了一些函数完成转换工作。

处理一个高维直方图时,经常会出现直方图中大部分元素都是0的情况,这时可以选择使用cv::SparseMat类表示直方图。


从数据创建直方图:cv::calcHist()

OpenCv提供函数cv::calcHist()可以从一个或多个数组中创建直方图。

直方图的维度和输入数组的维度或大小无关,而是取决于输入数组的数量。直方图中的每个维度对应于某个输入数组上的某个通道。

如果不想使用图片的所有通道,选择想使用的通道传入cv::calcHist()函数即可。

cv::calcHist()函数声明:

void calcHist(const Mat* images, int nimages, const int* channels, 
              InputArray mask, OutputArray hist, int dims, 
              const int* histSize, const float** ranges, 
              bool uniform=true, bool accumulate=false);

void calcHist(const Mat* images, int nimages, const int* channels, 
              InputArray mask, SparseMat& hist, int dims, 
              const int* histSize, const float** ranges, 
              bool uniform=true, bool accumulate=false);

void calcHist(InputArrayOfArrays images, const vector<int>& channels,
              InputArray mask, OutputArray hist, 
              const vector<int>& histSize, const vector<float>& ranges,
              bool accumulate=false);

前两种方式是C风格数组,第三种使用 STL vector模板类型的参数;前两种的主要区别是计算结果被保存在稠密数组还是稀疏数组中。

以一个示例说明这些参数。假设有一幅8bit三通道图像images,要求计算该图像的后两个通道的直方图,每个通道的直方图分为32个区间,每个区间的范围是均匀分布的。 示例代码如下:

	int nimages = 1;  // 数组个数
    int channels[] = { 1,2};  // 计算数组images中的1、2通道的直方图,通道从0开始
    int dims = 2;  // 输出的直方图是2维
    const int histSize[] = {32, 32};  // 每一通道的区间个数
    float range[] = {0, 256};  // uniform=true时每一维的区间的上限与下限,
                               // 比如对于1维区间范围是(256-0)/32=8,
                               // 其中第一个区间范围[0, 8),第二个区间范围[8, 16)...直到第20个区间范围[248, 256)
    const float *ranges[] = {range, range};  // 计算2个维度的直方图,所以有4个范围,每个范围都是[0,256)
    bool uniform = true;  // true表示使用规范化的情况
    bool accumulate = false;  // false表示计算结果被累加前,将设置hist为0

函数参数详细说明:

第一个参数是数组数据,images是一个指向C风格数组列表的指针,或者是一个更现代化的InputArrayOfArrays。直方图从images或InputArrayOfArrays中一个或更多数组中计算。images数组的要求:通道数可以不同,大小和数组的数据类型必须相同,数组元素可以是8位整数或32位浮点数。

如果以C风格作为输入,必须同时指定nimages作为images中包含的数组个数。

参数channels控制直方图计算时,哪些通道需要被考虑。channel中的整数表示在计算直方图时,使用输入数据images中的哪些通道。通道按顺序进行序列编号,比如images包含2个大小一致、数据类型相同的三通道数组,那么images[0]的3个通道被编号为 0 、 1 、 2 0、1、2 012,images[1]的3个通道被编号为 3 、 4 、 5 3、4、5 345,如果要计算images[0]的前2个通道和images[1]的后2个通道,那么channels={0,1,4,5};。channels中的元素个数是计算得到的直方图的维度。

数组mask是一个可选掩膜,可以用来选择images的每个数组中哪些像素参与直方图的计算。mask必须是一个8-bit数组并且和images中的输入数组大小相同。计算直方图时会考虑每个在mask中有非零值的像素,如果不想使用掩膜,传入cv::noArray()

参数hist是直方图计算的输出值。

参数dims是输出直方图的维度,该值是channels中元素的个数,每个维度的来源由channels指定。

参数histSize表示直方图中每个维度需要分为多少个区间,histSize中元素的个数也应该等于dims,与channels中元素的个数保持一致。

参数ranges指定每个区间对应的值。如果以C风格传参,ranges中的每个元素是一个数组,元素个数等于dims,与参数channel和histSize中元素个数保持一致,这时ranges[i]表示第i维中区间的结构。

参数uniform决定了ranges参数的具体内容。

对于C风格数组

  • 如果uniform=true,表示规范化的情况,即第i维中的所有区间都等长,此时只需要指定[下限,上限),此时得到的是均匀直方图;
  • 如果uniform=false,表示非规范化的情况,即第i维中的区间值需从ranges[i]中获取, 此时得到的是非均匀直方图。

对于vector风格的数组,含义与使用C风格时相同,只是将C风格中的内容摊平成为一维数组

  • 规范化情况下,按照维度的顺序每个维度对应两个元素;
  • 非规范化情况下按照维度的顺序每个维度对应 N + 1 N+1 N+1个元素。

简单说,在均匀直方图中,只需提供每个区间的最小边界和最大边界,在非均匀直方图中,需要提供每一维度中的每个区间的最小边界和最大边界。

参数accumulate表示在从images得到的数据被累加进入hist之前,不要将其中的元素删除,重新分配或是设置为0。


显示直方图

calcHist()计算得到的直方图hist不能直接显示出来,需要经过处理绘制直方图。

// 绘制二维直方图
void Draw2DimsHistogram(const cv::InputArray &calchist, const int *histSize, int scale, const char* histname = "histogram")
{
    cv::Mat hist_img(histSize[0]*scale, histSize[1]*scale, CV_8UC3);
    cv::Mat hist = calchist.getMat();
    for(int h=0; h<histSize[0]; ++h){
        for(int w=0; w<histSize[1]; ++w){
            float hVal = hist.at<float>(h,w);
            cv::rectangle(hist_img, cv::Rect(h*scale,w*scale, scale, scale), cv::Scalar::all(hVal), -1);
        }
    }

    cv::imshow(histname, hist_img);
}
// 绘制一维直方图
void Draw1DimHistogram(const cv::InputArray &calchist, const int histSize, int scale, const char *histname = "historgam")
{
    // 获取最大值
    double maxVal;
    cv::minMaxIdx(calchist, NULL, &maxVal);

    cv::Mat hist_img(maxVal + 20, histSize*scale, CV_8UC3);
    cv::Mat hist = calchist.getMat().t();

    for(int w=0; w<histSize; ++w){
        float hVal = hist.at<float>(0,w);
        cv::rectangle(hist_img, cv::Rect(w*scale, hist_img.rows-hVal, scale, int(hVal)), cv::Scalar(0,0,hVal), -1);
    }

    cv::imshow(histname, hist_img);
}
// 绘制一维直方图的折线图
void Draw1DimLineChartm(const cv::InputArray &calchist, const int histSize, int scale, const char *histname = "Line chart")
{
    // 获取最大值
    double maxVal;
    cv::minMaxIdx(calchist, NULL, &maxVal);

    cv::Mat hist_img(maxVal + 20, histSize*scale, CV_8UC3);

    cv::Mat hist = calchist.getMat().t();

    for(int w=1; w<histSize; ++w){
        float preVal = hist.at<float>(0, w-1);
        float hVal = hist.at<float>(0,w);
        cv::line(hist_img, cv::Point((w-1)*scale, hist_img.rows - preVal), cv::Point(w*scale, hist_img.rows - hVal), cv::Scalar(0,0,255), 2, 8);
    }

    cv::imshow(histname, hist_img);
}

直方图归一化

构造直方图时,首先需要将信息放入在各个区间。有时希望将直方图变为归一化的形式,这时的每个区间表示的是该区间内的信息占总体的百分比。比如说,对一幅8bit灰度图来说,设定区间为灰度值 [ 0 , 255 ] [0, 255] [0,255], 那么归一化前每个区间表示图像中当前灰度级的像素数量,归一化后表示当前灰度级的像素数量占总像素数量的百分比。

C++ API中,可以简单使用数组的代数算子和操作完成直方图的归一化,如下:

cv::Mat normalized = my_hist / sum(my_hist)[0];
// 或者
cv::normalize(my_hist, my_hist, 1, 0, NORM_L1);

直方图二值化

如果希望将直方图二值化并丢弃所有其中元素个数少于给定值的区间,可以使用标准数组二值化函数:

cv::threshold(my_hist, my_threshold_hist, threshold, 0, cv::THRESHOLD_TOZERO);

找出最显著的区间

如果希望找出所有元素个数高于某个给定阈值的区间,或者希望找出有最多元素的区间,比如在使用直方图表示概率分布的情况时,可以选择使用cv::minMaxLoc()

函数声明如下:

void minMaxLoc( cv::InputArray src, 
				double *minVal, double *maxVal, 
				cv::Point* minLoc=0, cv::Point* maxLoc=0,
				cv::InputArray mask = cv::noArray);

void minMaxLoc(const cv::SparseMat& src, 
				double* minVal, double* maxVal, 
				int* minLoc=0, int* maxLoc=0);

该函数需要传入minValmaxVal用来存放计算出来的最小值和最大值,需要传入minLocmaxLoc存放计算出的最小值和最大值的位置。如果不想要某个计算结果,将该参数置为NULL即可。

当处理二维数组时,可以使用cv::InputArray的形式。

当处理二维直方图时,可以使用cv::SparseMat的形式。如果处理的是一个一维vector<>数组,可以使用cv::Mat(vec).reshape(1)将其转换为一个 N × 1 N \times 1 N×1的二维数组。

需要注意使用稀疏矩阵作为输入时,要求传入的minLocmaxLoc为int*型,这是因为稀疏矩阵形式的minMaxLoc()支持任意维的矩阵,因此需要手动分配存放位置的变量并确保其可存放n维稀疏直方图对应的原图中的一个点。


比较两个直方图

可以通过对两幅图像取直方图,并通过直方图比较方法来比较原来的两幅图像,也可以借助直方图比较方法在图像中识别物体,方法是将物体的直方图和图片中不同的子区域的直方图进行比较,通过直方图相配的程度来判断该子区域是否有物体。

OpenCv提供函数cv::compareHist()通过特殊的判据对两个直方图的相似度进行度量。

cv::compareHist()函数声明:

double cv::compareHist(cv::InputArray H1, cv::InputArray H2, int method);
double cv::compareHist(const cv::SparseMat& H1, const cv::SparseMat& H2, int method);

前两个参数是待比较的直方图,该直方图应该具有相同的大小。

第三个参数是需要选择的距离度量方法。OpenCv提供四个可用的方法:

  • 相关性方法cv::COMP_CORRL,使用基于统计相关性的方法,实现了皮尔逊相关性系数方法,计算公式:$d_{correl}(H_1, H_2) = \frac{ {\textstyle \sum_{i}^{} H_1’(i) \cdot H_2’(i) } } {\sqrt{ {\textstyle \sum_{i}^{} H_1’^2(i) \cdot H_2’^2(i) } } } ,这里的 , 这里的 ,这里的H_1 和 和 H_2$解释为概率分布。大的相关性系数比小的相关性系数匹配的好。完全匹配的相关性系数为 1 1 1, 完全不匹配的相关性系数为 − 1 -1 1 0 0 0表示两个分布没有关系,随机组合;
  • 卡方方法cv::COMP_CHISQR_ALT,该距离度量是基于卡方检验统计量,是一种检验两个分布是否相关的替换检验方法,计算公式:$d_{chi-square} (H_1, H_2) = \sum_{i}^{} \frac{ (H_1(i) - H_2(i))^2 } { H_1(i) + H_2(i) } $,对于该检验方法,低分值表示直方图匹配的很好,完全匹配的分值为 0 0 0,完全不匹配的分值无下限,依赖于直方图的大小;
  • 交集法cv::COMP_INTERSECT,简单基于两个直方图的交集,该方法对两个直方图中的共同部分求和,计算公式: d i n t e r s e c t i o n ( H 1 , H 2 ) = ∑ i m i n ( H 1 ( i ) , H 2 ( i ) ) d_{intersection}(H_1, H_2) = \sum_{i}^{} min(H_1 (i), H_2(i)) dintersection(H1,H2)=imin(H1(i),H2(i)),对于该检验方法,高分值表示匹配的好,如果两个直方图都进行了归一化, 1 1 1表示完全匹配, 0 0 0表示完全不匹配。
  • 巴氏距离cv::COMP_BHATTACHARYYA,对两个分布重叠程度的一种度量方法,计算公式:$d(H_1, H_2) = \sqrt{1- \frac{ {\textstyle \sum_{i}^{} H_1(i) \cdot H_2(i)} }{\sqrt{ {\textstyle \sum_{i}^{} H_1(i) \cdot {\textstyle \sum_{i}^{} H_2(i)} } } } } $,对于该检验方法,值越小表示匹配的效果越好, 0 0 0表示完全匹配, 1 1 1表示完全不匹配。

交集法在快速而粗略的匹配中效果很好,卡方法和巴氏距离法效果更好,但是速度更慢。另外还有EMD距离方法可以比较直方图,该方法最符合直觉的匹配效果,但是计算速度更慢。


四、OpenCv提供的一些复杂的直方图操作

OpenCv提供的一些可用的复杂的方法,在某些特定应用中比较有用。


EMD距离

EMD距离的基本思路是:通过将一部分(或全部)直方图搬到一个新位置,度量花多大的功夫才能把一个直方图搬到另一个直方图里

光照的变化会使得颜色值产生很大的偏移,这种偏移不改变颜色直方图的形状,上面介绍的相关性法、卡方方法、交集法、巴氏距离法都是对直方图颜色位置的度量,而对直方图颜色位置的移动度量不准确。EMD距离是一种对平移不敏感的距离度量方法。EMD距离可以在任意维度下工作。

EMD算法允许用户设置自己的距离度量或是自己的移动花费矩阵,即记下材料如何从一个直方图流向另一个直方图的,并用从数据的先验信息导出的非线性距离对其度量。OpenCv提供的EMD函数为cv::EMD(),函数声明如下:

float cv::EMD(cv::InputArray signature1, cv::InputArray signature2, 
              int distType, cv::InputArray cost = cv::noArray(), 
              float* lowerBound = 0, 
              cv::OutputArray flow = cv::noArray());

这个函数要求使用签名的方式传入前两个数组参数,签名要求是float类型的数组,每行包括直方图区间的计数值,接下来是该区间的坐标。比如对于下列的一维直方图,其签名分别为:上- [ [ 1 , 0 ] , [ 0 , 1 ] ] [[1, 0], [0, 1]] [[1,0],[0,1]];中- [ [ 0.5 , 0 ] , [ 0.5 , 1 ] ] [[0.5, 0], [0.5, 1]] [[0.5,0],[0.5,1]];下- [ [ 0 , 0 ] , [ 1 , 1 ] ] [[0, 0], [1, 1]] [[0,0],[1,1]]。如果在一个三维直方图 ( x , y , z ) (x, y, z) (x,y,z)位置上有区间计数 537 537 537,那么该区间这个位置的签名为 [ 537 , x , y , z ] [537, x, y, z] [537,x,y,z]。在调用cv::EMD()函数之前需要将直方图转换为签名的形式。

在这里插入图片描述

参数distType应该选择以下之一:

  • 曼哈顿距离cv::DIST_L1
  • 欧几里得距离cv::DIST_L2
  • 棋盘距离cv::DIST_C
  • 用户自定义距离cv::DIST_USER,使用该选择时需要通过参数cost传进一个预先计算好的矩阵,这个矩阵大小是一个 n 1 × n 2 n_1 \times n_2 n1×n2,其中 n 1 n_1 n1 n 2 n_2 n2signature1signature2的大小。

参数lowerBound有两个功能,一个作为输入,一个作为输出。作为输出时,表示两个直方图重心距离的下限,要计算这个下限,必须提供一个标准的距离度量方法,同时两个签名的总权重必须相同,比如直方图归一化后的情况。如果选择提供一个下限,必须将该值初始化为一个有意义的值,低于这个下限的,EMD距离才会被计算。如果希望任何情况下都计算EMD距离,将lowerBound置为0即可。

参数flow是一个可选的 n 1 × n 2 n_1 \times n_2 n1×n2的矩阵,用来记录从signature1的第 i i i个点流向signature2的第 j j j个点的质量,本质上给出的是计算整个EMD中质量的具体安排情况。

示例:从直方图为EMD算法构造签名,在不同的光照条件下对手部的直方图进行比较。代码运行工具:Qt。

void on_action_EMD_triggered()
{
    QStringList files;
    // 0.jpg:model图片(该图片会被拆分为上下两部分) 1.jpg:和第一张图片一样 2.jpg/3.jpg/4.jpg:与第一张图片进行比较的三张图片
    files << "0.jpg" << "1.jpg" << "2.jpg" << "3.jpg" << "4.jpg";

    std::vector<cv::Mat> src(5);
    cv::Mat tmp;
    // 读取model图片
    tmp = cv::imread(files.at(0).toStdString().data());
    if(tmp.empty()){
        qCritical() << "Error on reading image 1," << files.at(0) << "\n\n";
        return;
    }
    // 将第一张图片拆分一半
    cv::Size size = tmp.size();
    int width = size.width;
    int height = size.height;
    int halfheight = height >> 1;

    qDebug() << "Getting size [[" << tmp.cols << "] [" << tmp.rows << "]]\n\n";
    qDebug() << "Got size (w, h): (" << width << ", " << height << ")\n";

    src[0] = cv::Mat(cv::Size(width, halfheight), CV_8UC3);
    src[1] = cv::Mat(cv::Size(width, halfheight), CV_8UC3);

    // 将第一张图像分为上下两部分,分别赋给src[0]和src[1]
    cv::Mat_<cv::Vec3b>::iterator tmpit = tmp.begin<cv::Vec3b>();
    // 上半部分
    cv::Mat_<cv::Vec3b>::iterator s0it = src[0].begin<cv::Vec3b>();
    for(int i=0; i<width*halfheight; ++i, ++tmpit, ++s0it)
        *s0it = *tmpit;
    // 下半部分
    cv::Mat_<cv::Vec3b>::iterator s1it = src[1].begin<cv::Vec3b>();
    for(int i=0; i<width*halfheight; ++i, ++tmpit, ++s1it)
        *s1it = *tmpit;

    // 加载另外三张图像
    for(int i=2; i<5;++i){
        src[i] = cv::imread(files.at(i).toStdString().data());
        if(src[i].empty()){
            qCritical() << "Error on reading image " << i << ": " << files.at(i) << "\n";
            return;
        }
    }

    // 计算HSV图像,分解为单独的平面
    std::vector<cv::Mat> hsv(5), hist(5), hist_img(5);
    int h_bins = 8;
    int s_bins = 8;
    int hist_size[] = {h_bins, s_bins}, ch[] = {0,1};
    float h_range[] = {0, 180};
    float s_range[] = {0, 360};
    const float *range[] = {h_range, s_range};
    int scale = 30;

    // 计算并绘制直方图
    for(int i=0; i<5; ++i){
        // 转换为HSV图像
        cv::cvtColor(src[i], hsv[i], cv::COLOR_BGR2HSV);
        //  计算直方图
        cv::calcHist(&src[i], 1, ch, cv::noArray(), hist[i], 2, hist_size, range);
        // 归一化
        cv::normalize(hist[i], hist[i], 0, 255, cv::NORM_MINMAX);

        // 绘制直方图
        hist_img[i] = cv::Mat::zeros(hist_size[0]*scale, hist_size[1]*scale, CV_8UC3);

        for(int h=0; h<hist_size[0]; ++h){
            for(int s=0; s<hist_size[1]; ++s){
                float hval = hist[i].at<float>(h,s);
                cv::rectangle(hist_img[i],
                              cv::Rect(h*scale, s*scale, scale, scale),
                              cv::Scalar::all(hval), -1);
            }
        }

    }

    // 显示
    int index=0;
    cv::namedWindow("0source", cv::WINDOW_AUTOSIZE); cv::imshow("0source", hsv[index]);
    cv::namedWindow("0H-S Histogram", cv::WINDOW_AUTOSIZE); cv::imshow("0H-S Histogram", hist_img[index++]);

    cv::namedWindow("1source", cv::WINDOW_AUTOSIZE); cv::imshow("1source", hsv[index]);
    cv::namedWindow("1H-S Histogram", cv::WINDOW_AUTOSIZE); cv::imshow("1H-S Histogram", hist_img[index++]);

    cv::namedWindow("2source", cv::WINDOW_AUTOSIZE); cv::imshow("2source", hsv[index]);
    cv::namedWindow("2H-S Histogram", cv::WINDOW_AUTOSIZE); cv::imshow("2H-S Histogram", hist_img[index++]);

    cv::namedWindow("3source", cv::WINDOW_AUTOSIZE); cv::imshow("3source", hsv[index]);
    cv::namedWindow("3H-S Histogram", cv::WINDOW_AUTOSIZE); cv::imshow("3H-S Histogram", hist_img[index++]);

    cv::namedWindow("4source", cv::WINDOW_AUTOSIZE); cv::imshow("4source", hsv[index]);
    cv::namedWindow("4H-S Histogram", cv::WINDOW_AUTOSIZE); cv::imshow("4H-S Histogram", hist_img[index++]);

    // 比较直方图 src0 vc src1 , vs src2, vs src3, vs src4
    std::cout << "Comparsion:\n"
             << "\t\t  Corr    Chi    Intersect   Bhat\n";

    for(int i=1; i<5; ++i){
        std::cout << "Hist[0] vs Hist[" << i << "]: ";
        for(int j=0;j<4;j++){  // 4种算法
            std::cout << /*"method[" << j << "]: "  <<*/ cv::compareHist(hist[0], hist[i], j) << "  ";
        }
        std::cout << std::endl;
    }

    // 使用EMD方法比较并输出数据
    std::vector<cv::Mat> sig(5);
    std::cout << "\nEMD:" << std::endl;

    // 转换为签名形式
    for(int i=0; i<5;++i){

        std::vector<cv::Vec3f> sigv;

        // 归一化直方图使得总和为1
        cv::normalize(hist[i], hist[i], 1, 0, cv::NORM_L1);
        // 转换
        for(int h=0; h<h_bins; ++h){
            for(int s=0; s<s_bins; ++s){
                float bin_val = hist[i].at<float>(h,s);
                if(bin_val != 0){
                    sigv.push_back(cv::Vec3f(bin_val, float(h), float(s)));
                }
            }
        }

        // 转换为 Nx3 32FC1的矩阵,n是直方图区间的个数
        sig[i] = cv::Mat(sigv).clone().reshape(1);

        if(i > 0){
            std::cout << "Hist[0] vs Hist[" << i << "]:"
                      << cv::EMD(sig[0], sig[i], CV_DIST_L2) << std::endl;
        }

    }

}


反向投影

反向投影是计算像素和直方图模型中像素吻合度的一种方法。比如,现有肤色的直方图,可以使用反向投影在图像中寻找肤色区域。

OpenCv提供实现这种基础操作的函数:cv::calcBackProject(),该函数从输入图像的指定通道中计算出一个向量。不同于cv::calcHist()在直方图中记录累计值,反向投影从输入图像的直方图中读取当前像素对应的计数值作为结果。比如,计算输入图像的H-S直方图,在某个区间的计数概率为 p p p,那么就将当前图像中在该区间的所有像素置为 p p p

从统计学的角度来看,如果将输入的直方图视为某个物体上特定的向量的一个概率分布,那么反向投影就是计算图片上某个特定部分来自该先验分布的概率。

函数声明如下:

void cv::calcBackProject(const cv::Mat* images, int nimages, 
                         const int* channels, 
                         cv::InputArray hist, cv::OutputArray backProject, 
                         const float** ranges, double scale, 
                         bool uniform = true);

void cv::calcBackProject(const cv::Mat* images, int nimages,
                        const int* channels,
                        const cv::SparseMat& hist,
                         cv::OutputArray backProject,
                        const float** ranges, double scale = 1,
                        bool uniform = true);

void cv::calcBackProject(cv::InputArrayOfArrays images,
                        const vector<int>& channels,
                        cv::InputArray hist,
                        cv::OutputArray backProject,
                        const vector<float>& ranges,
                        double scale = 1,
                        bool uniform = true);

cv::calcBackProject()函数以单通道或是多通道数据集合的形式提供图像,即传入的images参数。同时以cv::calcHist()输出的直方图的格式传入直方图,即hist参数。如果使用C风格数组,需要参数nimages指定输入数组中元素的个数。

参数channels指定在反向投影中发挥作用的通道的列表,该参数与cv::calcHist()时的参数形式一致。

反向投影的计算结果保存在参数backProject中,该参数和images[0]的大小类型相同,只有一个通道。

参数ranges作用与cv::calcHist中的参数作用一致。

参数scale是一个可选的应用于backProject的缩放因子,如果希望对结果可视化,这个参数很有用。

参数uniform表示输入的直方图是不是一个均匀的直方图,和cv::calcHist中意义相同。

在新的图片中寻找一个物体或是需要的区域的一个方法如下:

  1. 创建要寻找的物体或区域的直方图;
  2. 为了在新图片中找到它,使用步骤1中的直方图对新图片进行cv::calcBackProject()以创建一个反向投影图,在反向投影图中,具有最大值的区域很可能包含感兴趣的物体;
  3. 反向投影中的高值区域取局部直方图并使用cv::compareHist()将它与要寻找的物体或区域的直方图进行比较;

模板匹配

OpenCv提供函数cv::matchTemplate()进行模板匹配,但是该函数实现原理不基于直方图,而是使用一个图像块在输入图像上进行滑动,并用指定的匹配方法进行比较。

cv::matchTemplate()函数声明:

void cv::matchTemplate(cv::InputArray image,
                      cv::InputArray temp1,
                      cv::OutputArray result,
                      int method);

参数image是一幅8bit或浮点型灰度或彩色图像。

参数temp1是一个包含给定物体的另一张图片上选取的模板图片块,另一张图片和当前图片相似。

参数result存放计算结果,是一个单通道、大小为image.width - temp1.width +1, image.height - temp1.height +1、以整数字节或浮点型存储的图片。

参数method表示匹配方法,每一种匹配方法,都有一个归一化的版本。归一化的方法可以降低模板和图片之间由于光照不同引起的效果。具体方法如下所示, I I I表示输入图像, T T T表示模板, R R R表示输出的结果图像。

  • 方差匹配方法cv::TM_SQDIFF,以方差为依据进行匹配,完全匹配结果为0, 不匹配会得到一个很大的值。计算公式: R s q _ d i f f = ∑ x ′ , y ′ [ T ( x ′ , y ′ ) − I ( x + x ′ , y + y ′ ) ] 2 R_{sq\_diff} = \sum_{x', y'}^{} [T(x', y') - I(x+x', y+y')]^2 Rsq_diff=x,y[T(x,y)I(x+x,y+y)]2

  • 归一化方差匹配方法cv::TM_SQDIFF_NORMED,是方差匹配方法的归一化版本,完全匹配结果为0。计算公式:$R_{sq_diff_normed} = \frac{ \sum_{x’, y’}^{} [T(x’, y’) - I(x+x’, y+y’)]^2 }{\sqrt{ \sum_{x’, y’}^{} T(x’, y’)^2 \cdot \sum_{x’, y’}^{} I(x+x’, y+y’)^2 } } $。

  • 相关性匹配方法cv::TM_CCORR,以相乘的方式进行模板匹配,完全匹配结果值很大,不匹配会得到一个很小的值或0。计算公式: R c c o r r = ∑ x ′ , y ′ T ( x ′ , y ′ ) ⋅ I ( x + x ′ , y + y ′ ) R_{ccorr} = \sum_{x', y'}^{} T(x', y') \cdot I(x+x', y+y') Rccorr=x,yT(x,y)I(x+x,y+y)

  • 归一化的互相关匹配方法cv::TM_CCORR_NORMED,是相关性匹配方法的归一化版本,图片和模板极度不匹配,结果为0。计算公式:$R_{ccorr_normed} = \frac{R_{ccorr} = \sum_{x’, y’}^{} T(x’, y’) \cdot I(x+x’, y+y’)}{\sqrt{\sum_{x’, y’} T(x’, y’)^2} \cdot \sum _{x’, y’} I(x+x’, y+y’)^2 } $。

  • 相关系数匹配方法cv::TM_CCOEFF,该方法以模板相对于其平均值的变化和图片相对于其平均值的变化进行匹配,完全匹配会得到1,完全不匹配得到-1, 如果是随机图片匹配得到0。计算公式:

R c c o e f f = ∑ x ′ , y ′ T ′ ( x ′ , y ′ ) ⋅ I ′ ( x + x ′ , y + y ′ ) T ′ ( x ′ , y ′ ) = T ( x ′ , y ′ ) − ∑ x ′ ′ , y ′ ′ T ( x ′ ′ , y ′ ′ ) w − h I ′ ( x + x ′ , y + y ′ ) = I ( x + x ′ , y + y ′ ) − ∑ x ′ ′ , y ′ ′ I ( x ′ ′ , y ′ ′ ) w − h R_{ccoeff} = \sum_{x', y'}^{} T'(x', y') \cdot I'(x+x', y+y') \\ T'(x', y') = T(x', y') - \frac{\sum_{x'', y''} T(x'', y'')}{w-h} \\ I'(x+x', y+y') = I(x+x', y+y') - \frac{\sum_{x'', y''} I(x'', y'')}{w-h} Rccoeff=x,yT(x,y)I(x+x,y+y)T(x,y)=T(x,y)whx′′,y′′T(x′′,y′′)I(x+x,y+y)=I(x+x,y+y)whx′′,y′′I(x′′,y′′)

  • 归一化的相关系数匹配方法cv::TM_CCOEFF_NORMED,相关系数匹配方法的归一化版本,相对好的匹配得到正值,误匹配得到负值。计算公式: R c c o e d d _ n o r m e d = ∑ x ′ , y ′ T ′ ( x ′ , y ′ ) ⋅ I ′ ( x + x ′ , y + y ′ ) ∑ x ′ , y ′ T ′ ( x ′ , y ′ ) 2 ⋅ ∑ x ′ , y ′ I ′ ( x + x ′ , y + y ′ ) 2 R_{ccoedd\_normed} = \frac {\sum_{x', y'} T'(x', y') \cdot I'(x + x', y+ y')}{\sqrt { \sum_{x', y'} T'(x', y')^2 \cdot \sum_{x', y'} I'(x+x', y+y')^2 }} Rccoedd_normed=x,yT(x,y)2x,yI(x+x,y+y)2 x,yT(x,y)I(x+x,y+y),其中 T ′ T' T I ′ I' I与相关系数匹配中含义一致。

实际使用中可以从一个相对简单的度量方法换成一个更复杂的度量方法,比如从方差换成相关系数,会得到更精确的结果,但同时计算时间也会更长。最好的办法是尝试所有的度量方法,选择一个可以兼顾精确度和计算时间的最好的方法。


五、 MATLAB中的直方图操作

MATLAB提供了一些直方图操作函数。

计算并显示图像直方图

首先imread函数读取图像,然后imhist函数计算图像直方图,最后使用图窗绘制函数subplot及图像显示函数imshow将原图及结果显示出来。具体参考代码如下:

MATLAB版本:R2014B

%% 显示图像直方图

filename = './images/lena.jpg';
img = imread(filename);

% 如果是彩色图,转换为灰度图
if ndims(img) < 3
  grayimg = img;
else
    grayimg = rgb2gray(img);
end

% 显示原图
subplot(2,2,1); imshow(img); title('原图');
subplot(2,2,2); imshow(grayimg); title('灰度图');
% 计算并显示直方图,传入的图像为灰度图
subplot(2,2,[3,4]); bar(imhist(grayimg, 256), 'b');title('图像直方图');

直方图归一化

利用imhist()得到图像直方图,将该直方图除以像素总数即得到归一化的直方图。

MATLAB版本:R2014B

%% 图像直方图归一化

filename = './images/lena.jpg';
img = imread(filename);

% 如果是彩色图,转换为灰度图
if ndims(img) < 3
  grayimg = img;
else
    grayimg = rgb2gray(img);
end

% 显示原图
subplot(2,2,1); imshow(img); title('原图');
subplot(2,2,2); imshow(grayimg); title('灰度图');
% 计算并显示直方图,传入的图像为灰度图,默认划分256个灰度级,可以设置该参数
h = imhist(grayimg, 256);  % 计算图像直方图
subplot(2,2,3); bar(h, 'b'); title('图像直方图');
subplot(2,2,4); bar(h/numel(grayimg), 'r'); title('图像直方图归一化');

直方图均衡化

histeq函数实现图像直方图均衡化。

MATLAB版本:R2014B

%% 直方图均衡化

filename = './images/lena.jpg';
img = imread(filename);

% 如果是彩色图,转换为灰度图
if ndims(img) < 3
  grayimg = img;
else
    grayimg = rgb2gray(img);
end

% 显示原图
subplot(2,2,1); imshow(grayimg); title('原始灰度图');
% 计算并显示直方图,传入的图像为灰度图,默认划分256个灰度级,可以设置该参数
h1 = imhist(grayimg, 256);  % 计算图像直方图
subplot(2,2,3); bar(h1, 'b'); title('原始灰度直方图');
e = histeq(grayimg);  % 直方图均衡化
h2 = imhist(e);  % 计算均衡化后图像的直方图
subplot(2,2,2); imshow(e); title('均衡化后的图像');
subplot(2,2,4); bar(h2, 'b'); title('均衡化后图像的灰度直方图');

正比例变换

加入一幅图像的灰度范围在 [ 40 , 200 ] [40,200] [40,200]之间,将其缩放到 [ 0 , 255 ] [0, 255] [0,255]范围。

MATLAB版本:R2014B

%% 正比例变换
clc;
filename = './images/lena.jpg';
img = imread(filename);
if ndims(img) < 3
  grayimg = img;
else
    grayimg = rgb2gray(img);
end

subplot(2,2,1); imshow(grayimg); title('(a)原始灰度图');
subplot(2,2,2); imhist(grayimg); title('(b)原始灰度图的统计直方图')
l = min(min(grayimg))
h = max(max(grayimg))
% 原始图像灰度值最大为20, 最小为245,将其缩放到[0, 255]
a = imadjust(grayimg, [20/255, 245/255], [0, 1]);
subplot(2,2,3); imshow(a); title('(c)变换图像');
subplot(2,2,4); imhist(a); title('(d)变换图像的灰度统计直方图')

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/875673.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kafka的下载安装以及使用

一、Kafka下载 下载地址&#xff1a;https://kafka.apache.org/downloads 二、Kafka安装 因为选择下载的是 .zip 文件&#xff0c;直接跳过安装&#xff0c;一步到位。 选择在任一磁盘创建空文件夹&#xff08;不要使用中文路径&#xff09;&#xff0c;解压之后把文件夹内容…

2014-2022年阿里淘宝村省市县数据

2009-2022年阿里淘宝村-省市县数据&#xff08;原始数据汇总&#xff09; 从萌芽到扩散&#xff0c;再到大规模、集群式增长&#xff0c;生机勃勃的“淘宝村”和“淘宝镇”已成为中国农村电商发展的典范。2022年&#xff0c;在全面推动乡村振兴的进程中&#xff0c;又有一批村…

ChatGPT收录

VSCode插件-ChatGPT 多磨助手 多磨助手 (domore.run) Steamship Steamship 免费合集 免费chatGPT - Ant Design Pro 免费AI聊天室 (xyys.one)

OceanMind海睿思受邀出席2023长三角数字化大会,斩获两项数字化转型年度大奖

8月10日&#xff0c;由江苏省工业和信息化厅指导&#xff0c;长三角首席信息官联盟主办&#xff0c;江苏省企业信息化协会承办的“2023年长三角数字化转型大会”在江苏南京成功召开。 本次大会以“工业互联智造未来”为主题&#xff0c;旨在促进产业互联网的发展&#xff0c;并…

JavaScript函数声明与函数表达式

在 JavaScript 中&#xff0c;可以通过两种方式来定义函数&#xff1a;函数声明和函数表达式。 函数声明使用 function 关键字进行定义&#xff0c;并且在整个作用域中都可用。 函数声明的方式&#xff1a; function calcAge1(birthYear) {return 2037 - birthYear; } const …

制造执行系统(MES)在新能源领域的应用

制造执行系统&#xff08;MES&#xff09;在新能源领域有许多应用&#xff0c;特别是在管理、监控和优化新能源生产过程方面。新能源包括太阳能、风能、生物质能、地热能等。以下是一些MES在新能源方面的应用领域&#xff1a; 生产计划与调度&#xff1a;MES可以协助规划和调度…

FiboSearch Pro – Ajax Search for WooCommerce 商城AJAX实时搜索插件

FiboSearch Pro是最受欢迎的WooCommerce 产品搜索插件。它为您的用户提供精心设计的高级 AJAX 搜索栏&#xff0c;并提供实时搜索建议。默认情况下&#xff0c;WooCommerce 提供非常简单的搜索解决方案&#xff0c;没有实时产品搜索&#xff0c;甚至没有 SKU 搜索。FiboSearch&…

多种求组合数算法

目录 求组合数Ⅰ&#xff08;递推&#xff09;核心理论理论推导典型例题代码实现 求组合数Ⅱ&#xff08;预处理&#xff09;核心理论典型例题代码实现 求组合数Ⅲ&#xff08;Lucas定理&#xff09;核心理论Lucas定理的证明1.证明Lucas定理的第一形式2.证明Lucas定理的第二形式…

顺序表的插入,删除,修改和查找(详细解析)

目录 一.顺序表的初始化----静态分配 二.顺序表的初始化----动态分配 三.顺序表的插入 1.插入操作 2.插入操作的时间复杂度 三.顺序表的删除操作 1.顺序表的删除 2.删除操作的时间复杂度 四.顺序表的查找 1.按位查找操作&#xff1a;查找第i位置的元素 2.按位查找操作…

嘉实基金:金融科技与开源治理的双驱动,打造安全的资管业务数字系统

嘉实基金成立于1999年&#xff0c;是国内最早成立的十家基金管理公司之一&#xff0c;也是国内首家资产管理规模超过千亿的基金公司。作为一家领先的投资管理公司&#xff0c;嘉实基金也在数字化转型趋势中&#xff0c;积极推进金融科技在金融资管行业中的探索实践。为了支撑基…

7-zip 更换图标:定制你的7-Zip

7-Zip Theme Manager是一个用于管理和应用自定义主题的软件工具&#xff0c;专门为7-Zip文件压缩软件开发。它允许用户选择并应用各种主题来改变7-Zip的外观和用户界面&#xff0c;包括颜色方案、图标集、按钮样式等。通过更改主题&#xff0c;用户可以使7-Zip界面更加美观、易…

【PostgreSQL的CLOG解析】

同样还是这张图&#xff0c;之前发过shared_buffer和os cache、wal buffer和work mem的文章&#xff0c;今天的主题是图中的clog&#xff0c;即 commit log&#xff0c;PostgreSQL10之前放在数据库目录的pg_clog下面。PostgreSQL10之后修更名为xact,数据目录变更为pg_xact下面&…

【 运维这些事儿 】- Gerrit代码审查详

文章目录 背景作用代码审查工具Gerrit镜像构建Dockerfile 部署配置 Gitlab代码同步ssh-agent 相关概念常用命令Git 配置使用 Git Review针对已有项目添加commit-msg&#xff0c;用于自动添加changeId添加源配置 .gitreview备注指定审核人自定义git命令 开发使用代码审查 背景 …

image has dependent child images

问题&#xff1a;很多none的镜像无法被删除 解决过程&#xff1a; 1、通过 docker image prune -f 提示可删除为 0 2、直接进行删除报错&#xff1a; docker rmi 8f5116cbc201Error response from daemon: conflict: unable to delete 8f5116cbc201 (cannot be forced) - im…

面试题-React(一):React是什么?它的主要特点是什么?

探索React&#xff1a;前端开发中的重要角色与主要特点 引言&#xff1a; 在现代前端开发领域&#xff0c;React已经成为最受欢迎和广泛使用的JavaScript库之一。它由Facebook开发并于2013年首次发布。随着时间的推移&#xff0c;React在开发社区中获得了强大的支持和认可。本…

对锁的理解

悲观锁和乐观锁 每次操作的时候都在上锁解锁&#xff0c;能解决并发中的各种问题&#xff0c;不支持并发操作&#xff0c;效率低 每次操作都加上版本号&#xff0c;操作提交的时候会比较数据库版本和目前的版本号是否一致&#xff0c;不一致就会提交失败 表锁和行锁 表锁是对…

Joint HDR Denoising and Fusion: A Real-World Mobile HDR Image Dataset

Abstract 手机已经成为我们日常生活中无处不在、不可或缺的拍照设备&#xff0c;而小光圈和传感器尺寸使得手机更容易受到噪点和过饱和的影响&#xff0c;导致动态范围(LDR)低、画质低。 因此&#xff0c;为手机开发高动态范围(HDR)成像技术至关重要。 然而&#xff0c;现有的…

SpringBean的生命周期和循环依赖

Spring循环依赖 前言 大制作来啦&#xff0c;spring源码篇&#xff0c;很早之前我就想写一系列spring源码篇了&#xff0c;正好最近总是下雨&#xff0c;不想出门&#xff0c;那就让我来带大家走进Spring源码世界吧。 阅读建议 spring源码读起来有点难度&#xff0c;需要多Deb…

性价比最高的护眼灯,护眼台灯应该怎么挑选

随着技术的发展&#xff0c;灯光早已成为每家每户都需要的东西。但是灯光不好可能会对眼睛造成伤害是很多人没有注意到的。现在随着护眼灯产品越来越多&#xff0c;市场上台灯的选择越来越多样化&#xff0c;如何选择一个对眼睛无伤害、无辐射的台灯成为许多家长首先要考虑的问…

limereport报表使用

在这里我使用报表是以报表的形式显示数据库的信息。所以首先需要准备的资料有&#xff1a;limereport源码&#xff0c;还有数据库&#xff0c;我这里使用的是qsqlite数据库。 1、下载limereport报表源码 2、运行自带的案例&#xff1a;demo_r1 3、点击 “Run Report Designer”…