zoukankan      html  css  js  c++  java
  • 图像矫正-基于opencv实现

    一、引言

            上篇文章中四种方法对图像进行倾角矫正都非常有效。Hough变换和Radon相似,其抗干扰能力比较强,但是运算量大,程序执行慢,其改进方法为:我们可以不对整幅图像进行操作,可以在图像中选取一块(必须含有一条与倾角有关的直线)进行操作,从而减小运算量。这里Hough变换法和Radon变换法进行倾角检测的最大精度为1度。它们的优点是可以计算有断点的直线的倾角。最小二乘法的优点就是运算量小,但是其抗干扰能力比较差,容易受到噪声的影响。两点法虽然理论简单,但由于采样点比较多而且这些点服从随机分布,计算均值后能有效抑制干扰,实验表明其矫正效果很好,最大精度可以明显小于1度,而且计算量也很小。最小二乘法和两点法不能计算有断点的直线倾角,这是这两种方法的缺点。

    二、基于opencv的图像矫正实现

             对图像进行旋转矫正,关键是获取旋转角度是多少,在获取旋转角度后,可以用仿射变换对图像进行矫正。本文是基于opencv的houghline变换实现的图像旋转角度获取,具体代码为:

            _grayimage = cv2.cvtColor(self._srcimage,cv2.COLOR_RGB2GRAY)
            _cannyimage = cv2.Canny(_grayimage,CANNY_LOW_THRESHOLD, CANNY_HIGH_THRESHOLD, apertureSize=3)
            lines = cv2.HoughLinesP(_cannyimage,1,np.pi/180,160,minLineLength=200, maxLineGap=180)
            
    #        寻找长度最长的线
            distance = []
            for line in lines:
                x1,y1,x2,y2 = line[0]
                dis = np.sqrt(pow((x2-x1),2)+pow((y2-y1),2))
                distance.append(dis)
            max_dis_index = distance.index(max(distance))        
            max_line = lines[max_dis_index]
            x1,y1,x2,y2 = max_line[0]
    
    #       获取旋转角度
            angle = cv2.fastAtan2((y2-y1),(x2-x1))

    根据hough变化获取旋转角度后,根据仿射矩阵进行变换,进而得到矫正后的图像,具体代码为:

            centerpoint = (self._srcimage.shape[1]/2,self._srcimage.shape[0]/2)
            rotate_mat = cv2.getRotationMatrix2D(centerpoint,angle,1.0)         #获取旋转矩阵
            correct_image = cv2.warpAffine(self._srcimage,rotate_mat,(self._srcimage.shape[1],self._srcimage.shape[0]),borderValue =(255,255,255) )

    三、霍夫线变换

             上一篇文章已经针对霍夫变换有了一个介绍,本章将结合opencv中的函数进行详细讲解。

             在使用霍夫线变换之前, 首先要对图像进行边缘检测的处理,也即霍夫线变换的直接输入只能是边缘二值图像.

            OpenCV支持三种不同的霍夫线变换,它们分别是:标准霍夫变换(Standard Hough Transform,SHT)和多尺度霍夫变换(Multi-Scale Hough Transform,MSHT)累计概率霍夫变换(Progressive Probabilistic Hough Transform ,PPHT)。

              其中,多尺度霍夫变换(MSHT)为经典霍夫变换(SHT)在多尺度下的一个变种。累计概率霍夫变换(PPHT)算法是标准霍夫变换(SHT)算法的一个改进,它在一定的范围内进行霍夫变换,计算单独线段的方向以及范围,从而减少计算量,缩短计算时间。之所以称PPHT为“概率”的,是因为并不将累加器平面内的所有可能的点累加,而只是累加其中的一部分,该想法是如果峰值如果足够高,只用一小部分时间去寻找它就够了。这样猜想的话,可以实质性地减少计算时间。

               在OpenCV中,我们可以用HoughLines函数来调用标准霍夫变换SHT和多尺度霍夫变换MSHT。

               而HoughLinesP函数用于调用累计概率霍夫变换PPHT。累计概率霍夫变换执行效率很高,所有相比于HoughLines函数,我们更倾向于使用HoughLinesP函数。

    3.1 霍夫线变换原理

            一条直线在图像二维空间可由两个变量表示. 如:

    <1>在笛卡尔坐标系: 可由参数: 斜率和截距(m,b) 表示。

    <2>在极坐标系: 可由参数: 极径和极角表示。

             对于霍夫变换, 我们将采用第二种方式极坐标系来表示直线. 因此, 直线的表达式可为:

    化简便可得到:

    这就意味着每一对代表一条通过点的直线。

             如果对于一个给定点我们在极坐标对极径极角平面绘出所有通过它的直线, 将得到一条正弦曲线. 例如, 对于给定点X_0= 8 和Y_0= 6 我们可以绘出下图 (在平面):

             只绘出满足下列条件的点 和  .

             我们可以对图像中所有的点进行上述操作. 如果两个不同点进行上述操作后得到的曲线在平面相交, 这就意味着它们通过同一条直线. 例如,接上面的例子我们继续对点 和点 绘图, 得到下图:

                这三条曲线在平面相交于点 (0.925, 9.6), 坐标表示的是参数对 或者是说点, 点和点组成的平面内的的直线。

             以上的说明表明,一般来说, 一条直线能够通过在平面 寻找交于一点的曲线数量来检测。而越多曲线交于一点也就意味着这个交点表示的直线由更多的点组成. 一般来说我们可以通过设置直线上点的阈值来定义多少条曲线交于一点我们才认为检测到了一条直线。

             这就是霍夫线变换要做的. 它追踪图像中每个点对应曲线间的交点. 如果交于一点的曲线的数量超过了阈值, 那么可以认为这个交点所代表的参数对在原图像中为一条直线。

    关于霍夫变换的详细解释,可以看此英文页面:http://homepages.inf.ed.ac.uk/rbf/HIPR2/hough.htm

    3.2 HoughLinesP()函数详解

    因为opencv内部基于c++写的,在函数详解中以c++源码为主。

    C++: void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength=0, double maxLineGap=0 )
    • 第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
    • 第二个参数,InputArray类型的lines,经过调用HoughLinesP函数后后存储了检测到的线条的输出矢量,每一条线由具有四个元素的矢量(x_1,y_1, x_2, y_2)  表示,其中,(x_1, y_1)和(x_2, y_2) 是是每个检测到的线段的结束点。
    • 第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。
    • 第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
    • 第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
    • 第六个参数,double类型的minLineLength,有默认值0,表示最低线段的长度,比这个设定参数短的线段就不能被显现出来。即当检测出的直线长度大于minLinLength,才认为这是一条直线。
    • 第七个参数,double类型的maxLineGap,有默认值0,允许将同一行点与点之间连接起来的最大的距离。即假若直线从中间某处断开,那么所允许的缺口的最大长度。
    //-----------------------------------【头文件包含部分】---------------------------------------  
    //      描述:包含程序所依赖的头文件  
    //----------------------------------------------------------------------------------------------   
    #include <opencv2/opencv.hpp>  
    #include <opencv2/imgproc/imgproc.hpp>  
      
    //-----------------------------------【命名空间声明部分】---------------------------------------  
    //      描述:包含程序所使用的命名空间  
    //-----------------------------------------------------------------------------------------------   
    using namespace cv;  
    //-----------------------------------【main( )函数】--------------------------------------------  
    //      描述:控制台应用程序的入口函数,我们的程序从这里开始  
    //-----------------------------------------------------------------------------------------------  
    int main( )  
    {  
        //【1】载入原始图和Mat变量定义     
        Mat srcImage = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图  
        Mat midImage,dstImage;//临时变量和目标图的定义  
      
        //【2】进行边缘检测和转化为灰度图  
        Canny(srcImage, midImage, 50, 200, 3);//进行一此canny边缘检测  
        cvtColor(midImage,dstImage, CV_GRAY2BGR);//转化边缘检测后的图为灰度图  
      
        //【3】进行霍夫线变换  
        vector<Vec4i> lines;//定义一个矢量结构lines用于存放得到的线段矢量集合  
        HoughLinesP(midImage, lines, 1, CV_PI/180, 80, 50, 10 );  
      
        //【4】依次在图中绘制出每条线段  
        for( size_t i = 0; i < lines.size(); i++ )  
        {  
            Vec4i l = lines[i];  
            line( dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(186,88,255), 1, CV_AA);  
        }  
      
        //【5】显示原始图    
        imshow("【原始图】", srcImage);    
      
        //【6】边缘检测后的图   
        imshow("【边缘检测后的图】", midImage);    
      
        //【7】显示效果图    
        imshow("【效果图】", dstImage);    
      
        waitKey(0);    
      
        return 0;    
    }

    运行截图:

    来一张大图:

    3.3 源码解释

    void cv::HoughLinesP( InputArray _image,OutputArray _lines,  
                          double rho, double theta,int threshold,  
                          double minLineLength,double maxGap )  
    {  
       Ptr<CvMemStorage> storage = cvCreateMemStorage(STORAGE_SIZE);  
       Mat image = _image.getMat();  
       CvMat c_image = image;  
        CvSeq*seq = cvHoughLines2( &c_image, storage, CV_HOUGH_PROBABILISTIC,  
                        rho, theta, threshold,minLineLength, maxGap );  
       seqToMat(seq, _lines);  
    }
    CV_IMPL CvSeq*  
    cvHoughLines2( CvArr* src_image, void*lineStorage, int method,  
                   double rho, double theta, intthreshold,  
                   double param1, double param2 )  
    {  
       CvSeq* result = 0;  
       
       CvMat stub, *img = (CvMat*)src_image;  
       CvMat* mat = 0;  
       CvSeq* lines = 0;  
       CvSeq lines_header;  
       CvSeqBlock lines_block;  
       int lineType, elemSize;  
       int linesMax = INT_MAX;  
       int iparam1, iparam2;  
       
       img = cvGetMat( img, &stub );  
       
       if( !CV_IS_MASK_ARR(img))  
           CV_Error( CV_StsBadArg, "The source image must be 8-bit,single-channel" );  
       
       if( !lineStorage )  
           CV_Error( CV_StsNullPtr, "NULL destination" );  
       
       if( rho <= 0 || theta <= 0 || threshold <= 0 )  
           CV_Error( CV_StsOutOfRange, "rho, theta and threshold must bepositive" );  
       
       if( method != CV_HOUGH_PROBABILISTIC )  
        {  
           lineType = CV_32FC2;  
           elemSize = sizeof(float)*2;  
        }  
       else  
        {  
           lineType = CV_32SC4;  
           elemSize = sizeof(int)*4;  
        }  
       
       if( CV_IS_STORAGE( lineStorage ))  
        {  
           lines = cvCreateSeq( lineType, sizeof(CvSeq), elemSize,(CvMemStorage*)lineStorage );  
        }  
       else if( CV_IS_MAT( lineStorage ))  
        {  
           mat = (CvMat*)lineStorage;  
       
           if( !CV_IS_MAT_CONT( mat->type ) || (mat->rows != 1 &&mat->cols != 1) )  
               CV_Error( CV_StsBadArg,  
               "The destination matrix should be continuous and have a single rowor a single column" );  
       
           if( CV_MAT_TYPE( mat->type ) != lineType )  
               CV_Error( CV_StsBadArg,  
               "The destination matrix data type is inappropriate, see themanual" );  
       
           lines = cvMakeSeqHeaderForArray( lineType, sizeof(CvSeq), elemSize,mat->data.ptr,  
                                            mat->rows + mat->cols - 1, &lines_header, &lines_block );  
           linesMax = lines->total;  
           cvClearSeq( lines );  
        }  
       else  
           CV_Error( CV_StsBadArg, "Destination is not CvMemStorage* norCvMat*" );  
       
       iparam1 = cvRound(param1);  
       iparam2 = cvRound(param2);  
       
       switch( method )  
        {  
       case CV_HOUGH_STANDARD:  
             icvHoughLinesStandard( img, (float)rho,  
                   (float)theta, threshold,lines, linesMax );  
             break;  
       case CV_HOUGH_MULTI_SCALE:  
             icvHoughLinesSDiv( img, (float)rho, (float)theta,  
                    threshold, iparam1, iparam2,lines, linesMax );  
             break;  
       case CV_HOUGH_PROBABILISTIC:  
             icvHoughLinesProbabilistic( img, (float)rho, (float)theta,  
                    threshold, iparam1, iparam2,lines, linesMax );  
             break;  
       default:  
           CV_Error( CV_StsBadArg, "Unrecognized method id" );  
        }  
       
       if( mat )  
        {  
           if( mat->cols > mat->rows )  
               mat->cols = lines->total;  
           else  
               mat->rows = lines->total;  
        }  
       else  
           result = lines;  
       
       return result;  
    }

    先看Hough检测直线的代码,cvHoughLines2也只不过是个对不同Hough方法的封装,下面是该函数中的部分代码,选择不同的Hough变换方法。

    /* 这段注释解释了函数各个参数的作用
    Here image is an input raster;
    step is it's step; size characterizes it's ROI;
    rho and theta are discretization steps (in pixels and radians correspondingly).
    threshold is the minimum number of pixels in the feature for it
    to be a candidate for line. lines is the output
    array of (rho, theta) pairs. linesMax is the buffer size (number of pairs).
    Functions return the actual number of found lines.
    */
    static void
    icvHoughLinesStandard( const CvMat* img, float rho, float theta,
                           int threshold, CvSeq *lines, int linesMax )
    {
        cv::AutoBuffer<int> _accum, _sort_buf;    // _accum:计数用数组,_sort_buf,排序用数组
        cv::AutoBuffer<float> _tabSin, _tabCos;   // 提前计算sin与cos值,避免重复计算带来的计算性能下降
    
        const uchar* image;
        int step, width, height;
        int numangle, numrho;
        int total = 0;
        float ang;
        int r, n;
        int i, j;
        float irho = 1 / rho;   // rho指像素精度,常取1,因此irho常为1
        double scale;
    
        CV_Assert( CV_IS_MAT(img) && CV_MAT_TYPE(img->type) == CV_8UC1 );
    
        image = img->data.ptr;
        step = img->step;
        width = img->cols;
        height = img->rows;
    
        numangle = cvRound(CV_PI / theta);  // 根据th精度计算th维度的长度
        numrho = cvRound(((width + height) * 2 + 1) / rho);  // 根据r精度计算r维度的长度
    
        _accum.allocate((numangle+2) * (numrho+2));
        _sort_buf.allocate(numangle * numrho);
        _tabSin.allocate(numangle);
        _tabCos.allocate(numangle);
        int *accum = _accum, *sort_buf = _sort_buf;
        float *tabSin = _tabSin, *tabCos = _tabCos;
        
        memset( accum, 0, sizeof(accum[0]) * (numangle+2) * (numrho+2) );
    
        for( ang = 0, n = 0; n < numangle; ang += theta, n++ )   // 计算三角函数表,避免重复计算
        {
            tabSin[n] = (float)(sin(ang) * irho);
            tabCos[n] = (float)(cos(ang) * irho);
        }
    
        // stage 1. fill accumulator 
        for( i = 0; i < height; i++ )
            for( j = 0; j < width; j++ )
            {
                if( image[i * step + j] != 0 )
                    for( n = 0; n < numangle; n++ )
                    {
                        r = cvRound( j * tabCos[n] + i * tabSin[n] );  // Hough极坐标变换式
                        r += (numrho - 1) / 2;
                        accum[(n+1) * (numrho+2) + r+1]++;  // 计数器统计
                    }
            }
    
        // stage 2. find local maximums
        for( r = 0; r < numrho; r++ )
            for( n = 0; n < numangle; n++ )
            {
                int base = (n+1) * (numrho+2) + r+1;
                if( accum[base] > threshold &&             // 大于阈值,且是局部极大值
                    accum[base] > accum[base - 1] && accum[base] >= accum[base + 1] &&
                    accum[base] > accum[base - numrho - 2] && accum[base] >= accum[base + numrho + 2] )
                    sort_buf[total++] = base;
            }
    
        // stage 3. sort the detected lines by accumulator value
        icvHoughSortDescent32s( sort_buf, total, accum );
    
        // stage 4. store the first min(total,linesMax) lines to the output buffer
        linesMax = MIN(linesMax, total);  // linesMax是输入参数,表示最多输出多少个直线参数
        scale = 1./(numrho+2);
        for( i = 0; i < linesMax; i++ )
        {
            CvLinePolar line;           // 输出结构,就是(r,theta)
            int idx = sort_buf[i];
            int n = cvFloor(idx*scale) - 1;
            int r = idx - (n+1)*(numrho+2) - 1;
            line.rho = (r - (numrho - 1)*0.5f) * rho;
            line.angle = n * theta;
            cvSeqPush( lines, &line );  // 确定的直线入队列输出
        }
    }

    四、图像仿射变换

            几何变换可以看成图像中物体(或像素)空间位置改变,或者说是像素的移动。

            几何运算需要空间变换和灰度级差值两个步骤的算法,像素通过变换映射到新的坐标位置,新的位置可能是在几个像素之间,即不一定为整数坐标。这时就需要灰度级差值将映射的新坐标匹配到输出像素之间。最简单的插值方法是最近邻插值,就是令输出像素的灰度值等于映射最近的位置像素,该方法可能会产生锯齿。这种方法也叫零阶插值,相应比较复杂的还有一阶和高阶插值。

    4.1 仿射变换原理

    仿射变换的功能是从二维坐标到二维坐标之间的线性变换,且保持二维图形的“平直性”和“平行性”。仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,翻转,旋转和剪切。

    空间变换对应矩阵的仿射变换。一个坐标通过函数变换的新的坐标位置:

    所以在程序中我们可以使用一个2*3的数组结构来存储变换矩阵:


    4.1、opencv的图像变换函数

    void cvWarpAffine(   
        const CvArr* src,//输入图像  
        CvArr* dst, //输出图像  
        const CvMat* map_matrix,   //2*3的变换矩阵  
        int flags=CV_INTER_LINEAR+CV_WARP_FILL_OUTLIERS,   //插值方法的组合  
        CvScalar fillval=cvScalarAll(0)   //用来填充边界外的值  
    );
    cv2.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])

    非关键字参数有src, M, dsize,分别表示源图像,变换矩阵,变换后的图像的长宽。

    下面介绍一些典型的仿射变换:

    (1)平移,将每一点移到到(x+t , y+t),变换矩阵为

    (2)缩放变换  将每一点的横坐标放大或缩小sx倍,纵坐标放大(缩小)到sy倍,变换矩阵为

    (3)旋转变换原点:目标图形围绕原点顺时针旋转Θ 弧度,变换矩阵为

    (4) 旋转变换  :目标图形以(x , y )为轴心顺时针旋转θ弧度,变换矩阵为

    相当于两次平移与一次原点旋转变换的复合,即先将轴心(x,y)移到到原点,然后做旋转变换,最后将图片的左上角置为图片的原点,即

    有的人可能会说为什么这么复杂呢,那是因为在opencv的图像处理中,所有对图像的处理都是从原点进行的,而图像的原点默认为图像的左上角,而我们对图像作旋转处理时一般以图像的中点为轴心,因此就需要做如下处理。

    其中的变换矩阵由:

    M=cv2.getRotationMatrix2D(rotate_center, degree, scale)

    rotate_center为一个2元的元组,表示旋转中心坐标,degree表示逆时针旋转的角度,scale表示缩放的比例,获取。

    它得到的矩阵是:

    其中α = scale * cos( angle ) , β = scale  * sing( angle )  , ( center.x , center.y ) 表示旋转轴心

    五、测试代码

    # -*- coding: utf-8 -*-
    """
    Created on Thu Jun 29 14:23:56 2017
    
    @author: Administrator
    """
    
    import cv2
    import numpy as np
    import os
    
    CANNY_LOW_THRESHOLD = 50                # canny算法低阈值
    CANNY_HIGH_THRESHOLD = 150              #canny算法高阈值
    HOUGH_DELTARHO = 1                      #hough检测步长
    HOUGH_DELTA_THETA = 100
    TESTIMAGESDIR = 'C:\Users\Administrator\Desktop\OCR\retate'
    
    class RotateByHough(object):
        def __init__(self,image):
            self._srcimage = image
    
    #获取图像背景像素值        
        def get_background_pix(self,):
            width = self._srcimage.shape[1]
            height = self._srcimage.shape[0]
            left_coners = self._srcimage[0:9,0:9]
            right_coners = self._srcimage[height-10:height-1,width-10:width-1]
            left_array = np.array(left_coners).reshape(81,1,3)
            left_point = np.mean(left_array,0)        
            right_point = np.mean(np.array(right_coners).reshape(81,1,3),0)
    #        print(left_point)
            final = np.array((left_point[0],right_point[0]),np.float32).reshape(2,1,3)        
            final_point = np.mean(final,0)
            
            self._point_scale = (int(final_point[0][0]),int(final_point[0][1]),int(final_point[0][2]))
    #        print(self._point_scale)
    
    #       输入矫正图像的宽和高        
        def detect_hough_line(self):
            _grayimage = cv2.cvtColor(self._srcimage,cv2.COLOR_RGB2GRAY)
            _cannyimage = cv2.Canny(_grayimage,CANNY_LOW_THRESHOLD, CANNY_HIGH_THRESHOLD, apertureSize=3)
            lines = cv2.HoughLinesP(_cannyimage,1,np.pi/180,160,minLineLength=200, maxLineGap=180)
            
    #        寻找长度最长的线
            distance = []
            for line in lines:
                x1,y1,x2,y2 = line[0]
                dis = np.sqrt(pow((x2-x1),2)+pow((y2-y1),2))
                distance.append(dis)
            max_dis_index = distance.index(max(distance))        
            max_line = lines[max_dis_index]
            x1,y1,x2,y2 = max_line[0]
    
    #       获取旋转角度
            angle = cv2.fastAtan2((y2-y1),(x2-x1))
            centerpoint = (self._srcimage.shape[1]/2,self._srcimage.shape[0]/2)
            rotate_mat = cv2.getRotationMatrix2D(centerpoint,angle,1.0)         #获取旋转矩阵
            correct_image = cv2.warpAffine(self._srcimage,rotate_mat,(self._srcimage.shape[1],self._srcimage.shape[0]),borderValue =(255,255,255) )        
            return correct_image
    
    def get_files(images_dir):
        images = []
        for _file in os.listdir(images_dir):
            filename = os.path.join(images_dir, _file)
            images.append(filename)
            
        return images
    
    #==============================================================================
    # def key_board_event(event):
    # #    监听键盘输入值
    # 
    #     print("Key :", event.Key)
    #     if event.Key == 'Down':
    #         print('next file')        
    #         i = i + 1
    #         if i > len(image_files):
    #             i = len(image_files)        
    #         print(i)
    #         print(image_files[i])
    #     if event.Key == 'Up':
    #         print('pre file')
    #         i = i -1
    #         if i < 0:
    #             i = 0     
    #     
    #==============================================================================    
    def main():
        image_files = get_files(TESTIMAGESDIR)
        files_num = len(image_files)
        while True:
            num = int(input("Enter your input:"))        
            if num > 50:
                break;
            if num < 0 or num > files_num:
                print('resume load')
            else:
                file_name = image_files[num]
                print(file_name)
                image = cv2.imread(file_name)
                roteimage_ins = RotateByHough(image)
                roteimage = roteimage_ins.detect_hough_line()
                width = roteimage.shape[1]
                height = roteimage.shape[0]
    
                for i in range(0, height,  50):
                    for j in range(0, width, 50):
                        cv2.line(roteimage, (0,i),(width, i),(255, 0, 255), 1)                    
                cv2.imshow('test', roteimage)
                cv2.waitKey(0)
    #==============================================================================
    #     hm = pyHook.HookManager()             #设置一个钩子管理对象
    #     hm.KeyDown = key_board_event          #监听所有键盘事件
    #     hm.HookKeyboard()                     #设置键盘钩子
    #==============================================================================    
        
    if __name__ == "__main__":
        main()
  • 相关阅读:
    Spring Boot 2.1.10 学习笔记(2)
    Spring Boot 2.1.10 学习笔记(1)
    Win10 下载与激活 MSDN
    Java JDK 1.8 下载及其版本说明 8u202(最后一个免费版)
    shell函数开发意见优化系统脚本
    php大文件下载支持断点续传
    xunsearch使用笔记
    微信使用的curl方法
    php执行sql语句打印结果
    二维数组排序:array_orderby(php官网评论)
  • 原文地址:https://www.cnblogs.com/polly333/p/7240475.html
Copyright © 2011-2022 走看看