这两天在做关于车牌识别的实验,用了几种方式:
1.车牌颜色分布(HSV空间,YCrCb空间的没有颜色分布图谱,无法实验);利用HSV的H通道,效果一般,受环境影响大。
转载自:http://blog.csdn.net/sangni007/article/details/7444470
#include "highgui.h" #include "cv.h" #include <stdio.h> #include <math.h> #include <string> #include<iostream> using namespace std; CvPoint Point; IplImage* img=0; // skin region location using rgb limitation void SkinRGB(IplImage* rgb,IplImage* _dst) { assert(rgb->nChannels==3&& _dst->nChannels==3); static const int R=2; static const int G=1; static const int B=0; IplImage* dst=cvCreateImage(cvGetSize(_dst),8,3); cvZero(dst); for (int h=0;h<rgb->height;h++) { unsigned char* prgb=(unsigned char*)rgb->imageData+h*rgb->widthStep; unsigned char* pdst=(unsigned char*)dst->imageData+h*dst->widthStep; for (int w=0;w<rgb->width;w++) { if ((prgb[R]>95 && prgb[G]>40 && prgb[B]>20 && prgb[R]-prgb[B]>15 && prgb[R]-prgb[G]>15/*&& !(prgb[R]>170&&prgb[G]>170&&prgb[B]>170)*/)||//uniform illumination (prgb[R]>200 && prgb[G]>210 && prgb[B]>170 && abs(prgb[R]-prgb[B])<=15 && prgb[R]>prgb[B]&& prgb[G]>prgb[B])//lateral illumination ) { memcpy(pdst,prgb,3); } prgb+=3; pdst+=3; } } cvCopyImage(dst,_dst); cvReleaseImage(&dst); } // skin detection in rg space void cvSkinRG(IplImage* rgb,IplImage* gray) { assert(rgb->nChannels==3&&gray->nChannels==1); const int R=2; const int G=1; const int B=0; double Aup=-1.8423; double Bup=1.5294; double Cup=0.0422; double Adown=-0.7279; double Bdown=0.6066; double Cdown=0.1766; for (int h=0;h<rgb->height;h++) { unsigned char* pGray=(unsigned char*)gray->imageData+h*gray->widthStep; unsigned char* pRGB=(unsigned char* )rgb->imageData+h*rgb->widthStep; for (int w=0;w<rgb->width;w++) { int s=pRGB[R]+pRGB[G]+pRGB[B]; double r=(double)pRGB[R]/s; double g=(double)pRGB[G]/s; double Gup=Aup*r*r+Bup*r+Cup; double Gdown=Adown*r*r+Bdown*r+Cdown; double Wr=(r-0.33)*(r-0.33)+(g-0.33)*(g-0.33); if (g<Gup && g>Gdown && Wr>0.004) { *pGray=255; } else { *pGray=0; } pGray++; pRGB+=3; } } } // implementation of otsu algorithm // author: onezeros#yahoo.cn // reference: Rafael C. Gonzalez. Digital Image Processing Using MATLAB void cvThresholdOtsu(IplImage* src, IplImage* dst) { int height=src->height; int width=src->width; //histogram float histogram[256]={0}; for(int i=0;i<height;i++) { unsigned char* p=(unsigned char*)src->imageData+src->widthStep*i; for(int j=0;j<width;j++) { histogram[*p++]++; } } //normalize histogram int size=height*width; for(int i=0;i<256;i++) { histogram[i]=histogram[i]/size; } //average pixel value float avgValue=0; for(int i=0;i<256;i++) { avgValue+=i*histogram[i]; } int threshold; float maxVariance=0; float w=0,u=0; for(int i=0;i<256;i++) { w+=histogram[i]; u+=i*histogram[i]; float t=avgValue*w-u; float variance=t*t/(w*(1-w)); if(variance>maxVariance) { maxVariance=variance; threshold=i; } } cvThreshold(src,dst,threshold,255,CV_THRESH_BINARY); } void cvSkinOtsu(IplImage* src, IplImage* dst) { assert(dst->nChannels==1&& src->nChannels==3); IplImage* ycrcb=cvCreateImage(cvGetSize(src),8,3); IplImage* cr=cvCreateImage(cvGetSize(src),8,1); cvCvtColor(src,ycrcb,CV_BGR2YCrCb); cvSplit(ycrcb,0,cr,0,0); cvThresholdOtsu(cr,cr); cvCopyImage(cr,dst); cvReleaseImage(&cr); cvReleaseImage(&ycrcb); } void cvSkinYUV(IplImage* src,IplImage* dst) { IplImage* ycrcb=cvCreateImage(cvGetSize(src),8,3); //IplImage* cr=cvCreateImage(cvGetSize(src),8,1); //IplImage* cb=cvCreateImage(cvGetSize(src),8,1); cvCvtColor(src,ycrcb,CV_BGR2YCrCb); //cvSplit(ycrcb,0,cr,cb,0); static const int Cb=2; static const int Cr=1; static const int Y=0; //IplImage* dst=cvCreateImage(cvGetSize(_dst),8,3); cvZero(dst); for (int h=0;h<src->height;h++) { unsigned char* pycrcb=(unsigned char*)ycrcb->imageData+h*ycrcb->widthStep; unsigned char* psrc=(unsigned char*)src->imageData+h*src->widthStep; unsigned char* pdst=(unsigned char*)dst->imageData+h*dst->widthStep; for (int w=0;w<src->width;w++) { if ((pycrcb[Cr]<=126||pycrcb[Cr]>=130)&&(pycrcb[Cb]<=126||pycrcb[Cb]>=130)) { memcpy(pdst,psrc,3); } pycrcb+=3; psrc+=3; pdst+=3; } } //cvCopyImage(dst,_dst); //cvReleaseImage(&dst); } void cvSkinHSV(IplImage* src,IplImage* dst) { IplImage* hsv=cvCreateImage(cvGetSize(src),8,3); //IplImage* cr=cvCreateImage(cvGetSize(src),8,1); //IplImage* cb=cvCreateImage(cvGetSize(src),8,1); cvCvtColor(src,hsv,CV_BGR2HSV); //cvSplit(ycrcb,0,cr,cb,0); static const int V=2; static const int S=1; static const int H=0; //IplImage* dst=cvCreateImage(cvGetSize(_dst),8,3); cvZero(dst); for (int h=0;h<src->height;h++) { unsigned char* phsv=(unsigned char*)hsv->imageData+h*hsv->widthStep; unsigned char* psrc=(unsigned char*)src->imageData+h*src->widthStep; unsigned char* pdst=(unsigned char*)dst->imageData+h*dst->widthStep; for (int w=0;w<src->width;w++) { if (phsv[H]>=90&&phsv[H]<=135) { memcpy(pdst,psrc,3); } phsv+=3; psrc+=3; pdst+=3; } } //cvCopyImage(dst,_dst); //cvReleaseImage(&dst); } void on_mouse(int event,int x,int y,int flags,void* param ) { switch(event) { case CV_EVENT_LBUTTONUP: { Point=cvPoint(x,y); } cvCircle(img,Point,1,CV_RGB(255,0,0),1); CvScalar HSV=cvGet2D(img,x,y); cout<<"H:"<<HSV.val[0]<<"\t S:"<<HSV.val[1]<<"\t V:"<<HSV.val[2]<<endl; break; } //printf("( %d, %d) ",x,y); //printf("The Event is : %d ",event); //printf("The flags is : %d ",flags); //printf("The param is : %d\n",param); } int main() { IplImage* img0= cvLoadImage("D:/image/car/00.jpg"); //随便放一张jpg图片在D盘或另行设置目录 img=cvCreateImage(cvSize(400,300),8,3); cvResize(img0,img); IplImage* dstRGB=cvCreateImage(cvGetSize(img),8,3); IplImage* dstRG=cvCreateImage(cvGetSize(img),8,1); IplImage* dst_crotsu=cvCreateImage(cvGetSize(img),8,1); IplImage* dst_YUV=cvCreateImage(cvGetSize(img),8,3); IplImage* dst_HSV=cvCreateImage(cvGetSize(img),8,3); cvNamedWindow("inputimage", CV_WINDOW_AUTOSIZE); cvShowImage("inputimage", img); cvWaitKey(0); /* SkinRGB(img,dstRGB); cvNamedWindow("outputimage1", CV_WINDOW_AUTOSIZE); cvShowImage("outputimage1", dstRGB); cvWaitKey(0); cvSkinRG(img,dstRG); cvNamedWindow("outputimage2", CV_WINDOW_AUTOSIZE); cvShowImage("outputimage2", dstRG); cvWaitKey(0); cvSkinOtsu(img,dst_crotsu); cvNamedWindow("outputimage3", CV_WINDOW_AUTOSIZE); cvShowImage("outputimage3", dst_crotsu); cvWaitKey(0); cvSkinYUV(img,dst_YUV); cvNamedWindow("outputimage4", CV_WINDOW_AUTOSIZE); cvShowImage("outputimage4", dst_YUV); //cvSaveImage("D:/skin04.jpg",dst_YUV); cvWaitKey(0); */ cvSkinHSV(img,dst_HSV); cvNamedWindow("outputimage5", CV_WINDOW_AUTOSIZE); cvShowImage("outputimage5", dst_HSV); cvSaveImage("D:/image/car/car00.jpg",dst_HSV); cvWaitKey(0); return 0; }
2.Canny+Hough;效果也不好,但学习了hough变换的有关内容。
#include <cv.h> #include <highgui.h> #include <math.h> int main(int argc, char** argv) { const char* filename = argc >= 2 ? argv[1] : "D:/image/car/car04.jpg"; IplImage* src = cvLoadImage( filename, 0 ); cvDilate(src,src); IplImage* dst; IplImage* color_dst; CvMemStorage* storage = cvCreateMemStorage(0); CvSeq* lines = 0; int i; if( !src ) return -1; dst = cvCreateImage( cvGetSize(src), 8, 1 ); color_dst = cvCreateImage( cvGetSize(src), 8, 3 ); cvCanny( src, dst, 50, 150, 3 ); cvCvtColor( dst, color_dst, CV_GRAY2BGR ); #if 0 lines = cvHoughLines2( dst, storage, CV_HOUGH_STANDARD, 1, CV_PI/180, 100, 0, 0 ); for( i = 0; i < MIN(lines->total,100); i++ ) { float* line = (float*)cvGetSeqElem(lines,i); float rho = line[0]; float theta = line[1]; CvPoint pt1, pt2; double a = cos(theta), b = sin(theta); double x0 = a*rho, y0 = b*rho; pt1.x = cvRound(x0 + 1000*(-b)); pt1.y = cvRound(y0 + 1000*(a)); pt2.x = cvRound(x0 - 1000*(-b)); pt2.y = cvRound(y0 - 1000*(a)); cvLine( color_dst, pt1, pt2, CV_RGB(255,0,0), 3, CV_AA, 0 ); } #else lines = cvHoughLines2( dst, storage, CV_HOUGH_PROBABILISTIC, 1, CV_PI/180, 50, 5, 3 ); for( i = 0; i < lines->total; i++ ) { CvPoint* line = (CvPoint*)cvGetSeqElem(lines,i); cvLine( color_dst, line[0], line[1], CV_RGB(255,0,0), 3, CV_AA, 0 ); } //#endif cvNamedWindow( "Source", 1 ); cvShowImage( "Source", src ); cvNamedWindow( "Hough", 1 ); cvShowImage( "Hough", color_dst ); cvWaitKey(0); return 0; }
3.Coutour检测;效果勉强。
#include "cv.h" #include "highgui.h" #include <cxcore.h> #include <stdio.h> int BinarizeImageByOTSU (IplImage * src) { assert(src != NULL); //get the ROI CvRect rect = cvGetImageROI(src); //information of the source image int x = rect.x; int y = rect.y; int width = rect.width; int height = rect.height; int ws = src->widthStep; int thresholdValue=1;//阈值 int ihist [256] ; // 图像直方图, 256个点 int i, j, k,n, n1, n2, Color=0; double m1, m2, sum, csum, fmax, sb; memset (ihist, 0, sizeof (ihist)) ; // 对直方图置 零... for (i=y;i< y+height;i++) // 生成直方图 { int mul = i*ws; for (j=x;j<x+width;j++) { //Color=Point (i,j) ; Color = (int)(unsigned char)*(src->imageData + mul+ j); ihist [Color] +=1; } } sum=csum=0.0; n=0; for (k = 0; k <= 255; k++) { sum+= (double) k* (double) ihist [k] ; // x*f (x) 质量矩 n +=ihist [k]; //f (x) 质量 } // do the otsu global thresholding method fmax = - 1.0; n1 = 0; for (k=0;k<255;k++) { n1+=ihist [k] ; if (! n1) { continue; } n2=n- n1; if (n2==0) { break; } csum+= (double) k*ihist [k] ; m1=csum/ n1; m2= (sum- csum) /n2; sb = ( double) n1* ( double) n2* ( m1 - m2) * (m1- m2) ; if (sb>fmax) { fmax=sb; thresholdValue=k; } } //binarize the image cvThreshold( src, src ,thresholdValue, 255, CV_THRESH_BINARY ); return 0; } int main( int argc, char* argv[]) { IplImage* src; if((src=cvLoadImage("D:/image/car/05sobel.jpg", 0)))//载入图像 { //为轮廓显示图像申请空间,3通道图像,以便用彩色显示 IplImage* dst = cvCreateImage( cvGetSize(src), 8, 3); //创建内存块,将该块设置成默认值,当前默认大小为64k CvMemStorage* storage = cvCreateMemStorage(0); //可动态增长元素序列 CvSeq* contour = 0; //对图像进行自适二值化 BinarizeImageByOTSU(src); //图像膨胀 cvDilate(src,src); //图像腐蚀 cvErode(src,src); //显示源图像的二值图 cvNamedWindow( "Source", 1 ); cvShowImage( "Source", src ); //在二值图像中寻找轮廓 cvFindContours( src, storage, &contour, sizeof(CvContour), CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE ); cvZero( dst );//清空数组 cvCvtColor(src,dst,CV_GRAY2BGR); //目标轮廓最小下限 int mix_area = 2500; //目标轮廓最大上限 int max_area = 3500; //可存放在1-,2-,3-,4-TUPLE类型的捆绑数据的容器 CvScalar color = CV_RGB( 255, 0, 0); //在图像中绘制外部和内部的轮廓 for( ; contour != 0; contour = contour->h_next) { //取得轮廓的最小矩形 CvRect aRect = cvBoundingRect( contour, 1 ); //取得矩形的面积 int tmparea=aRect.height*aRect.height; //用车牌的形态做判断 if (((double)aRect.width/(double)aRect.height>3) && ((double)aRect.width/(double)aRect.height<6)) { cvRectangle(dst,cvPoint(aRect.x,aRect.y),cvPoint(aRect.x+aRect.width ,aRect.y+aRect.height),color,2); //cvDrawContours( dst, contour, color, color, -1, 1, 8 ); } } cvNamedWindow( "Components", 1 ); cvShowImage( "Components", dst ); cvWaitKey(0); cvDestroyWindow("Components"); cvReleaseImage(&dst); cvDestroyWindow("Source"); cvReleaseImage(&src); return 0; } return 1; }
4.Squares方式:Canny||Threshold+cvFindContours+cvApproxPoly;效果一般
#ifdef _CH_ #pragma package <opencv> #endif #ifndef _EiC #include "cv.h" #include "highgui.h" #include <stdio.h> #include <math.h> #include <string.h> #endif int thresh = 50; IplImage* img = 0; IplImage* img0 = 0; CvMemStorage* storage = cvCreateMemStorage(0); CvPoint pt[4]; const char* wndname = "Square Detection Demo"; // helper function: // finds a cosine of angle between vectors // from pt0->pt1 and from pt0->pt2 double angle( CvPoint* pt1, CvPoint* pt2, CvPoint* pt0 ) { double dx1 = pt1->x - pt0->x; double dy1 = pt1->y - pt0->y; double dx2 = pt2->x - pt0->x; double dy2 = pt2->y - pt0->y; return (dx1*dx2 + dy1*dy2)/sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10); } // returns sequence of squares detected on the image. //返回图像中的四边形序列 // the sequence is stored in the specified memory storage //序列存储在特定的storage中 CvSeq* findSquares4( IplImage* img, CvMemStorage* storage ) { CvSeq* contours; int i, c, l, N = 11; CvSize sz = cvSize( img->width & -2, img->height & -2 ); IplImage* timg = cvCloneImage( img ); // make a copy of input image复制输入图像 IplImage* gray = cvCreateImage( sz, 8, 1 ); IplImage* pyr = cvCreateImage( cvSize(sz.width/2, sz.height/2), 8, 3 );//尺度减小为1/2 IplImage* tgray; CvSeq* result; double s, t; // create empty sequence that will contain points - // 4 points per square (the square's vertices) //建立一个空序列存储每个四边形的四个顶点 CvSeq* squares = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvPoint), storage ); // select the maximum ROI in the image // with the width and height divisible by 2 //设定timg的ROI为最大值() cvSetImageROI( timg, cvRect( 0, 0, sz.width, sz.height )); // down-scale and upscale the image to filter out the noise //金字塔方式升和降来滤波去除噪声 //cvPyrDown( timg, pyr, 7 ); //cvPyrUp( pyr, timg, 7 ); tgray = cvCreateImage( sz, 8, 1 ); // find squares in every color plane of the image //寻找每个通道的四边形 for( c = 0; c < 3; c++ ) { // extract the c-th color plane //提取第c个通道 cvSetImageCOI( timg, c+1 ); cvCopy( timg, tgray, 0 ); // try several threshold levels //尝试每个阈值等级 for( l = 0; l < N; l++ ) { // hack: use Canny instead of zero threshold level. // Canny helps to catch squares with gradient shading //Canny代替零阈值,Canny通过梯度变化程度大来寻找四边形 if( l == 0 ) { // apply Canny. Take the upper threshold from slider // and set the lower to 0 (which forces edges merging) // l=0使用Canny cvCanny( tgray, gray,60, 180, 3 ); // // dilate canny output to remove potential // holes between edge segments cvDilate( gray, gray, 0, 1 ); } else { // apply threshold if l!=0: // tgray(x,y) = gray(x,y) < (l+1)*255/N ? 255 : 0 //cvThreshold( tgray, gray, (l+1)*255/N, 255, CV_THRESH_BINARY ); cvThreshold( tgray, gray, 50, 255, CV_THRESH_BINARY ); } // find contours and store them all as a list cvFindContours( gray, storage, &contours, sizeof(CvContour), CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE, cvPoint(0,0) ); // test each contour while( contours ) { // approximate contour with accuracy proportional // to the contour perimeter //用指定精度逼近多边形曲线 result = cvApproxPoly( contours, sizeof(CvContour), storage, CV_POLY_APPROX_DP, cvContourPerimeter(contours)*0.02, 0 ); // square contours should have 4 vertices after approximation // relatively large area (to filter out noisy contours) // and be convex. // Note: absolute value of an area is used because // area may be positive or negative - in accordance with the // contour orientation if( result->total == 4 && fabs(cvContourArea(result,CV_WHOLE_SEQ)) > 1000 && //cvContourArea计算整个轮廓或部分轮廓的面积 cvCheckContourConvexity(result) ) //CheckContourConvexity { s = 0; for( i = 0; i < 5; i++ ) { // find minimum angle between joint // edges (maximum of cosine) if( i >= 2 ) { t = fabs(angle( (CvPoint*)cvGetSeqElem( result, i ), (CvPoint*)cvGetSeqElem( result, i-2 ), (CvPoint*)cvGetSeqElem( result, i-1 ))); s = s > t ? s : t; } } // if cosines of all angles are small // (all angles are ~90 degree) then write quandrange // vertices to resultant sequence if( s < 0.3 ) for( i = 0; i < 4; i++ ) cvSeqPush( squares, (CvPoint*)cvGetSeqElem( result, i )); } // take the next contour contours = contours->h_next; } } } // release all the temporary images cvReleaseImage( &gray ); cvReleaseImage( &pyr ); cvReleaseImage( &tgray ); cvReleaseImage( &timg ); return squares; } // the function draws all the squares in the image void drawSquares( IplImage* img, CvSeq* squares ) { CvSeqReader reader; IplImage* cpy = cvCloneImage( img ); int i; // initialize reader of the sequence cvStartReadSeq( squares, &reader, 0 ); // read 4 sequence elements at a time (all vertices of a square) for( i = 0; i < squares->total; i += 4 ) { CvPoint* rect = pt; int count = 4; // read 4 vertices memcpy( pt, reader.ptr, squares->elem_size ); CV_NEXT_SEQ_ELEM( squares->elem_size, reader ); memcpy( pt + 1, reader.ptr, squares->elem_size ); CV_NEXT_SEQ_ELEM( squares->elem_size, reader ); memcpy( pt + 2, reader.ptr, squares->elem_size ); CV_NEXT_SEQ_ELEM( squares->elem_size, reader ); memcpy( pt + 3, reader.ptr, squares->elem_size ); CV_NEXT_SEQ_ELEM( squares->elem_size, reader ); // draw the square as a closed polyline cvPolyLine( cpy, &rect, &count, 1, 1, CV_RGB(0,255,0), 3, CV_AA, 0 ); } // show the resultant image cvShowImage( wndname, cpy ); cvReleaseImage( &cpy ); } void on_trackbar( int a ) { if( img ) drawSquares( img, findSquares4( img, storage ) ); } //char* names[] = { "D:/image/car/00.jpg", "D:/image/car/01.jpg", "D:/image/car/02.jpg", // "D:/image/car/03.jpg", "D:/image/car/04.jpg", "D:/image/car/05.jpg", 0 }; //char* names[] = { "D:/image/car/car00.jpg", "D:/image/car/car01.jpg", "D:/image/car/car02.jpg", // "D:/image/car/car03.jpg", "D:/image/car/car04.jpg", "D:/image/car/car05.jpg", 0 }; //char* names[] = { "D:/image/car/00sobel.jpg", "D:/image/car/01sobel.jpg", "D:/image/car/02sobel.jpg", // "D:/image/car/03sobel.jpg", "D:/image/car/04sobel.jpg", "D:/image/car/05sobel.jpg", 0 }; char* names[] = { "D:/image/car/06sobel_normal.jpg", "D:/image/car/0sobel_normal.jpg", "D:/image/car/08sobel_normal.jpg", "D:/image/car/09sobel_normal.jpg", "D:/image/car/10sobel_normal.jpg", "D:/image/car/11sobel_normal.jpg", "D:/image/car/12sobel_normal.jpg", "D:/image/car/13sobel_normal.jpg", "D:/image/car/14sobel_normal.jpg", "D:/image/car/15sobel_normal.jpg", "D:/image/car/16sobel_normal.jpg", "D:/image/car/17sobel_normal.jpg", "D:/image/car/18sobel_normal.jpg", "D:/image/car/19sobel_normal.jpg", "D:/image/car/20sobel_normal.jpg", "D:/image/car/21sobel_normal.jpg", "D:/image/car/22sobel_normal.jpg", "D:/image/car/23sobel_normal.jpg", "D:/image/car/00sobel_normal.jpg", "D:/image/car/01sobel_normal.jpg", "D:/image/car/02sobel_normal.jpg", "D:/image/car/03sobel_normal.jpg", "D:/image/car/04sobel_normal.jpg", "D:/image/car/05sobel_normal.jpg", 0 }; int main(int argc, char** argv) { int i, c; // create memory storage that will contain all the dynamic data storage = cvCreateMemStorage(0); for( i = 0; names[i] != 0; i++ ) { // load i-th image img0 = cvLoadImage( names[i], 1 ); if( !img0 ) { printf("Couldn't load %s/n", names[i] ); continue; } img = cvCloneImage( img0 ); // create window and a trackbar (slider) with parent "image" and set callback // (the slider regulates upper threshold, passed to Canny edge detector) cvNamedWindow( wndname,0 ); cvCreateTrackbar( "canny thresh", wndname, &thresh, 1000, on_trackbar ); // force the image processing on_trackbar(0); // wait for key. // Also the function cvWaitKey takes care of event processing c = cvWaitKey(0); // release both images cvReleaseImage( &img ); cvReleaseImage( &img0 ); // clear memory storage - reset free space position cvClearMemStorage( storage ); if( c == 27 ) break; } cvDestroyWindow( wndname ); return 0; } #ifdef _EiC main(1,"squares.c"); #endif
5.Sobel(横向求导,保留纵向纹理)+(颜色反向)+cvMorphologyEx(Close操作,IplConvKernel*(3x1)横向闭运算)+FindContours+cvBoundingRect+cvRectangle(满足一定条件)
正确率65% 主要由于没有加入仿射变换或变形