【特征匹配】SIFT原理与C源码剖析

关于SIFT的原理已经有很多大牛的博客上做了解析,本文重点将以Rob
Hess等人用C实现的代码做解析,结合代码SIFT原理会更容易理解。一些难理解点的用了☆标注。

欢迎大家批评指正!

SIFT(Scale-invariant feature transform)即尺度不变特征转换,提取的局部特征点具有尺度不变性,且对于旋转,亮度,噪声等有很高的稳定性。

SIFT特征点提取

本文将以下函数为参照顺序介绍SIFT特征点提取与描述方法。

1.图像预处理

2.构建高斯金字塔(不同尺度下的图像)

3.生成DOG尺度空间

4.关键点搜索与定位

5.计算特征点对应原图的位置

6.为特征点分配方向角

7.构建特征描述子

/**
   Finds SIFT features in an image using user-specified parameter values.  All
   detected features are stored in the array pointed to by \a feat.
*/
int _sift_features( IplImage* img, struct feature** feat, int intvls,
		    double sigma, double contr_thr, int curv_thr,
		    int img_dbl, int descr_width, int descr_hist_bins )
{
  IplImage* init_img;
  IplImage*** gauss_pyr, *** dog_pyr;
  CvMemStorage* storage;
  CvSeq* features;
  int octvs, i, n = 0;

  /* check arguments */
  if( ! img )
    fatal_error( "NULL pointer error, %s, line %d",  __FILE__, __LINE__ );
  if( ! feat )
    fatal_error( "NULL pointer error, %s, line %d",  __FILE__, __LINE__ );

  /* build scale space pyramid; smallest dimension of top level is ~4 pixels */
  init_img = create_init_img( img, img_dbl, sigma );                            //对进行图片预处理
  octvs = log( MIN( init_img->width, init_img->height ) ) / log(2) - 2;  //计算高斯金字塔的组数(octave),同时保证顶层至少有4个像素点
  gauss_pyr = build_gauss_pyr( init_img, octvs, intvls, sigma );  //建立高斯金字塔
  dog_pyr = build_dog_pyr( gauss_pyr, octvs, intvls );   //DOG尺度空间

  storage = cvCreateMemStorage( 0 );
  features = scale_space_extrema( dog_pyr, octvs, intvls, contr_thr,   //极值点检测,并去除不稳定特征点
				  curv_thr, storage );
  calc_feature_scales( features, sigma, intvls );                      //计算特征点所在的尺度
  if( img_dbl )
    adjust_for_img_dbl( features );                                       //如果图像初始被扩大了2倍,所有坐标与尺度要除以2
  calc_feature_oris( features, gauss_pyr );                               //计算特征点的方向角
  compute_descriptors( features, gauss_pyr, descr_width, descr_hist_bins );//计算特征描述子

  /* sort features by decreasing scale and move from CvSeq to array */
  cvSeqSort( features, (CvCmpFunc)feature_cmp, NULL );   //对特征点按尺度排序
  n = features->total;
  *feat = calloc( n, sizeof(struct feature) );
  *feat = cvCvtSeqToArray( features, *feat, CV_WHOLE_SEQ );
  for( i = 0; i < n; i++ )
    {
      free( (*feat)[i].feature_data );
      (*feat)[i].feature_data = NULL;
    }

  cvReleaseMemStorage( &storage );
  cvReleaseImage( &init_img );
  release_pyr( &gauss_pyr, octvs, intvls + 3 );
  release_pyr( &dog_pyr, octvs, intvls + 2 );
  return n;
}

—————————————————————————————————————————————————————

1.图像预处理

/************************ Functions prototyped here **************************/

/*
  Converts an image to 8-bit grayscale and Gaussian-smooths it.  The image is
  optionally doubled in size prior to smoothing.

  @param img input image
  @param img_dbl if true, image is doubled in size prior to smoothing
  @param sigma total std of Gaussian smoothing
*/
static IplImage* create_init_img( IplImage* img, int img_dbl, double sigma )
{
  IplImage* gray, * dbl;
  double sig_diff;

  gray = convert_to_gray32( img );   //转换为32位灰度图
  if( img_dbl )                                  // 图像被放大二倍
    {
      sig_diff = sqrt( sigma * sigma - SIFT_INIT_SIGMA * SIFT_INIT_SIGMA * 4 );   //  sigma = 1.6 , SIFT_INIT_SIGMA = 0.5  lowe认为图像在尺度0.5下最清晰
      dbl = cvCreateImage( cvSize( img->width*2, img->height*2 ),
			   IPL_DEPTH_32F, 1 );
      cvResize( gray, dbl, CV_INTER_CUBIC );  //双三次插值方法 放大图像
      cvSmooth( dbl, dbl, CV_GAUSSIAN, 0, 0, sig_diff, sig_diff );     //高斯平滑
      cvReleaseImage( &gray );
      return dbl;
    }
  else
    {
      sig_diff = sqrt( sigma * sigma - SIFT_INIT_SIGMA * SIFT_INIT_SIGMA );
      cvSmooth( gray, gray, CV_GAUSSIAN, 0, 0, sig_diff, sig_diff ); // 高斯平滑
      return gray;
    }
}

lowe建议把初始图像放大二倍,可以得到更多的特征点,提取到更多细节,并且认为图像在尺度σ = 0.5时图像最清晰,初始高斯尺度为σ = 1.6。

☆第19行因为图像被放大二倍,此时σ = 1.0 。因为对二倍化后的图像平滑是在σ = 0.5 上叠加的高斯模糊,

所以有模糊系数有sig_diff = sqrt (sigma *sigma - 0.5*0.5*4)=sqrt(1.6*1.6 -1) ;

2.构建高斯金字塔

构建高斯金字塔过程即构建出图像在不同尺度上图像,提取到的特征点可有具有尺度不变性。

图像的尺度空间L(x,y,σ)可以用一个高斯函数G(x,y,σ)与图像I(x,y)卷积产生,即L(x,y,σ) = G(x,y,σ) * I(x,y)

其中二维高斯核的计算为             

☆不同的尺度空间即用不同的高斯核函数平滑图像, 平滑系数越大,图像越模糊。即模拟出动物的视觉效果,因为事先不知道物体的大小,在不同的尺度下,图像的细节会表现的不同。当尺度由小变大的过程中,是一个细节逐步简化的过程,图像中特征不够明显的物体,就模糊的多了,而有些物体还可以看得到大致的轮廓。所以要在不同尺度下,观察物体的尺度响应,提取到的特征才能具有尺度不变性。

SIFT算法采用高斯金字塔实现连续的尺度空间的图像。金字塔共分为O(octave)组,每组有S(intervals)层 ,下一组是由上一组隔点采样得到(即降2倍分辨率),这是为了减轻卷积运算的工作量。

构建高斯金字塔(octave = 5, intervals +3=6):

全部空间尺度为:

☆1.这个尺度因子都是在原图上进行的,而在算法实现过程中,采用高斯平滑是在上一层图像上再叠加高斯平滑,即我们在程序中看到的平滑因子为

Eg. 在第一层上为了得到kσ的高斯模糊图像,可以在原图上直接采用kσ平滑,也可以在上一层图像上(已被σ平滑)的图像上采用平滑因子为平滑图像,效果是一样的。

☆2.我们在源码上同时也没有看到组间的2倍的关系,实际在对每组的平滑因子都是一样的,2倍的关系是由于在降采样的过程中产生的,第二层的第一张图是由第一层的平滑因子为2σ的图像上(即倒数第三张)降采样得到,此时图像平滑因子为σ,所以继续采用以上的平滑因子。但在原图上看,形成了全部的空间尺度。

☆3.每组(octave)有S+3层图像,是由于在DOG尺度空间上寻找极值点的方法是在一个立方体内进行,即上下层比较,所以不在DOG空间的第一层与最后一层寻找,即DOG需要S+2层图像,由于DOG尺度空间是由高斯金字塔相邻图像相减得到,即每组需要S+3层图像。

/*
  Builds Gaussian scale space pyramid from an image
  @param base base image of the pyramid
  @param octvs number of octaves of scale space
  @param intvls number of intervals per octave
  @param sigma amount of Gaussian smoothing per octave

  @return Returns a Gaussian scale space pyramid as an octvs x (intvls + 3)
    array 

给定组数(octave)和层数(intvls),以及初始平滑系数sigma,构建高斯金字塔
返回的每组中层数为intvls+3
*/
static IplImage*** build_gauss_pyr( IplImage* base, int octvs,
			     int intvls, double sigma )
{
  IplImage*** gauss_pyr;
  const int _intvls = intvls;             // lowe 采用了每组层数(intvls)为 3
 // double  sig_total, sig_prev;
	 double  k;
  int i, o;
  double *sig = (double *)malloc(sizeof(int)*(_intvls+3));  //存储每组的高斯平滑因子,每组对应的平滑因子都相同

  gauss_pyr = calloc( octvs, sizeof( IplImage** ) );
  for( i = 0; i < octvs; i++ )
    gauss_pyr[i] = calloc( intvls + 3, sizeof( IplImage *) );

  /*
    precompute Gaussian sigmas using the following formula:

    \sigma_{total}^2 = \sigma_{i}^2 + \sigma_{i-1}^2

    sig[i] is the incremental sigma value needed to compute
    the actual sigma of level i. Keeping track of incremental
    sigmas vs. total sigmas keeps the gaussian kernel small.
  */
  k = pow( 2.0, 1.0 / intvls );                 // k = 2^(1/S)
  sig[0] = sigma;
  sig[1] = sigma * sqrt( k*k- 1 );
  for (i = 2; i < intvls + 3; i++)
      sig[i] = sig[i-1] * k;                       //每组对应的平滑因子为 σ ,  sqrt(k^2 -1)* σ, sqrt(k^2 -1)* kσ , ...

  for( o = 0; o < octvs; o++ )
    for( i = 0; i < intvls + 3; i++ )
      {
	if( o == 0  &&  i == 0 )
	  gauss_pyr[o][i] = cvCloneImage(base);                       //第一组,第一层为原图

	/* base of new octvave is halved image from end of previous octave */
	else if( i == 0 )
	  gauss_pyr[o][i] = downsample( gauss_pyr[o-1][intvls] );  //第一层图像由上一层倒数第三张隔点采样得到

	/* blur the current octave's last image to create the next one */
	else
	  {
	    gauss_pyr[o][i] = cvCreateImage( cvGetSize(gauss_pyr[o][i-1]),
					     IPL_DEPTH_32F, 1 );
	    cvSmooth( gauss_pyr[o][i-1], gauss_pyr[o][i],
		      CV_GAUSSIAN, 0, 0, sig[i], sig[i] );                       //高斯平滑
	  }
      }

  return gauss_pyr;
}

3.生成DOG尺度空间

Lindeberg发现高斯差分函数(Difference of Gaussian ,简称DOG算子)与尺度归一化的高斯拉普拉斯函数非常近似,且

差分近似:

lowe建议采用相邻尺度的图像相减来获得高斯差分图像D(x,y,σ)来近似LOG来进行极值检测。

D(x,y,σ) = G(x,y,kσ)*I(x,y)-G(x,y,σ)*I(x,y)

=L(x,y,kσ) - L(x,y,σ)

对高斯金字塔的每组内相邻图像相减,形成DOG尺度空间,这时DOG中每组有S+2层图像

static IplImage*** build_dog_pyr( IplImage*** gauss_pyr, int octvs, int intvls )
{
  IplImage*** dog_pyr;
  int i, o;

  dog_pyr = calloc( octvs, sizeof( IplImage** ) );
  for( i = 0; i < octvs; i++ )
    dog_pyr[i] = calloc( intvls + 2, sizeof(IplImage*) );

  for( o = 0; o < octvs; o++ )
    for( i = 0; i < intvls + 2; i++ )
      {
	dog_pyr[o][i] = cvCreateImage( cvGetSize(gauss_pyr[o][i]),
				       IPL_DEPTH_32F, 1 );
	cvSub( gauss_pyr[o][i+1], gauss_pyr[o][i], dog_pyr[o][i], NULL );   //相邻两层图像相减,结果放在dog_pyr数组内
      }

  return dog_pyr;
}

4.关键点搜索与定位

在DOG尺度空间上,首先寻找极值点,插值处理,找到准确的极值点坐标,再排除不稳定的特征点(边界点)

/*
  Detects features at extrema in DoG scale space.  Bad features are discarded
  based on contrast and ratio of principal curvatures.

  @return Returns an array of detected features whose scales, orientations,
    and descriptors are yet to be determined.
*/
static CvSeq* scale_space_extrema( IplImage*** dog_pyr, int octvs, int intvls,
				   double contr_thr, int curv_thr,
				   CvMemStorage* storage )
{
  CvSeq* features;
  double prelim_contr_thr = 0.5 * contr_thr / intvls; //极值比较前的阈值处理
  struct feature* feat;
  struct detection_data* ddata;
  int o, i, r, c;

  features = cvCreateSeq( 0, sizeof(CvSeq), sizeof(struct feature), storage );
  for( o = 0; o < octvs; o++ )                     //对DOG尺度空间上,遍历从第二层图像开始到倒数第二层图像上,每个像素点
    for( i = 1; i <= intvls; i++ )
      for(r = SIFT_IMG_BORDER; r < dog_pyr[o][0]->height-SIFT_IMG_BORDER; r++)
	for(c = SIFT_IMG_BORDER; c < dog_pyr[o][0]->width-SIFT_IMG_BORDER; c++)
	  /* perform preliminary check on contrast */
	  if( ABS( pixval32f( dog_pyr[o][i], r, c ) ) > prelim_contr_thr )    // 排除像素值小于阈值prelim_contr_thr的点,提高稳定性
	    if( is_extremum( dog_pyr, o, i, r, c ) )             //与周围26个像素值比较,是否极大值或者极小值点
	      {
		feat = interp_extremum(dog_pyr, o, i, r, c, intvls, contr_thr); //插值处理,找到准确的特征点坐标
		if( feat )
		  {
		    ddata = feat_detection_data( feat );
		    if( ! is_too_edge_like( dog_pyr[ddata->octv][ddata->intvl],   //根据Hessian矩阵 判断是否为边缘上的点
					    ddata->r, ddata->c, curv_thr ) )
		      {
			cvSeqPush( features, feat );          //是特征点进入特征点序列
		      }
		    else
		      free( ddata );
		    free( feat );
		  }
	      }

  return features;
}

4.1

寻找极值点

每一个采样点都要与它相邻的像素点比较,看是否比它在图像域或尺度域的所有点的值大或者小。与它同尺度的相邻像素点有8个,上下相邻尺度的点共有2×9=18,共有26个像素点。也就在一个3×3的立方体内进行。搜索的过程是第二层开始到倒数第二层结束,共检测了octave组,每组S层。

/*
  Determines whether a pixel is a scale-space extremum by comparing it to it's
  3x3x3 pixel neighborhood.
*/
static int is_extremum( IplImage*** dog_pyr, int octv, int intvl, int r, int c )
{
  double val = pixval32f( dog_pyr[octv][intvl], r, c );
  int i, j, k;

  /* check for maximum */
  if( val > 0 )
    {
      for( i = -1; i <= 1; i++ )
	for( j = -1; j <= 1; j++ )
	  for( k = -1; k <= 1; k++ )
	    if( val < pixval32f( dog_pyr[octv][intvl+i], r + j, c + k ) )
	      return 0;
    }

  /* check for minimum */
  else
    {
      for( i = -1; i <= 1; i++ )
	for( j = -1; j <= 1; j++ )
	  for( k = -1; k <= 1; k++ )
	    if( val > pixval32f( dog_pyr[octv][intvl+i], r + j, c + k ) )
	      return 0;
    }

  return 1;
}

4.2

准确定位特征点

以上的极值点搜索是在离散空间进行的,极值点不真正意义上的极值点。通过对空间尺度函数拟合,可以得到亚像素级像素点坐标。

尺度空间的Taylor展开式:

,其中

求导并令其为0,得到亚像素级:

对应的函数值为:

是一个三维矢量,矢量在任何一个方向上的偏移量大于0.5时,意味着已经偏离了原像素点,这样的特征坐标位置需要更新或者继续插值计算。算法实现过程中,为了保证插值能够收敛,设置了最大插值次数(lowe 设置了5次)。同时当时(本文阈值采用了0.04/S)
,特征点才被保留,因为响应值过小的点,容易受噪声的干扰而不稳定。

对离散空间进行函数拟合(插值):

/*
  Performs one step of extremum interpolation.  Based on Eqn. (3) in Lowe's
  paper.

  r,c 为特征点位置,xi,xr,xc,保存三个方向的偏移量
*/

static void interp_step( IplImage*** dog_pyr, int octv, int intvl, int r, int c,
			 double* xi, double* xr, double* xc )
{
  CvMat* dD, * H, * H_inv, X;
  double x[3] = { 0 };

  dD = deriv_3D( dog_pyr, octv, intvl, r, c );      //计算三个方向的梯度
  H = hessian_3D( dog_pyr, octv, intvl, r, c );    // 计算3维空间的hessian矩阵
  H_inv = cvCreateMat( 3, 3, CV_64FC1 );
  cvInvert( H, H_inv, CV_SVD );           //计算逆矩阵
  cvInitMatHeader( &X, 3, 1, CV_64FC1, x, CV_AUTOSTEP );
  cvGEMM( H_inv, dD, -1, NULL, 0, &X, 0 );   //广义乘法 

  cvReleaseMat( &dD );
  cvReleaseMat( &H );
  cvReleaseMat( &H_inv );

  *xi = x[2];
  *xr = x[1];
  *xc = x[0];
}
/*
  Interpolates a scale-space extremum's location and scale to subpixel
  accuracy to form an image feature.
*/
static struct feature* interp_extremum( IplImage*** dog_pyr, int octv,          //通过拟合求取准确的特征点位置
					int intvl, int r, int c, int intvls,
					double contr_thr )
{
  struct feature* feat;
  struct detection_data* ddata;
  double xi, xr, xc, contr;
  int i = 0;

  while( i < SIFT_MAX_INTERP_STEPS )   //在最大迭代次数范围内进行
    {
      interp_step( dog_pyr, octv, intvl, r, c, &xi, &xr, &xc );          //插值后得到的三个方向的偏移量(xi,xr,xc)
      if( ABS( xi ) < 0.5  &&  ABS( xr ) < 0.5  &&  ABS( xc ) < 0.5 )
	break;

      c += cvRound( xc );    //更新位置
      r += cvRound( xr );
      intvl += cvRound( xi );

      if( intvl < 1  ||
	  intvl > intvls  ||
	  c < SIFT_IMG_BORDER  ||
	  r < SIFT_IMG_BORDER  ||
	  c >= dog_pyr[octv][0]->width - SIFT_IMG_BORDER  ||
	  r >= dog_pyr[octv][0]->height - SIFT_IMG_BORDER )
	{
	  return NULL;
	}

      i++;
    }

  /* ensure convergence of interpolation */
  if( i >= SIFT_MAX_INTERP_STEPS )
    return NULL;

  contr = interp_contr( dog_pyr, octv, intvl, r, c, xi, xr, xc );     //计算插值后对应的函数值
  if( ABS( contr ) < contr_thr / intvls )   //小于阈值(0.04/S)的点,则丢弃
    return NULL;

  feat = new_feature();
  ddata = feat_detection_data( feat );
  feat->img_pt.x = feat->x = ( c + xc ) * pow( 2.0, octv );       // 计算特征点根据降采样的次数对应于原图中位置
  feat->img_pt.y = feat->y = ( r + xr ) * pow( 2.0, octv );
  ddata->r = r;                  // 在本尺度内的坐标位置
  ddata->c = c;
  ddata->octv = octv;                 //组信息
  ddata->intvl = intvl;                 // 层信息
  ddata->subintvl = xi;              // 层方向的偏移量

  return feat;
}

待续...

5.计算不同尺度下特征点对应原图的位置

6.为特征点分配方向角

7.构建特征描述子

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-08-01 04:23:05

【特征匹配】SIFT原理与C源码剖析的相关文章

spring源码剖析(六)AOP实现原理剖析

Spring的AOP实现原理,酝酿了一些日子,写博客之前信心不是很足,所以重新阅读了一边AOP的实现核心代码,而且又从网上找了一些Spring Aop剖析的例子,但是发现挂羊头买狗肉的太多,标题高大上,内容却大部分都是比较浅显的一些介绍,可能也是由于比较少人阅读这部分的核心代码逻辑把,然后写这部分介绍的人估计也是少之又少,不过说实话,Spring Aop的核心原理实现介绍确实不太好写,里面涉及的类之间的调用还是蛮多的,关系图画的太细的画也很难画,而且最重要的一点就是,如果对AOP的概念以及spr

SpringMVC关于json、xml自动转换的原理研究[附带源码分析 --转

SpringMVC关于json.xml自动转换的原理研究[附带源码分析] 原文地址:http://www.cnblogs.com/fangjian0423/p/springMVC-xml-json-convert.html 目录 前言 现象 源码分析 实例讲解 关于配置 总结 参考资料 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:http://www.cnblogs.com/fangjian0423/p/springMVC-in

Python源码剖析笔记3-Python执行原理初探

Python源码剖析笔记3-Python执行原理初探 本文简书地址:http://www.jianshu.com/p/03af86845c95 之前写了几篇源码剖析笔记,然而慢慢觉得没有从一个宏观的角度理解python执行原理的话,从底向上分析未免太容易让人疑惑,不如先从宏观上对python执行原理有了一个基本了解,再慢慢探究细节,这样也许会好很多.这也是最近这么久没有更新了笔记了,一直在看源码剖析书籍和源码,希望能够从一个宏观层面理清python执行原理.人说读书从薄读厚,再从厚读薄方是理解了

老李推荐:第6章5节《MonkeyRunner源码剖析》Monkey原理分析-事件源-事件源概览-事件

老李推荐:第6章5节<MonkeyRunner源码剖析>Monkey原理分析-事件源-事件源概览-事件 从网络过来的命令字串需要解析翻译出来,有些命令会在翻译好后直接执行然后返回,但有一大部分命令在翻译后需要转换成对应的事件,然后放入到命令队列里面等待执行.Monkey在取出一个事件执行的时候主要是执行其injectEvent方法来注入事件,而注入事件根据是否需要往系统注入事件分为两种: 需要通过系统服务往系统注入事件:如MonkeyKeyEvent事件会通过系统的InputManager往系

老李推荐:第6章3节《MonkeyRunner源码剖析》Monkey原理分析-事件源-事件源概览-命令翻译类

老李推荐:第6章3节<MonkeyRunner源码剖析>Monkey原理分析-事件源-事件源概览-命令翻译类 每个来自网络的字串命令都需要进行解析执行,只是有些是在解析的过程中直接执行了事,而有些是需要在解析后创建相应的事件类实例并添加到命令队列里面排队执行.负责这部分工作的就是命令翻译类.那么我们往下还是继续在MonkeySourceNetwork这个范畴中MonkeyCommand类是怎么一回事: 图6-3-1 MonkeyCommand族谱 图中间的MonkeyCommand是一个接口,

SpringMVC源码剖析5:消息转换器HttpMessageConverter与@ResponseBody注解

转自 SpringMVC关于json.xml自动转换的原理研究[附带源码分析] 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Spring源码解析 https://blog.csdn.net/column/details/21851.html 部分代码会放在我的的Github:https://github.com/h2pl/ 目录 前言 现象 源码分析 实例讲解 关于配置 总结 参考资料 前言 SpringMVC是目前主流的Web MVC

ffmpeg/ffplay源码剖析笔记&lt;转&gt;

转载:http://www.cnblogs.com/azraelly/ http://www.cnblogs.com/azraelly/archive/2013/01/18/2865858.html 内容摘自<ffmpeg/ffplay源码剖析> 1.播放器一般原理 可以直观的看到播放这个媒体文件的基本模块(filter),七个模块按广度顺序:读文件模块(source filter),解复用模块(Demux filter),视/音频解码模块(Decode filter),颜色空间转换模块(Co

【Redis源码剖析】 - Redis内置数据结构值压缩字典zipmap

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51111230 今天为大家带来Redis中zipmap数据结构的分析,该结构定义在zipmap.h和zipmap.c文件中.我把zipmap称作"压缩字典"(不知道这样称呼正不正确)是因为zipmap利用字符串实现了一个简单的hash_table结构,又通过固定的字节表示节省空间.zipmap和前面介绍的ziplist结构十分类似,我们可以对比地进行学习: Redis中

一个Python开源项目-腾讯哈勃沙箱源码剖析(上)

前言 2019年来了,2020年还会远吗? 请把下一年的年终奖发一下,谢谢... 回顾逝去的2018年,最大的改变是从一名学生变成了一位工作者,不敢说自己多么的职业化,但是正在努力往那个方向走. 以前想的更多是尝试,现在需要考虑的更多是落地.学校和公司还是有很大的不一样,学到了很多东西. 2019年了,新年新气象,给大家宣布一下"七夜安全博客"今年的规划: 1. 2019年不再接任何商业广告(文末腾讯广告除外),纯粹输出安全技术干货. 2. 2019年每周至少两篇原创图文,也就是说每个