【opencv】示例-aruco_dict_utils.cpp 计算 ArUco 字典度量

4016747f97a8a4fca15b65323d64f7e6.png

该程序可用于计算 ArUco 字典度量。

要计算考虑翻转标记的指标,请使用 -'r' 标志。

该程序可用于创建和编写自定义 ArUco 词典。

12c62f192220aefee57f8be0b298a5cf.png

1ebaa80dcafb8732a4bbb5f6135590af.png

#include <opencv2/objdetect/aruco_detector.hpp> // 包含aruco marker检测相关功能的头文件
#include <iostream> // 包含输入输出流相关功能的头文件


using namespace cv; // 使用命名空间cv,这样我们就可以直接使用OpenCV的函数和类而不需要加cv::前缀
using namespace std; // 使用命名空间std,标准的C++库函数比如std::cout可以直接写成cout


// 下面是静态函数的定义,因为我们不需要实例化对象就可以直接用类名调用它们
//为了确保ArUco标记的独一无二,这个函数计算了一个标记的自身最小汉明距离(即标记的不同旋转形态之间的最小差异度量),这有助于保证即使在不同的旋转下,标记也能被准确地检测和识别。
static int _getSelfDistance(const Mat &marker) {
    // 计算单个marker自身的汉明距离(marker之间的差异程度)


    Mat bytes = aruco::Dictionary::getByteListFromBits(marker); // 将marker的位图转换为字节列表


    double minHamming = (double)marker.total() + 1; // 初始化最小汉明距离为marker的总数+1
    for(int r = 1; r < 4; r++) { // 对每个旋转的标记进行遍历(共有四种旋转,不包括未旋转)
        // 创建两个临时的字节行,用于存储转换后的字节信息
        cv::Mat tmp1(1, bytes.cols, CV_8UC1, Scalar::all(0));
        cv::Mat tmp2(1, bytes.cols, CV_8UC1, Scalar::all(0));
        uchar* rot0 = tmp1.ptr(); // 获取行tmp1的指针
        uchar* rot1 = tmp2.ptr(); // 获取行tmp2的指针


        for (int i = 0; i < bytes.cols; ++i) { // 将marker的字节旋转r*90度并拷贝到临时字节行中
            rot0[i] = bytes.ptr()[i];
            rot1[i] = bytes.ptr()[bytes.cols*r + i];
        }


        // 计算两个临时行的汉明距离
        double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING);
        // 更新最小汉明距离
        if (currentHamming < minHamming) minHamming = currentHamming;
    }
    
    // 对marker进行水平翻转和垂直翻转后的处理逻辑与上面类似,不再重复。
    // 在marker的所有可能的变换后,返回最小的汉明距离
    // 检查水平翻转后的情况
    Mat b; // 定义一个矩阵用于存储翻转的结果
    flip(marker, b, 0); // 对marker进行水平翻转
    Mat flipBytes = aruco::Dictionary::getByteListFromBits(b); // 获取翻转后marker的字节列表
    // 对翻转后的marker进行相似度检查,逻辑与上面类似
    for(int r = 0; r < 4; r++) {
        // ... 代码逻辑与上面类似 ...
        cv::Mat tmp1(1, flipBytes.cols, CV_8UC1, Scalar::all(0));
        cv::Mat tmp2(1, bytes.cols, CV_8UC1, Scalar::all(0));
        uchar* rot0 = tmp1.ptr();
        uchar* rot1 = tmp2.ptr();


        for (int i = 0; i < bytes.cols; ++i) {
            rot0[i] = flipBytes.ptr()[i];
            rot1[i] = bytes.ptr()[bytes.cols*r + i];
        }


        double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING);
        if(currentHamming < minHamming) minHamming = currentHamming;
    }
    // 检查垂直翻转后的情况
    flip(marker, b, 1); // 对marker进行垂直翻转
    flipBytes = aruco::Dictionary::getByteListFromBits(b); // 获取翻转后marker的字节列表
    // 对垂直翻转后的marker进行相似度检查,逻辑与上面类似
    for(int r = 0; r < 4; r++) {
        // ... 代码逻辑与上面类似 ...
        cv::Mat tmp1(1, flipBytes.cols, CV_8UC1, Scalar::all(0));
        cv::Mat tmp2(1, bytes.cols, CV_8UC1, Scalar::all(0));
        uchar* rot0 = tmp1.ptr();
        uchar* rot1 = tmp2.ptr();


        for (int i = 0; i < bytes.cols; ++i) {
            rot0[i] = flipBytes.ptr()[i];
            rot1[i] = bytes.ptr()[bytes.cols*r + i];
        }


        double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING);
        if(currentHamming < minHamming) minHamming = currentHamming;
    }
    // 返回最小汉明距离的四舍五入结果
    return cvRound(minHamming);
}
//计算给定字典中,某个ArUco标记与特定ID的其他标记之间在全部或部分旋转的情况下的最小汉明距离。这个函数可以用于评估字典中标记的独一无二性,以及是否可以准确识别翻转后的标记。
static inline int getFlipDistanceToId(const aruco::Dictionary& dict, InputArray bits, int id, bool allRotations = true) {
    // 根据给定的ID计算字典中一个标记与其余标记的汉明距离,包括考虑标记的翻转


    Mat bytesList = dict.bytesList; // 获取字典中所有标记的字节列表
    CV_Assert(id >= 0 && id < bytesList.rows); // 检查输入的ID是否有效


    unsigned int nRotations = 4; // 默认情况下,考虑所有4个旋转
    if(!allRotations) nRotations = 1; // 如果不考虑旋转,只需要计算未旋转的情况


    Mat candidateBytes = aruco::Dictionary::getByteListFromBits(bits.getMat()); // 获取候选标记的字节列表
    double currentMinDistance = int(bits.total() * bits.total()); // 初始化当前的最小汉明距离
    for(unsigned int r = 0; r < nRotations; r++) { // 遍历所有的旋转(可能包括未旋转)
        // 创建两个临时字节行
        cv::Mat tmp1(1, candidateBytes.cols, CV_8UC1, Scalar::all(0));
        cv::Mat tmp2(1, candidateBytes.cols, CV_8UC1, Scalar::all(0));
        uchar* rot0 = tmp1.ptr(); // 行tmp1的指针
        uchar* rot1 = tmp2.ptr(); // 行tmp2的指针


        for (int i = 0; i < candidateBytes.cols; ++i) { // 将字典中的标记旋转后,与候选标记的字节进行比较
            rot0[i] = bytesList.ptr(id)[r*candidateBytes.cols + i];
            rot1[i] = candidateBytes.ptr()[i];
        }


        // 计算当前的汉明距离
        double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING);
        // 更新最小汉明距离
        if(currentHamming < currentMinDistance) {
            currentMinDistance = currentHamming;
        }
    }
    // 对候选标记进行水平翻转和垂直翻转后的处理逻辑与上面类似,不再重复。
    // 返回分配给字典中特定ID的汉明距离
    Mat b; // 定义一个Mat对象b用于存储翻转后的图像
    flip(bits.getMat(), b, 0); // 将输入的图像bits沿着水平轴翻转并存入b
    candidateBytes = aruco::Dictionary::getByteListFromBits(b); // 将翻转后的图像b转换为字节列表
    
    for(unsigned int r = 0; r < nRotations; r++) { // 循环遍历每种旋转状态
        cv::Mat tmp1(1, candidateBytes.cols, CV_8UC1, Scalar::all(0)); // 创建一个用于储存旋转的字节的临时Mat对象tmp1
        cv::Mat tmp2(1, candidateBytes.cols, CV_8UC1, Scalar::all(0)); // 创建一个用于储存原始字节的临时Mat对象tmp2
        uchar* rot0 = tmp1.ptr(); // 获取tmp1的指针
        uchar* rot1 = tmp2.ptr(); // 获取tmp2的指针
    
        for (int i = 0; i < candidateBytes.cols; ++i) { // 循环遍历字节的每一列
            rot0[i] = bytesList.ptr(id)[r*candidateBytes.cols + i]; // 读取固定ID的翻转状态的字节
            rot1[i] = candidateBytes.ptr()[i]; // 从候选字节中读取对应列的字节
        }
    
        double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING); // 计算tmp1和tmp2之间的汉明距离
        if (currentHamming < currentMinDistance) { // 如果当前汉明距离小于当前记录的最小距离
            currentMinDistance = currentHamming; // 更新最小汉明距离
        }
    }
    
    flip(bits.getMat(), b, 1); // 将输入的图像bits沿着垂直轴翻转并存入b
    candidateBytes = aruco::Dictionary::getByteListFromBits(b); // 将翻转后的图像b转换为字节列表
    
    for(unsigned int r = 0; r < nRotations; r++) { // 循环遍历每种旋转状态,逻辑与上述相同
        // 对翻转的图像执行与上面相同的操作,检查汉明距离,并更新最小值
        // ... (代码逻辑与上面相同,未显示) ...
        cv::Mat tmp1(1, candidateBytes.cols, CV_8UC1, Scalar::all(0));
        cv::Mat tmp2(1, candidateBytes.cols, CV_8UC1, Scalar::all(0));
        uchar* rot0 = tmp1.ptr();
        uchar* rot1 = tmp2.ptr();


        for (int i = 0; i < candidateBytes.cols; ++i) {
            rot0[i] = bytesList.ptr(id)[r*candidateBytes.cols + i];
            rot1[i] = candidateBytes.ptr()[i];
        }


        double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING);
        if (currentHamming < currentMinDistance) {
            currentMinDistance = currentHamming;
        }
    }
    
    return cvRound(currentMinDistance);
}


// 以下函数用于生成定制的非对称ArUco字典
static inline aruco::Dictionary generateCustomAsymmetricDictionary(int nMarkers, int markerSize,
                                                                        const aruco::Dictionary &baseDictionary,
                                                                        int randomSeed) { 
    // 定义一个静态内联函数,用于生成定制的非对称的ArUco标记字典
    RNG rng((uint64)(randomSeed)); // 基于随机种子初始化一个随机数生成器


    aruco::Dictionary out; // 创建一个空的ArUco字典用于输出
    out.markerSize = markerSize; // 设置输出字典中标记的大小


    // 理论上最大的标记间汉明距离
    // 论文参考:S. Garrido-Jurado, et al., 2014. "Automatic generation and detection of highly reliable fiducial markers under occlusion".
    int C = (int)std::floor(float(markerSize * markerSize) / 4.f); // 计算理论最大标记间距
    int tau = 2 * (int)std::floor(float(C) * 4.f / 3.f); // 计算arity的临界值


    // 如果提供了基础字典,计算它的标记间距
    if(baseDictionary.bytesList.rows > 0) {
        CV_Assert(baseDictionary.markerSize == markerSize); // 确认基础字典的尺寸匹配
        out.bytesList = baseDictionary.bytesList.clone(); // 克隆基础字典的字节列表


        int minDistance = markerSize * markerSize + 1; // 初始化最小距离
        for(int i = 0; i < out.bytesList.rows; i++) { // 遍历基础字典所有标记
            Mat markerBytes = out.bytesList.rowRange(i, i + 1); // 获取当前标记的字节行
            Mat markerBits = aruco::Dictionary::getBitsFromByteList(markerBytes, markerSize); // 将当前标记的字节转换成位矩阵
            minDistance = min(minDistance, _getSelfDistance(markerBits)); // 更新最短汉明距离
            for(int j = i + 1; j < out.bytesList.rows; j++) { // 计算当前标记与其他标记的距离
                minDistance = min(minDistance, getFlipDistanceToId(out, markerBits, j)); // 更新最短汉明距离
            }
        }
        tau = minDistance; // 更新临界值tau
    }


    // 当前最佳的选项
    int bestTau = 0; // 初始化最佳tau
    Mat bestMarker; // 存储当前最佳标记


    // 经过指定次数未产生结果的迭代后,接受最佳选项
    const int maxUnproductiveIterations = 5000; // 最大无产出迭代次数
    int unproductiveIterations = 0; // 无产出迭代计数


    while(out.bytesList.rows < nMarkers) { // 当生成的标记数量还未满足要求时
        Mat currentMarker(markerSize, markerSize, CV_8UC1, Scalar::all(0)); // 创建一个新的空标记
        rng.fill(currentMarker, RNG::UNIFORM, 0, 2); // 使用随机数填充当前标记


        int selfDistance = _getSelfDistance(currentMarker); // 计算当前标记的自汉明距离
        int minDistance = selfDistance; // 将其设置为最小距离参考值


        // 如果自汉明距离大于或等于当前最佳,计算与之前接受的标记之间的距离
        if(selfDistance >= bestTau) {
            for(int i = 0; i < out.bytesList.rows; i++) { // 遍历之前接受的所有标记
                int currentDistance = getFlipDistanceToId(out, currentMarker, i); // 计算距离
                minDistance = min(currentDistance, minDistance); // 更新最小距离
                // 如果最小距离小于或等于最佳tau,则跳出循环
                if(minDistance <= bestTau) {
                    break;
                }
            }
        }


        // 如果距离足够大,接受当前标记
        // 如果距离足够大,则接受当前标记
        if(minDistance >= tau) { // 如果最小汉明距离大于等于预定的阈值tau
            unproductiveIterations = 0; // 重置无产出迭代计数器
            bestTau = 0; // 重置最好的tau值
            Mat bytes = aruco::Dictionary::getByteListFromBits(currentMarker); // 获取当前marker的字节列表
            out.bytesList.push_back(bytes); // 将当前marker添加到输出字典的字节列表中
        } else {
            // 如果距离不够大,则进入下面的流程
            unproductiveIterations++; // 无产出迭代计数器加一
        
            // 如果距离尚不够大,但比当前最佳选择要好
            if(minDistance > bestTau) { // 如果最小汉明距离大于当前最佳的tau值
                bestTau = minDistance; // 更新最好的tau值为当前的最小距离
                bestMarker = currentMarker; // 更新最好的marker为当前的marker
            }
        
            // 如果达到了无产出迭代的设定极限,接受当前最佳选择
            if(unproductiveIterations == maxUnproductiveIterations) { // 如果无产出迭代计数等于最大无产出迭代次数
                unproductiveIterations = 0; // 重置无产出迭代计数器
                tau = bestTau; // 设置tau为当前最好的tau值
                bestTau = 0; // 重置最好的tau值
                Mat bytes = aruco::Dictionary::getByteListFromBits(bestMarker); // 获取当前最佳marker的字节列表
                out.bytesList.push_back(bytes); // 将最佳marker添加到输出字典的字节列表中
            }
        }
    }


    // 更新生成字典的最大误差修正位数
    out.maxCorrectionBits = (tau - 1) / 2; // 计算并设置最大纠错位数


    return out; // 返回生成的自定义字典
}


// 以下函数用于获取字典中的最小汉明距离
static inline int getMinDistForDict(const aruco::Dictionary& dict) {
    // 定义一个静态内联函数,用于计算ArUco字典中所有标记的最小汉明距离
    const int dict_size = dict.bytesList.rows; // 获取字典中标记的数量
    const int marker_size = dict.markerSize; // 获取字典中标记的尺寸
    int minDist = marker_size * marker_size; // 初始化最小距离为标记尺寸的平方


    // 双重循环遍历每一对标记
    for (int i = 0; i < dict_size; i++) {
        Mat row = dict.bytesList.row(i); // 获取第i个标记的字节行
        Mat marker = dict.getBitsFromByteList(row, marker_size); // 将字节行转换为二进制位矩阵
        for (int j = 0; j < dict_size; j++) {
            // 确保不与自身比较
            if (j != i) {
                // 获取第i个标记与第j个标记的汉明距离,并更新最小距离
                minDist = min(dict.getDistanceToId(marker, j), minDist);
            }
        }
    }
    return minDist; // 返回最小距离
}
// 以下函数用于获取字典在考虑翻转marker的情况下的最小汉明距离
static inline int getMinAsymDistForDict(const aruco::Dictionary& dict) {
    // 定义一个静态内联函数,用于计算考虑翻转的情况下ArUco字典中所有标记的最小汉明距离
    const int dict_size = dict.bytesList.rows; // 获取字典中标记的数量
    const int marker_size = dict.markerSize; // 获取字典中标记的尺寸
    int minDist = marker_size * marker_size; // 初始化最小距离为标记尺寸的平方


    // 双重循环遍历每一对标记
    for (int i = 0; i < dict_size; i++)
    {
        Mat row = dict.bytesList.row(i); // 获取第i个标记的字节行
        Mat marker = dict.getBitsFromByteList(row, marker_size); // 将字节行转换为二进制位矩阵
        for (int j = 0; j < dict_size; j++)
        {
            if (j != i) // 确保不与自身比较
            {
                // 获取考虑翻转的情况下第i个标记与第j个标记的汉明距离,并更新最小距离
                minDist = min(getFlipDistanceToId(dict, marker, j), minDist);
            }
        }
    }
    return minDist; // 返回考虑翻转的最小距离
}


// 命令行参数定义字符串
const char* keys  =
        "{@outfile |<none> | Output file with custom dict }" // 输出文件参数,输出自定义字典到该文件
        "{r | false | Calculate the metric considering flipped markers }" // 计算考虑翻转标记的度量标准
        "{d | | Dictionary Name: ...}" // 字典名称参数
        "{nMarkers | | Number of markers in the dictionary }" // 字典中标记的数量参数
        "{markerSize | | Marker size }" // 标记大小参数
        "{cd | | Input file with custom dictionary }"; // 自定义字典的输入文件参数


// 程序简介
const char* about =
        "This program can be used to calculate the ArUco dictionary metric.\n"
        "To calculate the metric considering flipped markers use -'r' flag.\n"
        "This program can be used to create and write the custom ArUco dictionary.\n";


int main(int argc, char *argv[])
{
    CommandLineParser parser(argc, argv, keys); // 初始化命令行解析器
    parser.about(about); // 设置关于程序的信息
    if(argc < 2) {
        parser.printMessage(); // 如果参数数量不够,打印帮助信息
        return 0;
    }


    // 从解析器中取得命令行参数
    string outputFile = parser.get<String>(0); // 获取输出文件名
    int nMarkers = parser.get<int>("nMarkers"); // 获取标记数量
    int markerSize = parser.get<int>("markerSize"); // 获取标记大小
    bool checkFlippedMarkers = parser.get<bool>("r"); // 获取是否检查翻转标记的标志


    // 创建一个默认的ArUco字典
    aruco::Dictionary dictionary = aruco::getPredefinedDictionary(0);


    if (parser.has("d")) {
        // 如果提供了字典名称参数,获取相应的预定义字典
        string arucoDictName = parser.get<string>("d"); // 获取字典名称


        cv::aruco::PredefinedDictionaryType arucoDict; // 定义预定义字典类型变量
        // 根据提供的字典名称设置预定义字典类型变量
        if (arucoDictName == "DICT_4X4_50") { arucoDict = cv::aruco::DICT_4X4_50; } // 如果字典名称是"DICT_4X4_50"
        else if (arucoDictName == "DICT_4X4_100") { arucoDict = cv::aruco::DICT_4X4_100; } // 如果字典名称是"DICT_4X4_100"
        else if (arucoDictName == "DICT_4X4_250") { arucoDict = cv::aruco::DICT_4X4_250; } // 如果字典名称是"DICT_4X4_250"
        else if (arucoDictName == "DICT_4X4_1000") { arucoDict = cv::aruco::DICT_4X4_1000; } // 如果字典名称是"DICT_4X4_1000"
        else if (arucoDictName == "DICT_5X5_50") { arucoDict = cv::aruco::DICT_5X5_50; } // 如果字典名称是"DICT_5X5_50"
        else if (arucoDictName == "DICT_5X5_100") { arucoDict = cv::aruco::DICT_5X5_100; } // 如果字典名称是"DICT_5X5_100"
        else if (arucoDictName == "DICT_5X5_250") { arucoDict = cv::aruco::DICT_5X5_250; } // 如果字典名称是"DICT_5X5_250"
        else if (arucoDictName == "DICT_5X5_1000") { arucoDict = cv::aruco::DICT_5X5_1000; } // 如果字典名称是"DICT_5X5_1000"
        else if (arucoDictName == "DICT_6X6_50") { arucoDict = cv::aruco::DICT_6X6_50; } // 如果字典名称是"DICT_6X6_50"
        else if (arucoDictName == "DICT_6X6_100") { arucoDict = cv::aruco::DICT_6X6_100; } // 如果字典名称是"DICT_6X6_100"
        else if (arucoDictName == "DICT_6X6_250") { arucoDict = cv::aruco::DICT_6X6_250; } // 如果字典名称是"DICT_6X6_250"
        else if (arucoDictName == "DICT_6X6_1000") { arucoDict = cv::aruco::DICT_6X6_1000; } // 如果字典名称是"DICT_6X6_1000"
        else if (arucoDictName == "DICT_7X7_50") { arucoDict = cv::aruco::DICT_7X7_50; } // 如果字典名称是"DICT_7X7_50"
        else if (arucoDictName == "DICT_7X7_100") { arucoDict = cv::aruco::DICT_7X7_100; } // 如果字典名称是"DICT_7X7_100"
        else if (arucoDictName == "DICT_7X7_250") { arucoDict = cv::aruco::DICT_7X7_250; } // 如果字典名称是"DICT_7X7_250"
        else if (arucoDictName == "DICT_7X7_1000") { arucoDict = cv::aruco::DICT_7X7_1000; } // 如果字典名称是"DICT_7X7_1000"
        else if (arucoDictName == "DICT_ARUCO_ORIGINAL") { arucoDict = cv::aruco::DICT_ARUCO_ORIGINAL; } // 如果字典名称是"DICT_ARUCO_ORIGINAL"
        else if (arucoDictName == "DICT_APRILTAG_16h5") { arucoDict = cv::aruco::DICT_APRILTAG_16h5; } // 如果字典名称是"DICT_APRILTAG_16h5"
        else if (arucoDictName == "DICT_APRILTAG_25h9") { arucoDict = cv::aruco::DICT_APRILTAG_25h9; } // 如果字典名称是"DICT_APRILTAG_25h9"
        else if (arucoDictName == "DICT_APRILTAG_36h10") { arucoDict = cv::aruco::DICT_APRILTAG_36h10; } // 如果字典名称是"DICT_APRILTAG_36h10"
        else if (arucoDictName == "DICT_APRILTAG_36h11") { arucoDict = cv::aruco::DICT_APRILTAG_36h11; } // 如果字典名称是"DICT_APRILTAG_36h11"
        else {
            cout << "incorrect name of aruco dictionary \n"; // 如果没有匹配的字典名称,打印错误信息
            return 1; // 返回非零值表示错误
        }


        dictionary = aruco::getPredefinedDictionary(arucoDict); // 从预定义字典类型获取预定义字典
    }
    else if (parser.has("cd")) {
        // 如果提供了自定义字典文件参数,读取自定义字典
        FileStorage fs(parser.get<std::string>("cd"), FileStorage::READ);
        bool readOk = dictionary.readDictionary(fs.root());
        if(!readOk) {
            cerr << "Invalid dictionary file" << endl; // 如果读取失败,打印错误信息
            return 0;
        }
    }
    else if (outputFile.empty() || nMarkers == 0 || markerSize == 0) {
        cerr << "Dictionary not specified" << endl; // 如果必要的参数没有提供,打印错误信息
        return 0;
    }
    if (!outputFile.empty() && nMarkers > 0 && markerSize > 0)
    {
        // 如果提供了所需要的参数,则生成自定义字典并写入文件
        FileStorage fs(outputFile, FileStorage::WRITE);
        if (checkFlippedMarkers)
            dictionary = generateCustomAsymmetricDictionary(nMarkers, markerSize, aruco::Dictionary(), 0); // 生成考虑翻转的自定义字典
        else
            dictionary = aruco::extendDictionary(nMarkers, markerSize, aruco::Dictionary(), 0); // 扩展字典
        dictionary.writeDictionary(fs); // 写入字典到文件
    }


    // 计算并打印字典的最小汉明距离
    if (checkFlippedMarkers) {
        cout << "Hamming distance: " << getMinAsymDistForDict(dictionary) << endl; // 考虑翻转的最小汉明距离
    }
    else {
        cout << "Hamming distance: " << getMinDistForDict(dictionary) << endl; // 不考虑翻转的最小汉明距离
    }


    return 0; // 程序结束
}

a95473030047b144a27dd2c7dc85f85a.png

PS V:\learn\opencv\github\opencv\sources\test\x64\Debug> ./test.exe  @outfile=custom_dict.yml -nMarkers=50 -markerSize=6
Hamming distance: 12

c18005d654991924cef7f3497dcf74f5.png

@outfile=custom_dict.yml

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

供应链领域主题:生产制造关键术语和系统

BOM&#xff08;Bill of Material&#xff09;物料清单 BOM&#xff08;Bill of Material&#xff09;物料清单&#xff0c;是计算机可以识别的产品结构数据文件&#xff0c;也是ERP的主导文件。BOM使系统识别产品结构&#xff0c;也是联系与沟通企业各项业务的纽带。ERP系统中…

(源码)基于Spring Boot和Vue植物养殖技巧学习系统的设计与实现

前言 &#x1f497;博主介绍&#xff1a;✌专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅&#x1f447;&#x1f3fb; 2024年Java精品实战案例《100套》 &#x1f345;文末获取源码联系&#x1f345; &#x1f31f…

华为汽车的“计算+通信”电子电气架构

文章目录 整车结构 硬件平台 软件平台 总结展望 整车EEA&#xff08;电子电气架构&#xff09;&#xff0c;按照博世提出的演进路径&#xff0c;大致可以划分为四个阶段&#xff1a;分布式模块阶段、区域控制阶段、中央计算阶段、云计算阶段。示例如下&#xff1a; 本文选取…

MyBatis-Plus的学习笔记

MyBatis-Plus 一、MyBatis-Plus快速入门 1.1 简介 课程版本&#xff1a;3.5.3.1 https://baomidou.com/ MyBatis-Plus (opens new window)&#xff08;简称 MP&#xff09;是一个 MyBatis (opens new window) 的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&…

序列超图的下一项推荐 笔记

1 Title Next-item Recommendation with Sequential Hypergraphs&#xff08;Jianling Wang、Kaize Ding、Liangjie Hong、Huan Liu、James Caverlee&#xff09;【SIGIR 2020】 2 Conclusion This study explores the dynamic meaning of items in realworld scenarios and p…

微信小程序的页面交互2

一、自定义属性 &#xff08;1&#xff09;定义&#xff1a; 微信小程序中的自定义属性实际上是由data-前缀加上一个自定义属性名组成。 &#xff08;2&#xff09;如何获取自定义属性的值&#xff1f; 用到target或currentTarget对象的dataset属性可以获取数据 &#xff…

【LeetCode题解】2192. 有向无环图中一个节点的所有祖先+1026. 节点与其祖先之间的最大差值

文章目录 [2192. 有向无环图中一个节点的所有祖先](https://leetcode.cn/problems/all-ancestors-of-a-node-in-a-directed-acyclic-graph/)思路&#xff1a;BFS记忆化搜索代码&#xff1a; 思路&#xff1a;逆向DFS代码&#xff1a; [1026. 节点与其祖先之间的最大差值](https…

【JavaWeb】Day32.SpringBootWeb请求响应——分层解耦(二)

3.IOC&DI 3.1 IOC&DI入门 完成Controller层、Service层、Dao层的代码解耦 思路&#xff1a; 1. 删除Controller层、Service层中new对象的代码 2. Service层及Dao层的实现类&#xff0c;交给IOC容器管理 3. 为Controller及Service注入运行时依赖的对象 Controller程序…

经典机器学习模型(九)EM算法在高斯混合模型中的应用

经典机器学习模型(九)EM算法在高斯混合模型中的应用 EM算法的推导可以参考&#xff1a; 经典机器学习模型(九)EM算法的推导 若随机变量X服从一个数学期望为 μ μ μ、方差为 σ 2 σ^2 σ2的正态分布&#xff0c;可以记为 N ( μ &#xff0c; σ 2 ) N(μ&#xff0c;σ2)…

cmake学习笔记1

基础概念 CMake是什么&#xff1f; CMake是一个元构建系统(meta build-system),用于生产其他构建系统文件&#xff08;如Makefile或Ninja&#xff09;。 基础操作方式 CMake使用一个CMakeLists.txt文件描述配置&#xff0c;然后使用cmake驱动这个文件生成对应构建系统文件。…

【数据结构】ArrayList详解

目录 前言 1. 线性表 2. 顺序表 3. ArrayList的介绍和使用 3.1 语法格式 3.2 添加元素 3.3 删除元素 3.4 截取部分arrayList 3.5 其他方法 4. ArrayList的遍历 5.ArrayList的扩容机制 6. ArrayList的优缺点 结语 前言 在集合框架中&#xff0c;ArrayList就是一个…

【Linux】环境基础开发工具使用——vim使用

Linux 软件包管理器 yum 什么是软件包 1.在 Linux 下安装软件 , 一个通常的办法是下载到程序的源代码 , 并进行编译 , 得到可执行程序 . 2.但是这样太麻烦了 , 于是有些人把一些常用的软件提前编译好 , 做成软件包 ( 可以理解成 windows 上的安装程序) 放在一个服务器…

C#,简单,精巧,实用的文件夹时间整理工具FolderTime

点击下载本文软件&#xff08;5积分&#xff09;&#xff1a; https://download.csdn.net/download/beijinghorn/89071073https://download.csdn.net/download/beijinghorn/89071073 百度网盘&#xff08;不需积分&#xff09;&#xff1a; https://pan.baidu.com/s/1FwCsSz…

ThreadLocal上传下载文件

文章目录 ThreadLocal1.基本介绍1.什么是ThreadLocal&#xff1f;2.示意图 2.快速入门1.创建普通java项目2.编写代码1.T1.java2.T1Service.java3.T2Dao.java4.Dog.java 3.结果 3.ThreadLocal源码解读1.set方法2.set方法总结3.get方法 上传下载文件1.基本介绍1.基本说明2.文件上…

Spring Cloud介绍

一、SpringCloud总体概述 Cloud Foundry Service Broker&#xff1a;通用service集成进入Cloud Foundry Cluster&#xff1a;服务集群 Consul&#xff1a;注册中心 Security&#xff1a;安全认证 Stream&#xff1a;消息队列 Stream App Starters&#xff1a;Spring Cloud Stre…

Redis 客户端

Redis 客户端 客户端-服务器结构 Redis 同 Mysql 一样&#xff0c;也是一个客户端-服务器结构的程序&#xff0c;结构如下图&#xff1a; 注&#xff1a;Redis 客户端和服务器可以在同一个主机上&#xff0c;也可以在不同主机上 Redis 客户端的多种形态 自带的命令行客户端&…

【Qt 学习笔记】详解Qt中的信号和槽

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ 详解Qt中的信号与槽 文章编号&#xff1a;Qt 学习笔记 / 12 文章目录…

【Node.js】短链接

原文链接&#xff1a;Nodejs 第六十二章&#xff08;短链接&#xff09; - 掘金 (juejin.cn) 短链接是一种缩短长网址的方法&#xff0c;将原始的长网址转换为更短的形式。短链接的主要用途之一是在社交媒体平台进行链接分享。由于这些平台对字符数量有限制&#xff0c;长网址可…

旋转花键有哪些优缺点?

旋转花键是在花键外筒的外径上装上专用的轴承外套&#xff0c;使之运转动作&#xff0c;适用于水平多关节机械手臂&#xff08;SCARA&#xff09;、产业用机器人、自动装载机、镭射加工机、搬送装置、机械加工中心的ATC装置等各项设备。 目前&#xff0c;旋转花键的应用越来越普…

redis 哨兵

文章目录 前言主从复制的问题怎么人工恢复故障主节点 Redis Setinel 架构使用 docker 来配置哨兵结构安装 docker编排 redis 主从节点编排 redis 哨兵节点 观察哨兵模式的作用主从切换的具体流程小结 前言 redis 主从复制模式下, 一旦主节点出现故障, 不能提供服务的时候, 就需…