OpenCV实战(8)——直方图详解

news2024/11/16 3:30:22

OpenCV实战(8)——直方图详解

    • 0. 前言
    • 1. 直方图概念
    • 2. 直方图计算
      • 2.1 灰度图像直方图计算
      • 2.2 彩色图像直方图计算
    • 3. 应用查找表修改图像
      • 3.1 查找表
      • 3.2 拉伸直方图提高图像对比度
      • 3.3 在彩色图像上应用查找表
    • 4. 图像直方图均衡化
    • 5. 完整代码
    • 小结
    • 系列链接

0. 前言

图像由不同值(颜色)的像素组成,图像中像素值的分布构成了该图像的一个重要特征。本节介绍图像直方图的概念,学习如何计算直方图以及如何使用它来修改图像的外观。直方图还可用于表征图像的内容并检测图像中的特定对象或纹理。

1. 直方图概念

一张图像由若干像素组成,每个像素如果包含一个值(一个通道),则可以组成一张灰度图像;或者如果每个像素包含三个值(三个通道),则可以组成一张彩色图像。每个通道的取值范围为 0255。根据图像的内容,每个灰度值具有不同数量。
图像直方图是一种反映图像色调分布的直方图,其绘制每个色调值的像素数,每个色调值的像素数也称为频率 (frequency)。因此,灰度图像的直方图有 256 个条目(也称柱条或 bin)。bin 0 表示值为 0 的像素数,bin 1 表示值为 1 的像素数,依此类推。显然,如果对直方图的所有 bin 求和,可以得到图像中的像素总数。直方图也可以归一化,使得 bin 的总和等于 1,在这种情况下,每个 bin 都表示图像中具有此色调值的像素数所占百分比。
使用 cv::calcHist() 函数可以方便的计算图像直方图。这是一个通用函数,可以计算任何像素值类型和范围的多通道图像的直方图。

2. 直方图计算

2.1 灰度图像直方图计算

本节中,我们通过为单通道灰度图像的情况创建一个直方图类来使其更易于使用。对于其他类型的图像,可以直接使用 cv::calcHist() 函数,在下一小节中将解释它的每个参数。

(1) 首先,创建一个直方图类 Histogram1D

// 创建灰度图像直方图
class Histogram1D {
    private:
        int histSize[1];    // 直方图中 bin 的数量
        float hranges[2];   // 值的范围
        const float* ranges[1];     // 指向不同值范围的指针
        int channels[1];            // 通道数量
    public:
        Histogram1D() {
            // 默认参数
            histSize[0] = 256;      // 256 bins
            hranges[0] = 0.0;       // 从 0 开始
            hranges[1] = 256.0;     // 到 256 结束
            ranges[0] = hranges;
            channels[0] = 0;        // 使用通道 0
        }

(2) 使用定义的成员变量,可以使用以下方法完成灰度直方图的计算,该方法在 Histogram1D 类中实现:

// 计算 1D 直方图
cv:: Mat getHistogram(const cv::Mat& image) {
    cv::Mat hist;
    cv::calcHist(&image,
                1,              // 仅使用1张图像计算直方图
                channels,       // 所用通道
                cv::Mat(),      // 不使用掩码
                hist,           // 直方图
                1,              // 1D 直方图
                histSize,       // bins 的数量
                ranges);        // 像素范围
    return hist;
}

(3) 打开一个图像,创建一个 Histogram1D 实例,并调用 getHistogram 方法:

// 读取输入图像
cv::Mat image = cv::imread("1.png", 0);
Histogram1D h;
// 计算直方图
cv::Mat histo = h.getHistogram(image);

(4) 这里的 histo 对象是一个简单的一维数组,有 256bin;因此,可以通过简单地循环遍历此数组来读取每个 bin

for (int i=0; i<256; i++) {
    cout << "Value" << i << " = " << histo.at<float>(i) << endl;
}

执行以上程序,像素值的像素数输出如下:

...
Value117 = 13
Value118 = 14
Value119 = 16
Value120 = 21
...

显然很难从这个值序列中提取任何直观的含义。因此,将直方图进行可视化有利于直观观察图像像素值分布。

(5) 编写 getHistogramImage 方法来可视化直方图:

// 计算 1D 直方图并返回其图像
cv::Mat getHistogramImage(const cv::Mat& image, int zoom=1) {
    cv::Mat hist = getHistogram(image);
    return Histogram1D::getImageOfHistogram(hist, zoom);
}
// 不创建用于表示图像的直方图
static cv::Mat getImageOfHistogram(const cv::Mat& hist, int zoom) {
    double maxVal = 0;
    double minVal = 0;
    cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0);
    int histSize = hist.rows;
    // 显示直方图
    cv::Mat histImg(histSize*zoom, histSize*zoom, CV_8U, cv::Scalar(255));
    // 设定图像高度
    int hpt = static_cast<int>(0.9*histSize);
    // 绘制每个 bin
    for (int h=0; h<histSize; h++) {
        float binVal = hist.at<float>(h);
        if (binVal>0) {
            int intensity = static_cast<int>(binVal*hpt/maxVal);
            cv::line(histImg, cv::Point(h*zoom, histSize*zoom),
                    cv::Point(h*zoom, (histSize-intensity)*zoom),
                    cv::Scalar(0), zoom);
        }
    }
    return histImg;
}

(6) 使用 getImageOfHistogram 方法,可以以条形图的形式获取直方图的图像:

// 显示直方图
cv::namedWindow("Histogram");
cv::imshow("Histogram", h.getHistogramImage(image));

程序执行结果如下图所示:

直方图
从以上直方图可以看出,图像具有较大的中等灰度值和大量较暗的像素,这两组值分别对应图像的前景和背景,可以通过在这两组值之间的过渡处对图像进行阈值处理来验证(可以使用 OpenCV 函数 cv::threshold() 进行阈值处理)。

(7) 为了创建二值图像,我们将图像向直方图的峰值(灰度值 60 )增加之前的灰度值作为阈值:

// 创建二值图像
cv::Mat threshold;
cv::threshold(image, threshold, 70, 255, cv::THRESH_BINARY);

生成的二值图像清楚地显示了背景/前景分割结果:

二值图像
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)

大多数情况下,直方图是单个单通道或三通道图像。但是,cv::calcHist() 函数也可以指定分布在多个图像上的多通道图像,这就是为什么将图像数组输入到此函数中的原因。参数 dims 用于指定直方图的维数,例如,1 表示一维直方图,即使在分析多通道图像时,也不必在计算直方图时使用其所有通道,要计算的通道列在具有指定维度的通道数组中,单通道默认是通道 0;直方图本身由每个维度中的 bin 数(参数 histSize )以及每个维度中最小(包含)和最大(不包含)值(由两个元素组成的参数 ranges )描述。还可以定义非均匀直方图,在这种情况下,需要指定每个 bin 的限制。
与许多 OpenCV 函数一样,cv::calcHist() 函数也可以指定掩码,指示要包含在计数中的像素,忽略掩码值为 0 的像素。还可以指定两个额外的可选参数,它们都是布尔值,第一个指示直方图是否均匀(默认为均匀);第二个参数用于累积多个直方图计算的结果,如果此参数值为 true,则图像的像素数将添加到输入直方图的对应 bin 中。
结果直方图存储在 cv::Mat 实例中。事实上,cv::Mat 类可用于操作一般的 N 维矩阵,cv::Mat 类为矩阵定义了 at 方法,使我们能够在 getHistogramImage 方法中访问一维直方图的每个 bin

float binVal = hist.at<float>(h);

需要注意的是,直方图中的值存储为浮点值。本节中定义的 Histogram1D 类通过将其限制为一维直方图简化了 cv::calcHist 函数,这对灰度图像很有用,接下来,我们继续考虑彩色图像直方图。

2.2 彩色图像直方图计算

使用 cv::calcHist 函数,我们可以直接计算多通道图像的直方图。例如,计算彩色 BGR 图像直方图的类可以定义如下:

class ColorHistogram {
    private:
        int histSize[3];
        float hranges[2];
        const float* ranges[3];
        int channels[3];
    public:
        ColorHistogram() {
            // 默认参数
            histSize[0] = histSize[1] = histSize[2] = 256;
            hranges[0] = 0.0;
            hranges[1] = 256.0;
            ranges[0] = hranges;
            ranges[1] = hranges;
            ranges[2] = hranges;
            channels[0] = 0;
            channels[1] = 1;
            channels[2] = 2;
        }

在这种情况下,直方图将是三维的。因此,我们需要为每一维度指定一个范围。对于 BGR 图像,三个通道具有相同的范围 [0, 255]。定义好参数后,颜色直方图通过以下方法计算:

cv::Mat getHistogram(const cv::Mat &image) {
    // 计算直方图
    cv::Mat hist;
    hranges[0] = 0.0;
    hranges[1] = 256.0;
    channels[0] = 0;
    channels[1] = 1;
    channels[2] = 2;
    cv::calcHist(&image,
            1,              // 使用一张图片计算直方图
            channels,       // 使用的通道
            cv::Mat(),      // 不使用掩码
            hist,           // 结果
            3,              // 3D 直方图
            histSize,       // bins 数量
            ranges          // 像素值范围
    );
    return hist;
}

以上函数可以返回一个三维 cv::Mat 实例。 当选择具有 256 个条目的直方图时,此矩阵有 25 6 3 ≈ 16000000 256^3≈16000000 256316000000 个元素,表示 1600 多万个 bins,在应用中最好减少直方图计算中的 bin 数量。我们也可以使用 cv::SparseMat 数据结构,该数据结构旨在表示大型稀疏矩阵(即非零元素非常少的矩阵),而不会消耗太多内存。cv::calcHist 函数有一个可以返回稀疏矩阵的版本,为了返回 cv::SparseMatrix 需要修改 getHistogram 方法:

// 计算直方图
cv::SparseMat getSparseHistogram(const cv::Mat& image) {
    cv::SparseMat hist(3, histSize, CV_32F);
    hranges[0] = 0.0;
    hranges[1] = 256.0;
    channels[0] = 0;
    channels[1] = 1;
    channels[2] = 2;
    // 计算直方图
    cv:: calcHist(
        &image,
        1,
        channels,
        cv::Mat(),
        hist,
        3,
        histSize,
        ranges
    );
    return hist;
}

显然,也可以通过显示单独的 RGB 直方图来说明图像中的颜色分布。

3. 应用查找表修改图像

图像直方图使用可用的像素强度值捕捉场景的渲染方式。通过分析图像上像素值的分布,可以修改并改进图像。在本节中,将介绍如何使用由查找表表示的简单映射函数来修改图像的像素值,查找表通常是根据直方图分布定义的。

3.1 查找表

查找表是一个简单的一对一(或多对一)函数,它定义了如何将像素值转换为新值,查找表是一个一维数组。

(1) 表中第 i i i 项给出对应灰度的新强度值:

newIntensity= lookup[oldIntensity];

(2) OpenCV 中的 cv::LUT 函数将查找表应用于图像以生成新图像。我们可以将这个函数添加到 Histogram1D 类中:

// 应用查找表将输入图像转换为单通道图像
static cv::Mat applyLookUp(const cv::Mat& image, const cv::Mat& lookup) {
    cv::Mat result;
    cv::LUT(image, lookup, result);
    return result;
}

(3) 当查找表应用于图像时,它会产生一个新图像,其中像素强度值按照查找表的规则进行修改,例如,一个简单的转换如下:

// 创建图像反转表
int dim(256);
cv::Mat lut(1, &dim, CV_8U);
for (int i=0; i<256; i++) {
    lut.at<uchar>(i) = 255 - i;
}

这种变换只是简单地反转像素强度,即强度 0 变为 2551 变为 254,依此类推。在图像上应用该查找表可以得到原始图像的负片效果,结果如下所示:

查找表应用
查找表对于所有像素强度都被赋予新强度值的应用程序非常有用。但这种转换必须是全局性的,也就是说,具有相同强度值的所有像素都会经过相同的变换。

3.2 拉伸直方图提高图像对比度

可以通过修改原始图像直方图的查找表来提高图像的对比度。例如,上一节中图像的直方图并没有使用所有像素强度值(未使用较亮的强度值)。因此,可以通过拉伸直方图以生成具有较大对比度的图像。为此,我们使用百分比阈值来定义拉伸图像中黑白像素百分比,我们必须找到最低 (imin) 和最高 (imax) 强度值,以便我们得到低于或高于指定百分位数所需的最小像素数。然后可以重新映射强度值,以便将 imin 值重新定位为强度 0,并为 imax 值重映射为 255,图像中的中间的像素强度 i i i 进行简单地线性重新映射:

255.0*(i-imin)/(imax-imin);

计算完之后调用 applyLookUp 方法, 此外,在实践中,不仅可以忽略值为 0bin,还可以忽略计数小于给定值的 bin (此处定义为 minValue),调用方法如下:

cv::Mat streteched = h.stretch(image,0.01f);

得到的拉伸图像及其直方图如下:

直方图拉伸

3.3 在彩色图像上应用查找表

在像素操作一节中,我们定义了一个颜色减少函数,该函数修改图像的 BGR 值以减少可能的颜色数量,我们通过遍历图像的像素并对每个像素应用色彩还原函数来做到这一点。事实上,预先计算所有颜色减少后的新颜色值,然后使用查找表修改每个像素会更有效。新的减色函数可以修改如下:

void colorReduce(cv::Mat &image, int div=64) {
    // 创建 1D 查找表
    cv::Mat lookup(1, 256, CV_8U);
    // 定义减色查找表
    for (int i=0; i<256; i++) {
        lookup.at<uchar>(i) = i/div*div + div/2;
    }
    // 对所有通道应用查找表
    cv::LUT(image, lookup, image);
}

以上代码用于执行色彩减少函数,当将一维查找表应用于多通道图像时,相同的表将单独应用于所有通道;当查找表具有多个维度时,则必须将其应用于具有相同通道数的图像。

4. 图像直方图均衡化

在上一小节中,我们学习了如何通过拉伸直方图以扩展图像可用强度值的范围来提高图像的对比度,这是一种可以有效改善图像质量的简单方法。然而,在很多情况下,图像的视觉缺陷并不在于它使用的强度范围太小;反而是由于某些强度值的使用相较于其它强度值过于频繁。事实上,高质量的图像应该尽可能平衡利用所有可用的像素强度,这就是直方图均衡化概念的核心思想,即令图像直方图尽可能平坦。
OpenCV 提供了一个易于使用的函数来执行直方图均衡化:

cv::equalizeHist(image, result);

将其应用于图像后,可以得到以下结果:

直方图均衡化

当然,由于查找表是全局多对一转换,直方图不可能完全平坦,但是,可以看直方图的分布比原始图像更均匀。
在完全均匀的直方图中,所有 bin 都具有相同数量的像素。这意味着 50% 的像素值强度低于 12825% 的像素强度低于 64,依此类推。可以使用以下规则描述:在均匀直方图中,p% 的像素必须具有低于或等于 255*p% 的强度值。因此,所需的查找表可以使用以下方式构建:

lookup.at<uchar>(i)= static_cast<uchar>(255.0*p[i]/image.total());

其中,p[i] 是强度小于或等于 i 的像素数,p[i] 通常称为累积直方图;也就是说,它是一个包含低于或等于给定强度的像素计数的直方图,而不是包含具有特定强度值的像素计数。image.total() 返回图像中的像素数,因此可以使用 p[i]/image.total() 得到像素的百分比。
通常直方图均衡化可以极大地改善图像的质量,但根据视觉内容,结果的质量会因图像而异。

5. 完整代码

代码包含两部分,首先是头文件 histogram.h

#if !defined HISTOGRAM
#define HISTOGRAM

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>

// 创建灰度图像直方图
class Histogram1D {
    private:
        int histSize[1];    // 直方图中 bin 的数量
        float hranges[2];   // 值的范围
        const float* ranges[1];     // 指向不同值范围的指针
        int channels[1];            // 通道数量
    public:
        Histogram1D() {
            // 默认参数
            histSize[0] = 256;      // 256 bins
            hranges[0] = 0.0;       // 从 0 开始
            hranges[1] = 256.0;     // 到 256 结束
            ranges[0] = hranges;
            channels[0] = 0;        // 使用通道 0
        }
        // 设定需要计算的通道
        void setChannel(int c) {
            channels[0] = c;
        }
        // 获取所用通道
        int getChannel() {
            return channels[0];
        }
        // 获取最小像素值
        float getMinValue() {
            return hranges[0];
        }
        // 获取最大像素值
        float getMaxValue() {
            return hranges[1];
        }
        // 设定直方图中的 bin 数量
        void setNBins(int nbins) {
            histSize[0] = nbins;
        }
        // 获取直方图中的 bin 数量
        int getNBins() {
            return histSize[0];
        }
        // 计算 1D 直方图
        cv:: Mat getHistogram(const cv::Mat& image) {
            cv::Mat hist;
            cv::calcHist(&image,
                        1,              // 仅使用1张图像计算直方图
                        channels,       // 所用通道
                        cv::Mat(),      // 不使用掩码
                        hist,           // 直方图
                        1,              // 1D 直方图
                        histSize,       // bins 的数量
                        ranges);        // 像素范围
            return hist;
        }
        // 计算 1D 直方图并返回其图像
        cv::Mat getHistogramImage(const cv::Mat& image, int zoom=1) {
            cv::Mat hist = getHistogram(image);
            return Histogram1D::getImageOfHistogram(hist, zoom);
        }
        // 使用具有最小值的 bin 拉伸图像
        cv::Mat stretch(const cv::Mat& image, int minValue=0) {
            // 计算直方图
            cv::Mat hist = getHistogram(image);
            // 找到直方图的左端
            int imin = 0;
            for (; imin<histSize[0]; imin++) {
                // 忽略小于 minValue 的 bins
                if (hist.at<float>(imin) > minValue) break;
            }
            // 找到直方图的右端
            int imax = histSize[0] - 1;
            for (; imax>=0; imax--) {
                // 忽略小于 minValue 的 bins
                if (hist.at<float>(imax) > minValue) break;
            }
            // 创建查找表
            int dims[1] = {256};
            cv::Mat lookup(1, dims, CV_8U);
            for (int i=0; i<256; i++) {
                if (i<imin) lookup.at<uchar>(i) = 0;
                else if (i>imax) lookup.at<uchar>(i) = 255;
                else lookup.at<uchar>(i) = cvRound(255.0*(i-imin)/(imax-imin));
            }
            // 应用查找表
            cv::Mat result;
            result = applyLookUp(image, lookup);
            return result;
        }
        // 使用百分比拉伸图像
        cv::Mat stretch(const cv::Mat& image, float percentile) {
            float number = image.total()*percentile;
            cv::Mat hist = getHistogram(image);
            // 找到直方图的左端
            int imin = 0;
            for (float count=0.0; imin<256; imin++) {
                if ((count+=hist.at<float>(imin)) >= number) break;
            }
            // 找到直方图的右端
            int imax = 255;
            for (float count=0.0; imax>=0; imax--) {
                if ((count+=hist.at<float>(imax)) >= number) break;
            }
            // 创建查找表
            int dims[1] = {256};
            cv::Mat lookup(1, dims, CV_8U);
            for (int i=0; i<256; i++) {
                if (i<imin) lookup.at<uchar>(i) = 0;
                else if (i>imax) lookup.at<uchar>(i) = 255;
                else lookup.at<uchar>(i) = cvRound(255.0*(i-imin)/(imax-imin));
            }
            // 应用查找表
            cv::Mat result;
            result = applyLookUp(image, lookup);
            return result;
        }
        // 不创建用于表示图像的直方图
        static cv::Mat getImageOfHistogram(const cv::Mat& hist, int zoom) {
            double maxVal = 0;
            double minVal = 0;
            cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0);
            int histSize = hist.rows;
            // 显示直方图
            cv::Mat histImg(histSize*zoom, histSize*zoom, CV_8U, cv::Scalar(255));
            // 设定图像高度
            int hpt = static_cast<int>(0.9*histSize);
            // 绘制每个 bin
            for (int h=0; h<histSize; h++) {
                float binVal = hist.at<float>(h);
                if (binVal>0) {
                    int intensity = static_cast<int>(binVal*hpt/maxVal);
                    cv::line(histImg, cv::Point(h*zoom, histSize*zoom),
                            cv::Point(h*zoom, (histSize-intensity)*zoom),
                            cv::Scalar(0), zoom);
                }
            }
            return histImg;
        }
        // 归一化
        static cv::Mat equalize(const cv::Mat& image) {
            cv::Mat result;
            cv::equalizeHist(image, result);
            return result;
        }
        // 应用查找表将输入图像转换为单通道图像
        static cv::Mat applyLookUp(const cv::Mat& image, const cv::Mat& lookup) {
            cv::Mat result;
            cv::LUT(image, lookup, result);
            return result;
        }
        // 使用迭代器应用查找表将输入图像转换为单通道图像
        static cv::Mat applyLookUpWithIterator(const cv::Mat& image, const cv::Mat& lookup) {
            cv::Mat result(image.rows, image.cols, CV_8U);
            cv::Mat_<uchar>::iterator itr = result.begin<uchar>();
            cv::Mat_<uchar>::const_iterator it = image.begin<uchar>();
            cv::Mat_<uchar>::const_iterator itend = image.end<uchar>();
            // 对每一像素应用查找表
            for (; it!=itend; ++it, ++itr) {
                *itr = lookup.at<uchar>(*it);
            }
            return result;
        }
};

#endif

然后是主函数代码文件 hist.cpp

#include <iostream>
using namespace std;

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include "histogram.h"

int main() {
    // 读取输入图像
    cv::Mat image = cv::imread("1.png", 0);
    if (!image.data) return 0;
    cv::imwrite("1GS.png", image);
    cv::namedWindow("Image");
    cv::imshow("Image", image);
    Histogram1D h;
    // 计算直方图
    cv::Mat histo = h.getHistogram(image);
    for (int i=0; i<256; i++) {
        cout << "Value" << i << " = " << histo.at<float>(i) << endl;
    }
    // 显示直方图
    cv::namedWindow("Histogram");
    cv::imshow("Histogram", h.getHistogramImage(image));
    cv::Mat hi = h.getHistogramImage(image);
    cv::line(hi, cv::Point(70, 0), cv::Point(70, 255), cv::Scalar(128));
    cv::namedWindow("Histogram with threshold value");
    cv::imshow("Histogram with threshold value", hi);
    // 创建二值图像
    cv::Mat threshold;
    cv::threshold(image, threshold, 70, 255, cv::THRESH_BINARY);
    cv::namedWindow("Binary Image");
    cv::imshow("Binary Image", threshold);
    threshold = 255 - threshold;
    cv::imwrite("binary.png", threshold);
    // 直方图归一化
    cv::Mat eq = h.equalize(image);
    cv::namedWindow("Equalized Image");
    cv::imshow("Equalized Image", eq);
    cv::namedWindow("Equalized H");
    cv::imshow("Equalized H", h.getHistogramImage(eq));
    // 拉伸图像
    cv::Mat str = h.stretch(image, 0.01f);
    cv::namedWindow("Stretched Image");
    cv::imshow("Stretched Image", str);
    cv::namedWindow("Stretched H");
    cv::imshow("Stretched H", h.getHistogramImage(str));
    // 创建图像反转表
    cv::Mat lut(1, 256, CV_8U);
    // 或
    // int dim(256);
    // cv::Mat lut(1, &dim, CV_8U);
    for (int i=0; i<256; i++) {
        lut.at<uchar>(i) = 255 - i;
    }
    cv::namedWindow("Negative image");
    cv::imshow("Negative image", h.applyLookUp(image, lut));
    cv::waitKey();
    return 0;
}

小结

图像直方图是一种反映图像色调分布的直方图,绘制每个色调值的像素数。我们可以使用 cv2::calcHist() 函数来计算一个或多个数组的直方图,将其应用于单通道图像和多通道图像,利用查找表可以修改图像的视觉效果,使用 cv::equalizeHist() 函数可以对图像直方图执行均衡化,提高图像质量。

系列链接

OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换

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

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

相关文章

操作流程违规作业监测系统 yolov7

操作流程违规作业监测系统通过pythonyolov7网络深度学习技术&#xff0c;对高危场景下作业人员未按照操作流程进行正常操作行为进行实时分析识别检测&#xff0c;发现现场人员违规作业操作行为&#xff0c;不需人为干预&#xff0c;立即自动抓拍存档预警。YOLOv7 在 5 FPS 到 1…

在 VSCode 中像写 TypeScript 一样写 JavaScript

大家好&#xff0c;我是前端西瓜哥。 我们在 VSCode 编辑器中编写 js 代码&#xff0c;是会提供类型提示的。 VSCode 会推断一个变量是什么类型&#xff0c;并在你输入内容的时候&#xff0c;提供对应的 API 属性或方法补全。 如下图&#xff0c;在 js 文件中&#xff0c;ar…

【Java】到底什么是包?|最通俗易懂讲解|保姆级

博主简介&#xff1a;努力学习的预备程序媛一枚~博主主页&#xff1a; 是瑶瑶子啦所属专栏: Java岛冒险记【从小白到大佬之路】 目录Part1&#xff1a;类比理解&#xff1a;Part2&#xff1a;与包&#xff08;package)正式见面&#xff1a;2.1&#xff1a;包的本质--文件夹2.2&…

学习C++基本数值类型

写在前面 正在学习C/C/Javascript&#xff0c;面向初学者撰写专栏 博主原创C/C笔记&#xff08;干货&#xff09;&#xff0c;如有错误之处请各位读者指正 请读者评论回复、参与投票&#xff0c;反馈给作者&#xff0c;我会获得持续更新各类干货的动力。 致粉丝&#xff1a;可以…

力扣刷题记录——709. 转换成小写字母、771. 宝石与石头、704. 二分查找

本专栏主要记录力扣的刷题记录&#xff0c;备战蓝桥杯&#xff0c;供复盘和优化算法使用&#xff0c;也希望给大家带来帮助&#xff0c;博主是算法小白&#xff0c;希望各位大佬不要见笑&#xff0c;今天要分享的是——《力扣刷题记录——709. 转换成小写字母、771. 宝石与石头…

C++11并发指南二(stdthread详解)

C11并发指南二&#xff08;stdthread详解&#xff09; 文章目录C11并发指南二&#xff08;stdthread详解&#xff09;std::thread 构造move 赋值操作其他成员函数上一篇博客《 C11 并发指南一(C11 多线程初探)》中只是提到了 std::thread 的基本用法&#xff0c;并给出了一个最…

{(leetcode 题号:169. 多数元素)+(189. 轮转数组)}时间复杂度与空间复杂度分析:

目录 一. 基本概念 1.时间复杂度 2.空间复杂度 二.实例分析 实例(1):旋转数组 方法1:暴力旋转法(时间复杂度加空间复杂度分析) 方法2 :三步整体逆序法 (时间复杂度加空间复杂度分析) 实例(2):斐波那契递归的时间复杂度和空间复杂度分析 实例(3)&#xff1a;169. 多数元素…

模拟实现C库函数(1)

"啊~所有经历给它赋予魔力。"很久没更新过C专栏的文章了&#xff0c;借复习(review)的机会&#xff0c;本节的内容针对我们一些常见、常用的C库函数的模拟实现。“当你行走了一段时间后&#xff0c;回头往往那不管是起初咿咿呀呀胡乱踩陷的小坑时&#xff0c;还是之后…

C++11并发指南三(stdmutex详解)

C11并发指南三&#xff08;std:mutex详解&#xff09; 文章目录C11并发指南三&#xff08;std:mutex详解&#xff09;<mutex> 头文件介绍Mutex 系列类(四种)Lock 类&#xff08;两种&#xff09;其他类型函数std::mutex 介绍std::mutex 的成员函数std::recursive_mutex 介…

miracl

文章目录Windows平台编译网址 https://miracl.com/https://github.com/miracl/MIRACL Windows平台编译 源码目录下新建文件夹ms32或ms64&#xff0c;把/lib/ms32doit.bat或ms64doit.bat分别拷进去。 把源码include和source目录所有文件拷贝进要编译的ms32或ms64&#xff0c…

32. 实战:PyQuery实现抓取TX图文新闻

目录 前言 &#xff08;链接在评论区&#xff09;&#xff08;链接在评论区&#xff09;&#xff08;链接在评论区&#xff09; 目的 &#xff08;链接在评论区&#xff09;&#xff08;链接在评论区&#xff09;&#xff08;链接在评论区&…

ATAC-seq分析:Motifs分析(11)

1. 切割位点 ATACseq 应该在较小的保护区&#xff08;如转录因子结合位点&#xff09;周围生成较短的片段&#xff08;我们的无核小体区域&#xff09;。 因此&#xff0c;我们可以在不同组织/细胞类型/样本中寻找围绕感兴趣基序的切割位点堆积。 为了从我们的 BAM 文件中生成切…

FecMall多语言商城宝塔安装搭建教程

FecMall多语言商城宝塔安装搭建教程 1.1、删除禁用函数 PHP管理→禁用函数&#xff0c;删除putenv、pcntl_signal函数 如果不删除会报错&#xff1a;[ErrorException] pcntl_signal() has been disabled for security reasons 1.2下载fecmall 进入如下目录中cd /www/wwwroot 下…

行为型模式-中介模式

1.概述 一般来说&#xff0c;同事类之间的关系是比较复杂的&#xff0c;多个同事类之间互相关联时&#xff0c;他们之间的关系会呈现为复杂的网状结构&#xff0c;这是一种过度耦合的架构&#xff0c;即不利于类的复用&#xff0c;也不稳定。例如在下左图中&#xff0c;有六个…

LeetCode 2325. 解密消息

给你字符串 key 和 message &#xff0c;分别表示一个加密密钥和一段加密消息。解密 message 的步骤如下&#xff1a; 使用 key 中 26 个英文小写字母第一次出现的顺序作为替换表中的字母 顺序 。 将替换表与普通英文字母表对齐&#xff0c;形成对照表。 按照对照表 替换 mess…

〖产品思维训练白宝书 - 核心竞争力篇⑤〗- 产品经理核心竞争力解读之如何培养创造力

大家好&#xff0c;我是 哈士奇 &#xff0c;一位工作了十年的"技术混子"&#xff0c; 致力于为开发者赋能的UP主, 目前正在运营着 TFS_CLUB社区。 &#x1f4ac; 人生格言&#xff1a;优于别人,并不高贵,真正的高贵应该是优于过去的自己。&#x1f4ac; &#x1f4e…

【从零开始】力扣刷题(1)

文章目录前言数组&#xff1a;数组的遍历485.最大连续的一个数495.提莫攻击414.第三大的数628.三个数的最大乘积数组&#xff1a;统计数组中的元素645.错误的集合697.数组的度484.找到所有数组中消失的数组442.数组中重复的数据41.缺失的第一个正数274.H指数前言 我根据这里的…

Scalable SoftGroup for 3D Instance Segmentation on Point Clouds

Abstract 本文考虑了一个称为SoftGroup的网络&#xff0c;用于准确和可扩展的3D实例分割。现有的最先进方法会产生硬语义预测&#xff0c;然后进行分组以获得实例分割结果。然而&#xff0c;源于硬决策的错误会传播到分组中&#xff0c;导致预测实例与ground truth的低重叠和大…

数据结构:排序的基本概念

排序(sorting)是按关键字的非递减或非递增顺序对一组记录重新进行整队(或排列)的操作。确切描述如下: 假设含有 n 个记录的序列为 {r1 ,r2 , … ,rn} (3-1) 它们的关键字相应为 {k1 ,k2 , … ,kn} 对式(3-1)的记录序列进行排序就是要确定序号 1,2,,n 的一种排列 p1,p2 , … …

MyBatis 表连接查询写法|三种对应关系

❤️作者主页&#xff1a;微凉秋意 ✅作者简介&#xff1a;后端领域优质创作者&#x1f3c6;&#xff0c;CSDN内容合伙人&#x1f3c6;&#xff0c;阿里云专家博主&#x1f3c6; ✨精品专栏&#xff1a;C面向对象 &#x1f525;系列专栏&#xff1a;JavaWeb 文章目录前言表连接…