基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

分享本科毕业设计的项目,完整代码在文章底部

一个基于颜色特征和边缘检测(采用Canny边缘检测算子)的图像检索系统。以C++作为开发语言,Qt5.12作为主要开发框架进行界面设计,OpenCV4.4作为开发工具库,使用MySQL数据库设计和实现系统的各项功能。

程序运行之前应确保成功安装MinGW x64版本的Qt(5.12及以上)及Qt Creator(Community),在Qt中使用CMake配置OpenCV(4.4及以上),安装并调试MySQL数据库。

在Qt Creator中构建并运行项目,验证数据库成功连接后可上传图像进行测试。

基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

颜色特征是图像检索领域中使用最为广泛的特征。大部分图像都含有丰富的颜色信息,即使是灰度图像也有着丰富的灰度等级分布。使用颜色特征在判断差异性和相似性时,不仅方便、效果好,执行速度也比较快。边缘特征是介于纹理特征和形状特征之间的一类图像特征,对于图像内容的区域性表达也很有代表性。同颜色特征一样,边缘特征的提取也比较方便,已经有包括Roberts算子、Sobel算子、Prewitt算子、LOG算子、Canny算子等在内的多种成熟的提取方法。

因颜色特征和边缘特征的优势所在,本文的图像检索方法设计拟将这两种特征作为融合处理的基本特征。其中,边缘特征采用Canny边缘检测算子进行处理,这是因为相比传统的微分算子,Canny算子是当前最优化且最受推崇的一种微分算子。而OpenCV也提供了一个相当便捷的边缘接口Canny函数(Canny()),对图像使用高斯滤波函数(GaussianBlur())后,再通过Canny函数转换即可得到相应的边缘检测之后的图像。

基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

具体的操作步骤是:

(1)调用函数cvtColor(image1, image2,CV_BGR2HSV)将原始待检索图像与数据库中的图像从RGB空间转换到HSV空间。

(2)调用函数calcHist(&image, 1, channels, Mat(), hist_base, 2, histsize, histRanges, true, false)计算直方图,其中image为传入图像。

(3)调用函数 normalize(hist_base, hist_base, 0, 1, NORM_MINMAX, -1, Mat())进行归一化处理。

(4)调用函数compareHist(hist_base, hist_base, CV_COMP_BHATTACHARYYA)进行相关性比较,并返回结果值。

(5)将上一步所返回的结果值存入数据库,与文件路径相对应,方便查询和调取。

(6)因经相关性比较计算所得值越小,说明图像之间差异性越小,即两图越相似。所以采取升序查询的方式将颜色特征相似值最低的前八张图显示在用户界面上。

代码:

if(ui->radioButton_color->isChecked()){

        std::vector  filelist_color;
        std::vector  finallist_color;
        QString qstr_1;
        QString qstr_2;
        QSqlQuery query_color_pre;
        QSqlQuery query_color;
        QSqlQuery query_color_final;
        std::vector  convertlist;
        std::vector  HSVlist;
//        std::vector  color_list;

//        std::vector  Simlist;

        query_color_pre.exec( "select * from image_address" );
        if(!query_color_pre.exec()){
            QMessageBox::information(this,"Warning",query_color_pre.lastError().text());
        }else{

        QMessageBox::information(this,"Method","Color-Based");
        while(query_color_pre.next())
                {
                     qstr_1 = query_color_pre.value(1).toString();
                     filelist_color.push_back(qstr_1.toStdString());
                }

        for (auto &i : filelist_color) {
            cout<

基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

具体的操作步骤是:

(1)进行边缘检测前需先将图像进行灰度化处理。

调用函数cvtColor(image1, image2,COLOR_BGR2GRAY)将原始待检索图像与数据库中的图像进行颜色空间转换,输出的image2即为完成灰度化转换的图像。

(2)调用函数GaussianBlur(base_gray, base_blurred, cv::Size(3, 3), 3);将灰度图进行高斯滤波器平滑处理,目的是尽可能去除噪声。其中内核的大小Size(w, h)必须是正奇数对。

(3)调用函数Canny(base_blurred, base_edge, Min_Threshold, Max_Threshold)进行基于Canny算子的边缘检测,其中Min_Threshold为最低阈值Max_Threshold为最高阈值。低于最低阈值的像素点将不会被认为是边缘;高于最高阈值的像素点则会被认为是边缘;介于最低阈值和最高阈值之间的像素点需要进行进一步判断,当其与所得到的边缘像素点相邻才被认为是边缘。本文所设计的系统将这两个阈值设置成了可输入调节的模式,通过参数传递用户可以自定义键入最低阈值和最高阈值来达到所需的边缘检测效果。如当待检索图像纹理细节较为丰富时,用户可适当调高阈值,当待检索图像构成线条较为简明时,用户可适当降低阈值以获得理想的检索效果。

(4)调用sift->detect(image, keypoints)检测特征点。调用sift->compute(image, keypoints, descriptors1)计算特征描述符。

(5)调用函数cv::Ptr

(cv::DescriptorMatcher::BRUTEFORCE)进行特征匹配。

(6)t1 = cv::getTickCount();

matcher->match(descriptors1, descriptors2, matches);

t2 = cv::getTickCount();

tmatch_bf = 1000.0*(t2-t1) / cv::getTickFrequency();

进行暴力匹配,其中tmatch_bf返回汉明距离作为相似性度量。

(7)由前一步骤得到的汉明距离越小,说明图像之间差异性越小,即两图越相似。所以采取升序查询的方式将边缘特征相似值最低的前八张图显示在用户界面上。

代码:

    if(ui->radioButton_edge->isChecked()){

        std::vector  filelist_edge;
        std::vector  finallist_edge;
        QString qstr_1;
        QString qstr_2;
        QSqlQuery query_edge_pre;
        QSqlQuery query_edge;
        QSqlQuery query_edge_final;
        std::vector  convertlist;
        std::vector  Cannylist;
        std::vector  Edgelist;

        double Simlist[filelist_edge.size()];

        int64 t1, t2;
        int k=0;
//        double tkpt, tdes;
        double tmatch_bf;

        query_edge_pre.exec( "select * from image_address" );
        if(!query_edge_pre.exec()){
            QMessageBox::information(this,"Warning",query_edge_pre.lastError().text());
        }else{

            lowerThreshold = 80;
            max_lowThreshold = 150;

            ui->lineEdit_min->setValidator(new QIntValidator(ui->lineEdit_min));
            ui->lineEdit_max->setValidator(new QIntValidator(ui->lineEdit_max));

            QString min = ui->lineEdit_min->text();

            QString max = ui->lineEdit_max->text();

            cout< max.toInt() ||  max.toInt()  250)
            {
                QMessageBox::information(this,"Message","Please input integers from 0 to 250, or it will be detect by default from 80 to 150.");
            }else{

            lowerThreshold = min.toInt();
            max_lowThreshold = max.toInt();
            }

            cout< keypoints1;
            std::vector keypoints2;
            cv::Ptr sift = cv::SiftFeatureDetector::create();

            // 2. 计算特征点
//            t1 = cv::getTickCount();
            sift->detect(image1, keypoints1);
//            t2 = cv::getTickCount();
//            tkpt = 1000.0*(t2-t1) / cv::getTickFrequency();
            sift->detect(image2, keypoints2);

            // 3. 计算特征描述符
            cv::Mat descriptors1, descriptors2;
//            t1 = cv::getTickCount();
            sift->compute(image1, keypoints1, descriptors1);
//            t2 = cv::getTickCount();
//            tdes = 1000.0*(t2-t1) / cv::getTickFrequency();
            sift->compute(image2, keypoints2, descriptors2);

            // 4. 特征匹配
            cv::Ptr matcher = cv::DescriptorMatcher::create(cv::DescriptorMatcher::BRUTEFORCE);
            // cv::BFMatcher matcher(cv::NORM_L2);

            // 直接暴力匹配
            std::vector matches;
            t1 = cv::getTickCount();
            matcher->match(descriptors1, descriptors2, matches);
            t2 = cv::getTickCount();
            tmatch_bf = 1000.0*(t2-t1) / cv::getTickFrequency();
            cout<

基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

数据库表

列名

类型

可空

默认值

注释

id

INT

NOT NULL

主键id

filename

VARCHAR[45]

NOT NULL

文件路径

similarity_color

DOUBLE

NULL

NULL

颜色特征相似值

similarity_edge

DOUBLE

NULL

NULL

边缘特征相似值

similarity_multi

DOUBLE

NULL

NULL

多特征融合相似值

基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

代码

void MainWindow::on_pushButton_Histogram_clicked()
{
    Mat src, dst, dst1;
        src = imread(Current_File_Name.toStdString());
        if (!src.data)
        {
            QMessageBox::information(this,"Warning","Image Load Faild");
        }else{

        //步骤一:分通道显示
        vectorbgr_planes;
        split(src, bgr_planes);
        //split(// 把多通道图像分为多个单通道图像 const Mat &src, //输入图像 Mat* mvbegin)// 输出的通道图像数组

        //步骤二:计算直方图
        int histsize = 256;
        float range[] = { 0,256 };
        const float*histRanges = { range };
        Mat b_hist, g_hist, r_hist;
        calcHist(&bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histsize, &histRanges, true, false);
        calcHist(&bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histsize, &histRanges, true, false);
        calcHist(&bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histsize, &histRanges, true, false);

        //归一化
        int hist_h = 400;//直方图的图像的高
        int hist_w = 512;//直方图的图像的宽
        int bin_w = hist_w / histsize;//直方图的等级
        Mat histImage(hist_w, hist_h, CV_8UC3, Scalar(0, 0, 0));//绘制直方图显示的图像
        normalize(b_hist, b_hist, 0, hist_h, NORM_MINMAX, -1, Mat());//归一化
        normalize(g_hist, g_hist, 0, hist_h, NORM_MINMAX, -1, Mat());
        normalize(r_hist, r_hist, 0, hist_h, NORM_MINMAX, -1, Mat());

        //步骤三:绘制直方图(render histogram chart)
        for (int i = 1; i < histsize; i++)
        {
            //绘制蓝色分量直方图
            line(histImage, Point((i - 1)*bin_w, hist_h - cvRound(b_hist.at(i - 1))),
                Point((i)*bin_w, hist_h - cvRound(b_hist.at(i))), Scalar(255, 0, 0), 2, CV_AA);
            //绘制绿色分量直方图
            line(histImage, Point((i - 1)*bin_w, hist_h - cvRound(g_hist.at(i - 1))),
                Point((i)*bin_w, hist_h - cvRound(g_hist.at(i))), Scalar(0, 255, 0), 2, CV_AA);
            //绘制红色分量直方图
            line(histImage, Point((i - 1)*bin_w, hist_h - cvRound(r_hist.at(i - 1))),
                Point((i)*bin_w, hist_h - cvRound(r_hist.at(i))), Scalar(0, 0, 255), 2, CV_AA);
        }
        imshow("Histogram Image", histImage);
        waitKey(0);
        }
}

基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

代码:

Mat img, gray, blurred, edge;
int lowerThreshold_1 = 0;
int max_lowThreshold_1 = 250;

void CannyThreshold(int, void*) {
    GaussianBlur(gray,
        blurred,
        cv::Size(3, 3),  //Smoothing window width and height in pixels
        3);  //How much the image will be blur

    Canny(blurred,
        edge,
        lowerThreshold_1, //lower threshold
        50);    //higher threshold
    imshow("Edge Detection", edge);
}

Mat Canny_for_edgematches(const cv::String s){
    Mat img, gray, blurred, edge;

    img=imread(s);
    cvtColor(img, gray, COLOR_BGR2GRAY);

    GaussianBlur(gray,
        blurred,
        cv::Size(3, 3),  //Smoothing window width and height in pixels
        3);  //How much the image will be blur

    Canny(blurred,
        edge,
        lowerThreshold, //lower threshold
        max_lowThreshold);
    return edge;
}

void MainWindow::on_pushButton_Edge_clicked()
{
    img = imread(Current_File_Name.toStdString());
    if (!img.data)
    {
        QMessageBox::information(this,"Warning","Image Load Faild");
     }else{

    cvtColor(img, gray, COLOR_BGR2GRAY);
    cv::namedWindow("Edge Detection", 0);
    cv::resizeWindow("Edge Detection",900,800);
    createTrackbar("Min Threshold:", "Edge Detection", &lowerThreshold_1, max_lowThreshold_1, CannyThreshold);
    CannyThreshold(lowerThreshold_1, 0);
    waitKey(0);
    }
}

基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】
Mat HSV_show(const cv::String str,const int i)
{
    Mat srcimage = imread(str);

        Mat srcimageHSV;
        //图像转化HSV颜色空间图像
        cvtColor(srcimage, srcimageHSV, COLOR_BGR2HSV);
//        imshow("HSV空间图像", srcimageHSV);
        int channels = 0;
        int histsize[] = { 518 };
        float midranges[] = { 0,255 };
        const float *ranges[] = { midranges };
        MatND  dsthist;
        calcHist(&srcimageHSV, 1, &channels, Mat(), dsthist, 1, histsize, ranges, true, false);
        Mat b_drawImage = Mat::zeros(Size(518, 518), CV_8UC3);

        double g_dhistmaxvalue;
        minMaxLoc(dsthist, 0, &g_dhistmaxvalue, 0, 0);
        for (int i = 0;i < 518;i++) {
            int value = cvRound(518 * 0.9 *(dsthist.at(i) / g_dhistmaxvalue));
            line(b_drawImage, Point(i, b_drawImage.rows - 1), Point(i, b_drawImage.rows - 1 - value), Scalar(255, 0, 0));
        }
//        imshow("H通道直方图", b_drawImage);

        channels = 1;
        calcHist(&srcimageHSV, 1, &channels, Mat(), dsthist, 1, histsize, ranges, true, false);
        Mat g_drawImage = Mat::zeros(Size(518, 518), CV_8UC3);
        for (int i = 0;i < 518;i++) {
            int value = cvRound(518 * 0.9 *(dsthist.at(i) / g_dhistmaxvalue));
            line(g_drawImage, Point(i, g_drawImage.rows - 1), Point(i, g_drawImage.rows - 1 - value), Scalar(0, 255, 0));
        }
//        imshow("S通道直方图", g_drawImage);

        channels = 2;
        calcHist(&srcimageHSV, 1, &channels, Mat(), dsthist, 1, histsize, ranges, true, false);
        Mat r_drawImage = Mat::zeros(Size(518, 518), CV_8UC3);
        for (int i = 0;i < 518;i++) {
            int value = cvRound(518 * 0.9 *(dsthist.at(i) / g_dhistmaxvalue));
            line(r_drawImage, Point(i, r_drawImage.rows - 1), Point(i, r_drawImage.rows - 1 - value), Scalar(0, 0, 255));
        }
//        imshow("V通道直方图", r_drawImage);
        cv::Mat add1, add2;
        add(b_drawImage, g_drawImage, add1);
        add(add1, r_drawImage, add2);

        QSqlQuery query_color_final;
        QString qstr_2;
        std::vector  finallist_color;

        query_color_final.exec( "select * from image_address order by similarity_color asc" );
//---------------------------------------------------------------------
        while(query_color_final.next())
                {
                     qstr_2 = query_color_final.value(2).toString();
                     finallist_color.push_back(qstr_2.toStdString());
                }
        cv::String text="HSV-Similarity:"+finallist_color[i];

        putText(add2,text,Point(30,30),CV_FONT_HERSHEY_COMPLEX,0.7,Scalar(255,255,255),1,1);

        return add2;
//        在img图片上,显示Hello,位置在(50,50),字体类型为FONT_HERSHEY_SIMPLEX,字体大小为2,颜色为绿色,字体厚度为4,线型默认为8.

//         imshow("HSV",add2);
//         waitKey(0);
}
Mat Matches_edge(const cv::String im1,const cv::String im2,const int i){
//        int64 t1, t2;
//        double tkpt, tdes;
//        double tmatch_bf;

        // 1. 读取图片
        const cv::Mat image1 = Canny_for_edgematches(im1);//Load as grayscale
        const cv::Mat image2 = Canny_for_edgematches(im2); //Load as grayscale
        std::vector keypoints1;
        std::vector keypoints2;

        cv::Ptr sift = cv::SiftFeatureDetector::create();
        // 2. 计算特征点
//        t1 = cv::getTickCount();
        sift->detect(image1, keypoints1);
//        t2 = cv::getTickCount();
//        tkpt = 1000.0*(t2-t1) / cv::getTickFrequency();
        sift->detect(image2, keypoints2);

        // 3. 计算特征描述符
        cv::Mat descriptors1, descriptors2;
//        t1 = cv::getTickCount();
        sift->compute(image1, keypoints1, descriptors1);
//        t2 = cv::getTickCount();
//        tdes = 1000.0*(t2-t1) / cv::getTickFrequency();
        sift->compute(image2, keypoints2, descriptors2);

        // 4. 特征匹配
        cv::Ptr matcher = cv::DescriptorMatcher::create(cv::DescriptorMatcher::BRUTEFORCE);
        // cv::BFMatcher matcher(cv::NORM_L2);

        // (1) 直接暴力匹配
        std::vector matches;
//        t1 = cv::getTickCount();
        matcher->match(descriptors1, descriptors2, matches);
//        t2 = cv::getTickCount();
//        tmatch_bf = 1000.0*(t2-t1) / cv::getTickFrequency();
        // 画匹配图
        cv::Mat img_matches_bf;
        drawMatches(image1, keypoints1, image2, keypoints2, matches, img_matches_bf);

        QSqlQuery query_edge_final;
        QString qstr_2;
        std::vector  finallist_edge;

        query_edge_final.exec( "select * from image_address order by similarity_edge asc" );
//---------------------------------------------------------------------
        while(query_edge_final.next())
                {
                     qstr_2 = query_edge_final.value(3).toString();
                     finallist_edge.push_back(qstr_2.toStdString());
                }
        cv::String text="Edge-Similarity:"+finallist_edge[i];

        putText(img_matches_bf,text,Point(20,30),CV_FONT_HERSHEY_COMPLEX,0.7,Scalar(255,255,255),1,1);

        return img_matches_bf;
//        在img图片上,显示Hello,位置在(50,50),字体类型为FONT_HERSHEY_SIMPLEX,字体大小为2,颜色为白色,字体厚度为4,线型默认为8.

//        imshow("Edge Matches", img_matches_bf);

}

基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

代码:

Mat edge_list(int i){
    QSqlQuery query_edge_final;
    QString qstr_2;
    std::vector  finallist_edge;

    query_edge_final.exec( "select * from image_address order by similarity_edge asc" );
    while(query_edge_final.next())
            {
                 qstr_2 = query_edge_final.value(1).toString();
                 finallist_edge.push_back(qstr_2.toStdString());
            }
//    int n = atoi(finallist_edge[i].c_str());
//    imshow("bf_matches", img_matches_bf[n]);
      return Matches_edge(Current_File_Name.toStdString(),finallist_edge[i],i);

}

Mat color_list(int i){
    QSqlQuery query_color_final;
    QString qstr_2;
    std::vector  finallist_color;

    query_color_final.exec( "select * from image_address order by similarity_color asc" );
    while(query_color_final.next())
            {
                 qstr_2 = query_color_final.value(1).toString();
                 finallist_color.push_back(qstr_2.toStdString());
            }
//    int n = atoi(finallist_edge[i].c_str());
//    imshow("bf_matches", img_matches_bf[n]);
      return HSV_show(finallist_color[i],i);

}
bool displayImage(const Mat img1,const Mat img2,const int i)
{
    if (img1.empty() || img2.empty())
        {
            return false;
        }
//        double height = img1.rows;

//        double width1 = img1.cols;
//        double width2 = img2.cols;
        double height2 = img2.rows;

        cout< img2.rows)
//        {
//            height = img2.rows;
//            width1 = img1.cols * ((float)img2.rows / (float)img1.rows);
//            resize(img1, img1, Size(width1, height));
//        }
//        else if(img1.rows  vImgs;
            Mat result;
            vImgs.push_back(img1);
            vImgs.push_back(re_img);
//            vconcat(vImgs, result); //垂直方向拼接
            hconcat(vImgs, result); //水平方向拼接

    QSqlQuery query_multi_final;
    QString qstr_2;
    std::vector  finallist_multi;

    query_multi_final.exec( "select * from image_address order by similarity_color asc" );
//---------------------------------------------------------------------
    while(query_multi_final.next())
            {
                 qstr_2 = query_multi_final.value(4).toString();
                 finallist_multi.push_back(qstr_2.toStdString());
            }
    cv::String text="Multi-Similarity:"+finallist_multi[i];

    putText(result,text,Point(30,60),CV_FONT_HERSHEY_COMPLEX,0.7,Scalar(255,255,255),1,1);

    imshow("Multi-Features Fusion", result);

    waitKey(0);
    return 0;

//    ui->label->clear();     //清空缓存区,否则会导致第二次加载同一张图片不会显示,详细百度有说
//    ui->label->setPixmap(image);   //3.方便起见,直接采用label显示
}

选择检索模式的逻辑代码(部分):

bool MainWindow::eventFilter(QObject *obj, QEvent *event){

    if (obj == ui->label_show_1)//指定某个QLabel
         {
             if (event->type() == QEvent::MouseButtonPress) //鼠标点击
             {
                 if(flag_edge_only && flag_color_only){

                  displayImage(color_list(0),edge_list(0),0);

                 }else{
                 if(flag_edge_only){
                 QMouseEvent *mouseEvent = static_cast(event); // 事件转换

                 if(mouseEvent->button() == Qt::LeftButton)
                 {

                    imshow("Edge Matches", edge_list(0));

                     return true;
                 }

             }
                  if(flag_color_only){
                     QMouseEvent *mouseEvent = static_cast(event); // 事件转换

                     if(mouseEvent->button() == Qt::LeftButton)
                     {

                        imshow("HSV",color_list(0));

                         return true;
                     }

                 }
                  else
                  {
                      return false;
                  }

             }
             }
             else
             {
                 return false;
             }
         }

         else if (obj == ui->label_show_2)
    {
.......

注意该系统查全率及查准率较低,且为了保证检索效率建议上传25张以内的图像至指定目录

项目

分类

平均查全率(R)

平均查准率(R)

平均检索时间(T)

单一颜色特征

彩色图

灰度图

0.81

0

0.73

0

0.8

0.6

单一边缘特征

彩色图

灰度图

0.47

0.51

0.43

0.45

16.3

16.2

多特征融合

彩色图

灰度图

0.83

0.51

0.74

0.45

0.3

0.3

完整源码放在GitHub上了,需要自取:GitHub – Karly0828/project: This is an Image Retrieval System based on Color-features and Edge Dection (Canny edge detector), uses C++ as the development language, Qt5.12 as the main development framework for interface design, OpenCV4.4 as the development tool library, and MySQL database to design and implement the functions of the system.

记得先浏览系统使用说明书,安装并调试好合适版本的软件工具

源码写的很烂,大致是扒了外网的一些教程+四处缝了下代码+按自己需求改了

如果有人需要的话后续可以贴一篇详细点的源码注解,如果没人看的话……再说吧

有任何问题也可留言给我,看到会及时回复

Original: https://blog.csdn.net/karly08/article/details/125863869
Author: karly08
Title: 基于边缘检测(Canny算子)和颜色特征的图像检索系统(含源码)OpenCV+QT+MySQL【C++】

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/645087/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球