研究这个前前后后也有快两三个月了,因为之前也一直在弄模板匹配方面的东西,所以偶尔还是有不少朋友咨询或者问你有没有研究过linemod这个算法啊,那个效率啥的还不错啊,有段时间一直不以为然,觉得我现在用的那个匹配因该很不错的,没必要深究了。后来呢,还是忍不住手痒,把论文打出来看了看,又找了点资料研究了下,结果没想到一弄又是两个月过去了,中间也折腾了很久,浪费了不少时间。总算还是有点收获,稍微整理下做个交流。

至于linemod的具体数学原理,我也不需要详谈,毕竟论文和opencv的代码就摆在那里, github上也有一些别人改进的版本。

我就觉得啊,linemod这个基于计算边缘的模板匹配啊,他使用的是选中的特征点的梯度的角度方向作为特征,而不是梯度的值,而后计算模板和测试不同位置角度的余弦的绝对值,这个都是常规的操作。 作者把这个角度线性量化为一些特定的值,这个本质上呢降低了算法的精度,但是由于特征点较多,基本不会影响识别结果。  关键是这个量化啊,能够带来很多很多的好处,有些真的是意想不到。

  论文里呢把360的角度量化为8个值,即以45度为间隔,分别用整数0、1、2、3、4、5、6、7表示,这样呢不同的两个角度之间的差异绝对值呢,只有0、1、2、3、4这5种可能,分别对应5个得分,比如模板的某个特征点的角度为210度,则量化值为4,目标中某个位置的角度值为52度,则量化值为1,这样角度之间差异值为3,则对应的得分为1。

  接着论文里说为了减少微小的变形引起的识别误差,建议将量化后的值进行扩散,这个扩散也是设计的非常有技巧,很有意思,充分利用了或运算的优异特性。

后续说为了减少计算量呢,可以提前计算出8个响应图,这样匹配计算时就可以直接查表,而无需实时计算。  

再后续还有一个线性化内存,算了,我已经没看那个了,到前面这一步就已经打止了,因为我已经开始编程了。

第一步呢,我就是在考虑算法的优化问题,我看了下opencv的代码,写的很好,又很不好,让你读的很难受,但是写的确实稳健,考虑到了很多不同的硬件配置,这也许就是大工程的特性吧。

角度量化的问题和代码方面我不想提,也有很多可优化的地方,大家可自行考虑。

在谈到提速之前,我说一个重点,那就是所谓的梯度扩散、计算响应图都是在查找模板时进行的, 对不同的图都要有重新计算,而不是离线玩。所以这里的每个耗时,都和检测速度有关。

第一:那个梯度扩散,CV的代码有下面这一大堆:

/****************************************************************************************\
* Response maps *
\****************************************************************************************
static void orUnaligned8u(const uchar * src, const int src_stride,
uchar * dst, const int dst_stride,
const int width, const int height)
{
#if CV_SSE2
volatile bool haveSSE2 = checkHardwareSupport(CPU_SSE2);
#if CV_SSE3
volatile bool haveSSE3 = checkHardwareSupport(CPU_SSE3);
#endif
bool src_aligned = reinterpret_cast<unsigned long long>(src) % 16 == 0;
#endif for (int r = 0; r < height; ++r)
{
int c = 0; #if CV_SSE2
// Use aligned loads if possible
if (haveSSE2 && src_aligned)
{
for ( ; c < width - 15; c += 16)
{
const __m128i* src_ptr = reinterpret_cast<const __m128i*>(src + c);
__m128i* dst_ptr = reinterpret_cast<__m128i*>(dst + c);
*dst_ptr = _mm_or_si128(*dst_ptr, *src_ptr);
}
}
#if CV_SSE3
// Use LDDQU for fast unaligned load
else if (haveSSE3)
{
for ( ; c < width - 15; c += 16)
{
__m128i val = _mm_lddqu_si128(reinterpret_cast<const __m128i*>(src + c));
__m128i* dst_ptr = reinterpret_cast<__m128i*>(dst + c);
*dst_ptr = _mm_or_si128(*dst_ptr, val);
}
}
#endif
// Fall back to MOVDQU
else if (haveSSE2)
{
for ( ; c < width - 15; c += 16)
{
__m128i val = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src + c));
__m128i* dst_ptr = reinterpret_cast<__m128i*>(dst + c);
*dst_ptr = _mm_or_si128(*dst_ptr, val);
}
}
#endif
for ( ; c < width; ++c)
dst[c] |= src[c]; // Advance to next row
src += src_stride;
dst += dst_stride;
}
} /**
* \brief Spread binary labels in a quantized image.
*
* Implements section 2.3 "Spreading the Orientations."
*
* \param[in] src The source 8-bit quantized image.
* \param[out] dst Destination 8-bit spread image.
* \param T Sampling step. Spread labels T/2 pixels in each direction.
*/
static void spread(const Mat& src, Mat& dst, int T)
{
// Allocate and zero-initialize spread (OR'ed) image
dst = Mat::zeros(src.size(), CV_8U); // Fill in spread gradient image (section 2.3)
for (int r = 0; r < T; ++r)
{
int height = src.rows - r;
for (int c = 0; c < T; ++c)
{
orUnaligned8u(&src.at<unsigned char>(r, c), static_cast<int>(src.step1()), dst.ptr(),
static_cast<int>(dst.step1()), src.cols - c, height);
}
}
}

  我翻译成我容易接受的代码,并且剔除一些对硬件环境的判断的语句,如下所示:

void IM_Spread_Ori(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int T)
{
memset(Dest, 0, Height * Stride);
for (int J = 0; J < T; J++)
{
int H = Height - J;
for (int I = 0; I < T; I++)
{
int W = Width - I;
int BlockSize = 16, Block = W / BlockSize;
unsigned char *SrcP = Src + J * Stride + I;
unsigned char *DestP = Dest;
for (int Y = 0; Y < H; Y++)
{
for (int X = 0; X < Block * BlockSize; X += BlockSize)
{
__m128i SrcV = _mm_loadu_si128((__m128i*)(SrcP + X));
__m128i DstV = _mm_loadu_si128((__m128i*)(DestP + X));
_mm_storeu_si128((__m128i *)(DestP + X), _mm_or_si128(SrcV, DstV));
}
for (int X = Block * BlockSize; X < W; X++)
{
DestP[X] |= SrcP[X];
}
SrcP += Stride;
DestP += Stride;
}
}
}
}

  要说啊,这个代码本身来说有是个比较高效的代码了,但是,我一想到论文中的T=8,那就意味着差不多是8*8=64次全图这样的数据or操作,哪怕就算or操作再快, 这个也不太可能快过 64次memcpy的,特别当一个图比较大的时候,这个就有点明显了,我测试了下,对于一个3000*3000的灰度图(工业上遇到这么大的图应该不算离谱吧),初步测试了下,居然需要大概200ms的时间,对于模板匹配这种需要高频操作的需求来说,单独这一步的耗时还是大了点。

  有么有在不改变效果的情况下,进一步提高这个算法的方法呢,其实是有的,我们知道or操作时不分前后顺序的,即多个数据or可以随便谁和谁先操作,因此,我们可以安排T行之间先or,然后再对结果记性T列之间or操作,这样则只需要2*T次or计算,而且有一些额外的好处就是避免很多cache miss,这是隐藏的速度提升。

  改写后的代码如下所示:

void IM_Spread(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int T)
{
// 利用了或运算的行和列分离特性
memset(Dest, 0, Height * Stride);
int BlockSize = 16, Block = Width / BlockSize;
for (int Y = 0; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = Dest + Y * Stride;
for (int J = 0; J < ((Y + T > Height) ? Height - Y : T); J++)
{
// 高度方向依次向下进行T次或运算
for (int X = 0; X < Block * BlockSize; X += BlockSize)
{
__m128i SrcV = _mm_loadu_si128((__m128i*)(LinePS + X));
__m128i DstV = _mm_loadu_si128((__m128i*)(LinePD + X));
_mm_storeu_si128((__m128i *)(LinePD + X), _mm_or_si128(SrcV, DstV));
}
for (int X = Block * BlockSize; X < Width; X++)
{
LinePD[X] |= LinePS[X];
}
LinePS += Stride; // 源数据向下移动,目标数据不动
}
} BlockSize = 16, Block = (Width - T) / BlockSize; for (int Y = 0; Y < Height; Y++)
{
unsigned char *LinePD = Dest + Y * Stride;
for (int X = 0; X < Block * BlockSize; X += BlockSize)
{
__m128i Value = _mm_setzero_si128();
for (int I = 0; I < T; I++)
{
__m128i SrcV = _mm_loadu_si128((__m128i*)(LinePD + X + I));
Value = _mm_or_si128(Value, SrcV);
}
// 这个读取和写入是没有重叠的,所以可以不利用中间内存
_mm_storeu_si128((__m128i *)(LinePD + X), Value);
}
for (int X = Block * BlockSize; X < Width; X++)
{
int Value = 0;
for (// 此处删除部分代码,供读者自行补充 )
{
Value = Value | LinePD[X + I];
}
LinePD[X] = Value;
}
}
}

  同样的测试图,同样的T=8,速度一下子提升到了45ms左右,有近5倍的速度提升。

  为什么我会分享这段代码呢,因为后面我发现他根本没什么卵用。

  第二:那个计算响应图的代码,也可以继续优化。

// Auto-generated by create_similarity_lut.py
CV_DECL_ALIGNED(16) static const unsigned char SIMILARITY_LUT[256] = {0, 4, 3, 4, 2, 4, 3, 4, 1, 4, 3, 4, 2, 4, 3, 4, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 0, 3, 4, 4, 3, 3, 4, 4, 2, 3, 4, 4, 3, 3, 4, 4, 0, 1, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 0, 2, 1, 2, 0, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 0, 3, 2, 3, 1, 3, 2, 3, 0, 3, 2, 3, 1, 3, 2, 3, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 0, 4, 3, 4, 2, 4, 3, 4, 1, 4, 3, 4, 2, 4, 3, 4, 0, 1, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 3, 4, 4, 3, 3, 4, 4, 2, 3, 4, 4, 3, 3, 4, 4, 0, 2, 1, 2, 0, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 0, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 0, 3, 2, 3, 1, 3, 2, 3, 0, 3, 2, 3, 1, 3, 2, 3, 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4}; /**
* \brief Precompute response maps for a spread quantized image.
*
* Implements section 2.4 "Precomputing Response Maps."
*
* \param[in] src The source 8-bit spread quantized image.
* \param[out] response_maps Vector of 8 response maps, one for each bit label.
*/
static void computeResponseMaps(const Mat& src, std::vector<Mat>& response_maps)
{
CV_Assert((src.rows * src.cols) % 16 == 0); // Allocate response maps
response_maps.resize(8);
for (int i = 0; i < 8; ++i)
response_maps[i].create(src.size(), CV_8U); Mat lsb4(src.size(), CV_8U);
Mat msb4(src.size(), CV_8U); for (int r = 0; r < src.rows; ++r)
{
const uchar* src_r = src.ptr(r);
uchar* lsb4_r = lsb4.ptr(r);
uchar* msb4_r = msb4.ptr(r); for (int c = 0; c < src.cols; ++c)
{
// Least significant 4 bits of spread image pixel
lsb4_r[c] = src_r[c] & 15;
// Most significant 4 bits, right-shifted to be in [0, 16)
msb4_r[c] = (src_r[c] & 240) >> 4;
}
} #if CV_SSSE3
volatile bool haveSSSE3 = checkHardwareSupport(CV_CPU_SSSE3);
if (haveSSSE3)
{
const __m128i* lut = reinterpret_cast<const __m128i*>(SIMILARITY_LUT);
for (int ori = 0; ori < 8; ++ori)
{
__m128i* map_data = response_maps[ori].ptr<__m128i>();
__m128i* lsb4_data = lsb4.ptr<__m128i>();
__m128i* msb4_data = msb4.ptr<__m128i>(); // Precompute the 2D response map S_i (section 2.4)
for (int i = 0; i < (src.rows * src.cols) / 16; ++i)
{
// Using SSE shuffle for table lookup on 4 orientations at a time
// The most/least significant 4 bits are used as the LUT index
__m128i res1 = _mm_shuffle_epi8(lut[2*ori + 0], lsb4_data[i]);
__m128i res2 = _mm_shuffle_epi8(lut[2*ori + 1], msb4_data[i]); // Combine the results into a single similarity score
map_data[i] = _mm_max_epu8(res1, res2);
}
}
}
else
#endif
{
// For each of the 8 quantized orientations...
for (int ori = 0; ori < 8; ++ori)
{
uchar* map_data = response_maps[ori].ptr<uchar>();
uchar* lsb4_data = lsb4.ptr<uchar>();
uchar* msb4_data = msb4.ptr<uchar>();
const uchar* lut_low = SIMILARITY_LUT + 32*ori;
const uchar* lut_hi = lut_low + 16; for (int i = 0; i < src.rows * src.cols; ++i)
{
map_data[i] = std::max(lut_low[ lsb4_data[i] ], lut_hi[ msb4_data[i] ]);
}
}
}
}

  看上去又是一大堆代码,简化后如下所示:

void IM_ComputeResponseMaps_Slow(unsigned char *Src, unsigned char **ResponseMaps, int Width, int Height)
{
static const unsigned char SIMILARITY_LUT[256] = { 0, 4, 3, 4, 2, 4, 3, 4, 1, 4, 3, 4, 2, 4, 3, 4, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 0, 3, 4, 4, 3, 3, 4, 4, 2, 3, 4, 4, 3, 3, 4, 4, 0, 1, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 0, 2, 1, 2, 0, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 0, 3, 2, 3, 1, 3, 2, 3, 0, 3, 2, 3, 1, 3, 2, 3, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 0, 4, 3, 4, 2, 4, 3, 4, 1, 4, 3, 4, 2, 4, 3, 4, 0, 1, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 3, 4, 4, 3, 3, 4, 4, 2, 3, 4, 4, 3, 3, 4, 4, 0, 2, 1, 2, 0, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 0, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 0, 3, 2, 3, 1, 3, 2, 3, 0, 3, 2, 3, 1, 3, 2, 3, 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4 };
unsigned char *lsb4 = (unsigned char *)malloc(Width * Height);
unsigned char *msb4 = (unsigned char *)malloc(Width * Height); for (int Y = 0; Y < Height * Width; Y++)
{
lsb4[Y] = Src[Y] & 15;
msb4[Y] = (Src[Y] & 240) >> 4;
}
int BlockSize = 16, Block = (Width * Height) / BlockSize;
for (int Z = 0; Z < 8; Z++)
{
for (int Y = 0; Y < Block * BlockSize; Y += BlockSize)
{
__m128i Res1 = _mm_shuffle_epi8(_mm_loadu_si128((__m128i *)(SIMILARITY_LUT + 32 * Z + 0)), _mm_loadu_si128((__m128i *)(lsb4 + Y)));
__m128i Res2 = _mm_shuffle_epi8(_mm_loadu_si128((__m128i *)(SIMILARITY_LUT + 32 * Z + 16)), _mm_loadu_si128((__m128i *)(msb4 + Y)));
_mm_storeu_si128((__m128i *)(ResponseMaps[Z] + Y), _mm_max_epu8(Res1, Res2));
}
for (int Y = Block * BlockSize; Y < Width * Height; Y++)
{
ResponseMaps[Z][Y] = IM_Max(SIMILARITY_LUT[lsb4[Y] + 32 * Z], SIMILARITY_LUT[msb4[Y] + 32 * Z + 16]);
}
}
free(lsb4);
free(msb4);
}

  同样3000*3000的测试图,这个函数平均耗时80ms,也算是非常快的。

  那有没有改进空间呢,其实是有的,下面这两句明显也是可以用SIMD指令优化的嘛,&操作直接由对应的_mm_loadu_si128指令,至于byte类型数据的移位,确实没有直接指令可以使用,但是自己写个又有什么难度呢.

     lsb4[Y] = Src[Y] & 15;
msb4[Y] = (Src[Y] & 240) >> 4;

  比如这样就可以:

 //    无符号字节数据右移四位
inline __m128i _mm_srli4_epu8(__m128i v)
{
v = _mm_srli_epi16(v, 4);
v = _mm_and_si128(v, _mm_set1_epi8(0x0f));
return v;
}

  好了,其他的代码就不需要我写了吧,优化后这个速度能够提高到50ms。

   我从这个代码里最大的收获不是其他的,就是_mm_shuffle_epi8这个语句,利用这个很巧妙的实现了一个查找表的过程,其实我想起来了,在我的博文【算法随记七】巧用SIMD指令实现急速的字节流按位反转算法。 里就已经使用了这个技巧。他能轻松的实现少于16个元素的字节类型的查找表。而且效率比普通的C语言查表方式不知道高了多少倍,后面文章我们还会说道这个指令的一个更为优异的特性。

  说到这里,大家也许会认为我会继续谈下后续的线性内存方面的优化,但是可惜了,后面的我就没有看了,因为我觉得到了这一步,我就已经有了我自己的路可以走了,不需要后续的那个东西。那个可能对我来说还是累赘。于是我耐着性子,在我以前的大框架的基础上,修改局部函数,终于能跑出了初步的效果, 比如下图,我们取T=8时,得到的匹配结果如下:

     整个的基本都错位了。

  而当我们的T取为3时,结果就比较好了。

  但是仔细观察,可以明显的发现目标还是会有一到2个像素的偏移,如果我们T取值为1时,结果如下:

  整体的准确度明显有所提高,为了证明这个结果,在T=1时,我还测试了很多其他图像,结果都表面是完全的,比如一下几幅图像:

    这是两幅很具有代表性的测试图像,一个是高重叠,第二个是强边缘,可以看到效果很准确。

  T=1意味这什么,即不需要进行扩散。所以我们由此没有理由不怀疑论文里结论的准确性。为此,我尝试分析深层次的原因。

  我的测试方法是:从一副图像中剪切一块小的图像,然后直接按照上述代码计算这个小块图像在原图中的各个不同位置的得分,注意到我们得分通常是按照计算后的总分值除以4*特征点的数量。

  具体的测试代码详见附件工程: 扩散结果验证(生成的RAW图像保存D盘根目录下),  这里对算了做一些简单的简化,但不影响实质。

  以下面两幅图为例:

     

  小图在大图中的准确坐标位置为【 104,76】。

  为了显示方便,我们把计算得到的得分值,量化到0和32768之间,然后保存为RAW文件,这样就可以用PS打开查看其像素结果了。当T=1时,可得到如下视觉结果(下左图),明显图中有个最亮的点,我们放大后查看其结果(下右图)。

   

  可以看到准确的定位到了104,76这个坐标。

  当我们选择T=3时,同样的结果如下面所示:

  

  我们可以看到,坐标104,76此处依旧是最大值,但是由于扩散的作用,使得周边也出现了同样的最大值,由于PS只能同时有四个取样点,所以实际上还不止,如果把T扩大到8,那么将会有更多的同样的最大值,这就导致了一个问题,程序无法确定这些最大值那个才是真正的准确的位置了,而又必须确定一个,否则无法得到最终的结果,因此,在上述的T=8的匹配中,由于程序无原则的去了一个最值,然后逐层像金字塔上层传递,每层传递都有可能出现错误,所以导致最终误差越来越大。

  那么结论来了,要想准确的匹配,根本就不需要扩散过程。

  好希望我的结论是错误的啊,本来还想用这扩散来解决模型的建立慢的问题,以及实现可同时检查带有缩放和旋转的匹配呢。可惜,都是泡影。

  虽然如此,但是这个算法还是有很好的价值的,下一篇文章将讲述基于T=1时改算法的进一步扩展和优化,以及如何实现更高效率的算法效果,先分享一个测试工具了:16角度高速模板匹配

【工程应用五】 opencv中linemod模板匹配算法诸多疑惑和自我解读。的更多相关文章

  1. 使用Opencv中matchTemplate模板匹配方法跟踪移动目标

    模板匹配是一种在图像中定位目标的方法,通过把输入图像在实际图像上逐像素点滑动,计算特征相似性,以此来判断当前滑块图像所在位置是目标图像的概率. 在Opencv中,模板匹配定义了6种相似性对比方式: C ...

  2. OpenCV中的模板匹配/Filter2d

    1.模板匹配 模板匹配是在图像中寻找目标的方法之一.Come On, Boy.我们一起来看看模板匹配到底是怎么回事. 参考链接:http://www.opencv.org.cn/opencvdoc/2 ...

  3. 【工程应用七】接着折腾模板匹配算法 (Optimization选项 + no_pregeneration模拟 + 3D亚像素插值)

    在折腾中成长,在折腾中永生. 接着玩模板匹配,最近主要研究了3个课题. 1.创建模型的Optimization选项模拟(2022.5.16日) 这两天又遇到一个做模板匹配隐藏的高手,切磋起来后面就还是 ...

  4. 立体视觉-opencv中立体匹配相关代码

    三种匹配算法比较 BM算法: 该算法代码: view plaincopy to clipboardprint? CvStereoBMState *BMState = cvCreateStereoBMS ...

  5. [OpenCV-Python] OpenCV 中的图像处理 部分 IV (六)

    部分 IVOpenCV 中的图像处理 OpenCV-Python 中文教程(搬运)目录 23 图像变换 23.1 傅里叶变换目标本小节我们将要学习: • 使用 OpenCV 对图像进行傅里叶变换 • ...

  6. 【工程应用一】 多目标多角度的快速模板匹配算法(基于NCC,效果无限接近Halcon中........)

    愿意写代码的人一般都不太愿意去写文章,因为代码方面的艺术和文字中的美学往往很难兼得,两者都兼得的人通常都已经被西方极乐世界所收罗,我也是只喜欢写代码,让那些字母组成美妙的歌曲,然后自我沉浸在其中自得其 ...

  7. OpenCV模板匹配算法详解

    1 理论介绍 模板匹配是在一幅图像中寻找一个特定目标的方法之一,这种方法的原理非常简单,遍历图像中的每一个可能的位置,比较各处与模板是否“相似”,当相似度足够高时,就认为找到了我们的目标.OpenCV ...

  8. 【计算机视觉】OpenCV篇(10) - 模式识别中的模板匹配

    什么是模式识别? 它指的是,对表征事物或现象的各种形式的信息进行处理和分析,从而达到对事物或现象进行描述.辨认.分类和解释的目的. 我们之所以可以很快辨别猫是猫.O不是0,就是因为在我们大脑中已经给猫 ...

  9. [OpenCV-Python] OpenCV 中的图像处理 部分 IV (五)

    部分 IVOpenCV 中的图像处理 OpenCV-Python 中文教程(搬运)目录 22 直方图 22.1 直方图的计算,绘制与分析目标 • 使用 OpenCV 或 Numpy 函数计算直方图 • ...

随机推荐

  1. feiQ发送信息

    import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import j ...

  2. JSP页面 CTRL+F 功能实现

    .res { color: rgba(255, 0, 0, 1) } .result { background: rgba(255, 255, 0, 1) } --- js 部分 var oldKey ...

  3. 【HDU6662】Acesrc and Travel(树型Dp)

    题目链接 大意 给出一颗树,每个点上有一个权值\(A[i]\),有两个绝顶聪明的人甲和乙. 甲乙两人一起在树上轮流走,不能走之前经过的点.(甲乙时刻在一起) 甲先手,并可以确定起点.甲想要走过的点权之 ...

  4. Solution -「CTS 2019」「洛谷 P5404」氪金手游

    \(\mathcal{Description}\)   Link.   有 \(n\) 张卡牌,第 \(i\) 张的权值 \(w_i\in\{1,2,3\}\),且取值为 \(k\) 的概率正比于 \ ...

  5. SonarQube之采购选型参考

    SonarQube是DevOps实践中主流的一款质量内建工具,过插件机制,Sonar 可以集成不同的测试工具,代码分析工具,以及持续集成工具,比如pmd-cpd.checkstyle.findbugs ...

  6. Eureka Server启动过程

    前面对Eureka的服务端及客户端的使用均已成功实践,对比Zookeeper注册中心的使用区别还是蛮大的: P:分区容错性(⼀定的要满⾜的)C:数据⼀致性 A:⾼可⽤:CAP不可能同时满⾜三个,要么是 ...

  7. 实战 | 关于随手一点就发现XSS漏洞这件事

    起因 今天正在做一个项目时,需要找一个云接码平台接码去登录系统.当我随手从百度找了一个接码平台时,一个偶然的发现,有了今天这篇文章. 正文 当我进入这个接码平台随便找了个手机号,然后等了好几分钟都没啥 ...

  8. k8s虚拟机未关闭,电脑重启后,虚拟机无法启动

    莫名其妙电脑重启了,虚拟机连不上,心里真的是一万匹草泥马,显示如下: Generating "/run/initramfs/rdsosreport.txt" Entering em ...

  9. 使用fireworks解决图片隐写的问题

    同样也可以用ps来解决图层间隐写的问题

  10. C++的两种实例化方式

    C++中,类有两种实例化方式.一种是有new关键字,一种没有new关键字.那么,这两种实例化方式有什么区别呢? A a;//(1) a存在于栈上 A* a = new A();//(2) a存在于堆中 ...