实验四
一、实验目的和要求
- 目的
- 了解图像变换的种类
- 掌握常见几何变换(平移、旋转、对称、投影、放缩)的方法。
- 了解插值的原因、目的及原理。
- 掌握线性插值、双线性插值的方法,并比较其效果。
- 要求
- 自己写c语言程序,实现所有操作,不得调用库。
二、实验内容和原理
内容: 对图像进行几何变换。
- 读入24位彩图
- 依次对图像进行平移、旋转、对称、投影和放缩变换
- 输出变换后的图像
原理:
对图像进行几何变换操作的原理是,对于即将生成的新图像中每个像素点$P’=(x’,y’)$ ,我们利用图像几何变换的逆变换求得其在原始图像中对应的点 $P=(x,y)=T(x’,y’)$ 。则$R(x’,y’)=R(x,y);G(x’,y’)=G(x,y);B(x’,y’)=B(x,y);$
很多情况下我们得到的 $(x,y)$ 并不是整数,这个时候我们需要选择一个方法来得到$(x,y) $应当对应的$RGB$值。这个时候就用到插值法。即利用$(x,y)$ 附近的一些像素点的$RGB$值来计算得到$(x’,y’)$的$RGB$值。
平移 $(dx,dy)$
$$ \left{ \begin{aligned} x = x’-dx\y = y’-dy \end{aligned} \right. $$
逆时针旋转 $\theta$
一些常量的定义:
$pw,ph (原图的宽度和高度)$
$nw,nh (新图的宽度和高度)$
$cx=pw/2 (原图中心点的横坐标)$
$cy=ph/2 (原图中心点的横坐标)$
$dx=(nw-pw)/2$
$dy=(nh-ph)/2$
定义两个中间变量:
$a=x’-dx-cx+cxcos\theta-cysin\theta$
$b=y’-dy-cy+cycos\theta+cxsin\theta$
则
$$ \left{ \begin{aligned} x = bsin\theta +acos\theta \y = bcos\theta-asin\theta \end{aligned} \right. $$
**投影 $\theta$ **
x轴方向 $$ \begin{cases} x = x’ -y’*tan\theta \y =y’ \end{cases} $$
y轴方向 $$ \begin{cases} x = x’ \y =y’ -x’*tan\theta \end{cases} $$
对称
x轴方向 $$ \begin{cases} x = pw -x’-1 \y =y’ \end{cases} $$
y轴方向 $$ \begin{cases} x = x’ \y =ph-y’-1 \end{cases} $$
放缩,比例为$(a,b)$
$$ \begin{cases} x = \frac{x’}{a} \y =\frac{y’}{b} \end{cases} $$
插值法
一些常量的定义:
$x,y \rightarrow (根据变换公式计算得到的坐标,可能为小数)$
$x_1= \lfloor x \rfloor $
$y_1= \lfloor y \rfloor $
$x_2= \lceil x \rceil $
$y_2= \lceil y \rceil $
$radio_{11}=x-x_1$
$radio_{12}=x_2-x$
$radio_{21}=y-y1$
$radio_{22}=y2-y$
(理论上 $radio_{11}=\frac{x-x_1}{x_2-x_1}$ ,由于$x_2-x_1=1$ 。其他$radio$同理)
$C\in {R,G,B} \rightarrow (原始图像的RGB值)$
$C’\in {R’,G’,B’} \rightarrow (处理后图像的RGB值)$
则:
$$C’(x’,y’)=radio_{21}*(radio_{11}*C(x_1,y_1)+radio_{12}C(x_1,y_2))\\qquad\qquad+ radio_{22}(radio_{11}*C(x_2,y_1)+radio_{12}*C(x_2,y_2))$$
上述公式即为双线性插值法。
其本质在于,利用像素点变换前的实际位置周围四个像素点的 $RGB$ 值的加权平均来计算新像素点的 $RGB$ 值。并且如果周围的像素点距离实际位置越远,其的值对实际的 $RGB$ 值影响越小。原理如下图所示。
三、源代码和分析
源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529/*
本代码的坐标系统:
左上角对应坐标原点,水平向右为x轴正向,垂直向下为y轴正向。
根据bmp文件的扫描顺序,这样的坐标系统刚好符合常规的坐标系。
*/
const double pi=3.14;
int ph,pw,psize,lsize,cx,cy;
typedef struct BMP_FILE_HEADER
{
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
}BMPFILEHEADER;
/* 位图信息头 */
typedef struct BMP_INFO
{
DWORD biSize; // 信息头的大小
DWORD biWidth; // 图像的宽度
DWORD biHeight; // 图像的高度
WORD biPlanes; // 图像的位面数
WORD biBitCount; // 每个像素的位数
DWORD biCompression; // 压缩类型
DWORD biSizeImage; // 图像的大小,以字节为单位
DWORD biXPelsPerMeter; // 水平分辨率
DWORD biYPelsPerMeter; // 垂直分辨率
DWORD biClrUsed; // 使用的色彩数
DWORD biClrImportant; // 重要的颜色数
} BMPINF; // 40 字节
BMPFILEHEADER thisBmpFileHeader; // 定义一个 BMP 文件头的结构体
BMPINF thisBmpInfo; // 定义一个 BMP 文件信息结构体
void readFileHead(FILE *fp); //fp指向的文件的文件头读入到thisBMPFileHeader中
void writeFileHead(FILE *fp); //thisBMPFileHeader写入到fp指向的文件的文件头中
void Translation(BYTE *p,int dx,int dy,int flag);//进行平移操作,dx,dy为坐标移动的增量。flag=0时,画布增大,flag=1时,画布不增大。
void Rotation(BYTE *p,double theta);//对图像旋转theta角度
void Rotation2(BYTE *p,double theta);//对图像旋转theta角度
void Shear(BYTE *p,double theta,int flag);//对图像投影theta角度
void Scale(BYTE *p,float a,float b);//对图像放缩变换,a,b分别表示横纵坐标方向的比例。
void Mirror(BYTE *p,int flag);//对图像进行对称变换,flag=0时x轴,flag=1时y轴
double GetRolX(int x,int y,double theta);
double GetRolY(int x,int y,double theta);
int GetRolHW(int *x,int *y,double theta);
int Interpolation(BYTE *p,double x,double y,int _);
void OutPutImg(BYTE *pOUT,int nw,int nh,char *name);
int main()
{
FILE *fp; // 定义一个文件指针
if((fp=fopen("in.bmp","rb"))==NULL)
{
printf("Can't open the file\n");
return 0;
}
printf("open the file\n");
readFileHead(fp);
ph=thisBmpInfo.biHeight;//高度
pw=thisBmpInfo.biWidth;//宽度
psize=pw*ph*3;//图片信息字节总数
lsize=pw*3;//每行信息字节总数
BYTE* p=(BYTE*)malloc(psize);
fread(p,psize,1,fp);
fclose(fp);
Translation(p,-200,-100,0);
Translation(p,-200,-100,1);
Rotation(p,pi/3.0);
Rotation2(p,pi/3.0);
Scale(p,1.2,0.8);
Shear(p,pi/6,0);
Shear(p,pi/6,1);
Mirror(p,0);
Mirror(p,1);
return 0;
}
void readFileHead(FILE *fp)
{
fread(&thisBmpFileHeader.bfType,sizeof(thisBmpFileHeader.bfType),1,fp);
fread(&thisBmpFileHeader.bfSize,sizeof(thisBmpFileHeader.bfSize),1,fp);
fread(&thisBmpFileHeader.bfReserved1,sizeof(thisBmpFileHeader.bfReserved1),1,fp);
fread(&thisBmpFileHeader.bfReserved2,sizeof(thisBmpFileHeader.bfReserved2),1,fp);
fread(&thisBmpFileHeader.bfOffBits,sizeof(thisBmpFileHeader.bfOffBits),1,fp);
fread(&thisBmpInfo.biSize, sizeof(thisBmpInfo.biSize), 1, fp);
fread(&thisBmpInfo.biWidth, sizeof(thisBmpInfo.biWidth), 1, fp);
fread(&thisBmpInfo.biHeight, sizeof(thisBmpInfo.biHeight), 1, fp);
fread(&thisBmpInfo.biPlanes, sizeof(thisBmpInfo.biPlanes), 1, fp);
fread(&thisBmpInfo.biBitCount, sizeof(thisBmpInfo.biBitCount), 1, fp);
fread(&thisBmpInfo.biCompression, sizeof(thisBmpInfo.biCompression), 1, fp);
fread(&thisBmpInfo.biSizeImage, sizeof(thisBmpInfo.biSizeImage), 1, fp);
fread(&thisBmpInfo.biXPelsPerMeter, sizeof(thisBmpInfo.biXPelsPerMeter), 1, fp);
fread(&thisBmpInfo.biYPelsPerMeter, sizeof(thisBmpInfo.biYPelsPerMeter), 1, fp);
fread(&thisBmpInfo.biClrUsed, sizeof(thisBmpInfo.biClrUsed), 1, fp);
fread(&thisBmpInfo.biClrImportant, sizeof(thisBmpInfo.biClrImportant), 1, fp);
}
void writeFileHead(FILE *fp)
{
// 写入位图文件头
fwrite(&thisBmpFileHeader.bfType,sizeof(thisBmpFileHeader.bfType),1,fp);
fwrite(&thisBmpFileHeader.bfSize,sizeof(thisBmpFileHeader.bfSize),1,fp);
fwrite(&thisBmpFileHeader.bfReserved1,sizeof(thisBmpFileHeader.bfReserved1),1,fp);
fwrite(&thisBmpFileHeader.bfReserved2,sizeof(thisBmpFileHeader.bfReserved2),1,fp);
fwrite(&thisBmpFileHeader.bfOffBits,sizeof(thisBmpFileHeader.bfOffBits),1,fp);
// 写入位图信息头
fwrite(&thisBmpInfo.biSize, sizeof(thisBmpInfo.biSize), 1, fp);
fwrite(&thisBmpInfo.biWidth, sizeof(thisBmpInfo.biWidth), 1, fp);
fwrite(&thisBmpInfo.biHeight, sizeof(thisBmpInfo.biHeight), 1, fp);
fwrite(&thisBmpInfo.biPlanes, sizeof(thisBmpInfo.biPlanes), 1, fp);
fwrite(&thisBmpInfo.biBitCount, sizeof(thisBmpInfo.biBitCount), 1, fp);
fwrite(&thisBmpInfo.biCompression, sizeof(thisBmpInfo.biCompression), 1, fp);
fwrite(&thisBmpInfo.biSizeImage, sizeof(thisBmpInfo.biSizeImage), 1, fp);
fwrite(&thisBmpInfo.biXPelsPerMeter, sizeof(thisBmpInfo.biXPelsPerMeter), 1, fp);
fwrite(&thisBmpInfo.biYPelsPerMeter, sizeof(thisBmpInfo.biYPelsPerMeter), 1, fp);
fwrite(&thisBmpInfo.biClrUsed, sizeof(thisBmpInfo.biClrUsed), 1, fp);
fwrite(&thisBmpInfo.biClrImportant, sizeof(thisBmpInfo.biClrImportant), 1, fp);
}
void Translation(BYTE *p,int dx,int dy,int flag)
{
if(flag!=1&&flag!=0)
{
printf("Translation Error! Please input a right value of flag!\n");
return;
}
if(dx%4) dx=(dx/4+1)*4;//保证x轴方向的偏移量为4的倍数
int i,j,tmp,n;
if(flag==1)
{
BYTE *pOUT=(BYTE*)malloc(psize);
for(i=0;i<ph;i++)
for(j=0;j<pw;j++)
{
tmp=lsize*i+j*3;
int tmpi=i-dy,tmpj=j-dx;
if(tmpi<0) tmpi+=ph;
if(tmpi>=ph) tmpi-=ph;
if(tmpj<0) tmpj+=pw;
if(tmpj>=pw) tmpj-=pw;
int Ntmp = tmpi*lsize+tmpj*3;
pOUT[tmp]=p[Ntmp];
pOUT[tmp+1]=p[Ntmp+1];
pOUT[tmp+2]=p[Ntmp+2];
}
OutPutImg(pOUT,pw,ph,"Translation1");
}
else
{
int nh = ph+abs(dy),nw=pw+abs(dx),nsize=nw*nh*3;
BYTE *pOUT=(BYTE*)malloc(nw*nh*3);
int oi[2],oj[2];
if(dx>=0)
{
oj[0]=dx;
oj[1]=nw-1;
}
if(dx<0)
{
oj[0]=0;
oj[1]=ph-1;
}
if(dy>=0)
{
oi[0]=dy;
oi[1]=nh-1;
}
if(dy<0)
{
oi[0]=0;
oi[1]=pw-1;
}
for(i=0;i<nh;i++)
for(j=0;j<nw;j++)
{
int Ntmp = i*nw*3+j*3;
if(i>=oi[0]&&i<=oi[1]&&j>=oj[0]&&j<=oj[1])
{
tmp=lsize*(i-oi[0])+(j-oj[0])*3;
pOUT[Ntmp]=p[tmp];
pOUT[Ntmp+1]=p[tmp+1];
pOUT[Ntmp+2]=p[tmp+2];
}
else
{
pOUT[Ntmp]=pOUT[Ntmp+1]=pOUT[Ntmp+2]=0;
}
}
OutPutImg(pOUT,nw,nh,"Translation0");
}
}
void Rotation(BYTE *p,double theta)//对图像旋转theta角度
{
int nh,nw;
GetRolHW(&nw,&nh,theta);
BYTE *pOUT=(BYTE*)malloc(nh*nw*3);
int dX=(nw-pw)/2,dY=(nh-ph)/2; // 由于图像放大,计算坐标后需要移动一定距离
int i,j,tmp;
for(i=0;i<nh;i++)
for(j=0;j<nw;j++)
{
int _;
tmp=i*nw*3+j*3;
for(_=0;_<3;_++)
pOUT[tmp+_]=0;
}
for(i=0;i<nh;i++)
for(j=0;j<nw;j++)
{
tmp=nw*i*3+j*3;
double a,b;
a=j-dX-cx+cx*cos(theta)-cy*sin(theta);
b=i-dY-cy+cy*cos(theta)+cx*sin(theta);
double x,y;
x=b*sin(theta)+a*cos(theta);
y=b*cos(theta)-a*sin(theta);
int _;
for(_=0;_<3;_++)
pOUT[tmp+_]=Interpolation(p,x,y,_);
}
int x=theta*180/pi;
char s1[100]="Rotation",s[20];
sprintf(s,"_%d_degree",x);
strcat(s1,s);
OutPutImg(pOUT,nw,nh,s1);
}
void Shear(BYTE *p,double theta,int flag)//对图像投影theta角度,flag==0 -> x轴投影,1->y轴
{
BYTE* pOUT;
char s1[100]="Shear",s[20];
int x=theta*180/pi;
if(flag!=1&&flag!=0)
{
printf("Shear Error! Please input a right value of flag!\n");
return;
}
int nw,nh,tmp,i,j,_;
if(flag==0)
{
sprintf(s,"_X_%d_degree",x);
nh=ph;
nw=pw+ph*tan(theta);
if(nw%4) nw=(nw/4+1)*4;
pOUT=(BYTE*)malloc(nw*nh*3);
for(i=0;i<nh;i++)
for(j=0;j<nw;j++)
for(_=0;_<3;_++)
pOUT[i*nw*3+j*3+_]=0;
for(i=0;i<nh;i++)
for(j=0;j<nw;j++)
{
double x=(double)j-tan(theta)*i,
y=(double)i;
tmp=i*nw*3+j*3;
int _;
for(_=0;_<3;_++)
pOUT[tmp+_]=Interpolation(p,x,y,_);
}
}
else
{
sprintf(s,"_Y_%d_degree",x);
nh=ph+pw*tan(theta);
nw=pw;
pOUT=(BYTE*)malloc(nw*nh*3);
for(i=0;i<nh;i++)
for(j=0;j<nw;j++)
for(_=0;_<3;_++)
pOUT[i*nw*3+j*3+_]=0;
for(i=0;i<nh;i++)
for(j=0;j<nw;j++)
{
double y=(double)i-tan(theta)*j,
x=(double)j;
tmp=i*nw*3+j*3;
int _;
for(_=0;_<3;_++)
pOUT[tmp+_]=Interpolation(p,x,y,_);
}
}
strcat(s1,s);
OutPutImg(pOUT,nw,nh,s1);
}
void Scale(BYTE *p,float a,float b)//对图像放缩变换,a,b分别表示横纵坐标方向的比例。
{
int nh=(int)(ph*b),
nw=(int)(pw*a);
if(nw%4) nw=(nw/4+1)*4;
BYTE *pOUT=(BYTE*)malloc(nh*nw*3);
int *flag=(int *)malloc(sizeof(int)*nw*nh);
memset(pOUT,0,nh*nw*3);
memset(flag,0,sizeof(int)*nh*nw);
int i,j,tmp;
for(i=0;i<nh;i++)
for(j=0;j<nw;j++)
{
tmp=i*nw*3+j*3;
double x=floor((j*1.0)/a),
y=floor((i*1.0)/b);
int _;
for(_=0;_<3;_++)
pOUT[tmp+_]=Interpolation(p,x,y,_);
}
OutPutImg(pOUT,nw,nh,"Scale");
}
void Mirror(BYTE *p,int flag)//对图像进行对称变换,flag=0时x轴,flag=1时y轴
{
BYTE *pOUT=(BYTE*)malloc(psize);
int i,j,tmp;
if(flag==0)
{
for(i=0;i<ph;i++)
for(j=0;j<pw;j++)
{
tmp=i*lsize+j*3;
pOUT[tmp]=p[i*lsize+(pw-j-1)*3];
pOUT[tmp+1]=p[i*lsize+(pw-j-1)*3+1];
pOUT[tmp+2]=p[i*lsize+(pw-j-1)*3+2];
}
FILE *fp=fopen("MirrorX.bmp","wb");
writeFileHead(fp);
fwrite(pOUT,psize,1,fp);
fclose(fp);
printf("MirrorX finised!\n");
}
if(flag==1)
{
for(j=0;j<pw;j++)
for(i=0;i<ph;i++)
{
tmp=i*lsize+j*3;
pOUT[tmp]=p[(ph-i-1)*lsize+j*3];
pOUT[tmp+1]=p[(ph-i-1)*lsize+j*3+1];
pOUT[tmp+2]=p[(ph-i-1)*lsize+j*3+2];
}
FILE *fp=fopen("MirrorY.bmp","wb");
writeFileHead(fp);
fwrite(pOUT,psize,1,fp);
fclose(fp);
printf("MirrorY finised!\n");
}
}
double GetRolX(int x,int y,double theta)
{
cx=pw/2;
cy=ph/2;
x-=cx;
y-=cy;
double RolX=x*cos(theta)-y*sin(theta);
return RolX=RolX+cx;
}
double GetRolY(int x,int y,double theta)
{
cx=pw/2;
cy=ph/2;
x-=cx;
y-=cy;
double RolY=x*sin(theta)+y*cos(theta);
return RolY=RolY+cy;
}
int GetRolHW(int *x,int *y,double theta)
{
double tmpx[4],tmpy[4],Maxx=-1,Minx=INF,Maxy=-1,Miny=INF;
int fx[4]={0},fy[4]={0};
fx[2]=fx[1]=pw;
fy[2]=fy[3]=ph;
int i;
for(i=0;i<4;i++)
{
tmpx[i]=GetRolX(fx[i],fy[i],theta);
tmpy[i]=GetRolY(fx[i],fy[i],theta);
if(tmpx[i]>Maxx) Maxx=tmpx[i];
if(tmpx[i]<Minx) Minx=tmpx[i];
if(tmpy[i]>Maxy) Maxy=tmpy[i];
if(tmpy[i]<Miny) Miny=tmpy[i];
}
*x=(int)(Maxx-Minx)+2;
if((*x)%4) *x=(*x/4+1)*4;
*y=(int)(Maxy-Miny)+2;
}
int Interpolation(BYTE *p,double x,double y,int _)
{
int x1,x2,y1,y2;
x1=floor(x);y1=floor(y);
x2=x1+1;y2=y1+1;
if(x1>=0&&x1<pw-1&&y1>=0&&y1<ph-1)
{
float radio11=x-x1,radio12=x2-x,
radio21=y-y1,radio22=y2-y;
int tmp1,tmp2,tmp3,tmp4;
tmp1=y1*lsize+x1*3;
tmp2=y1*lsize+x2*3;
tmp3=y2*lsize+x1*3;
tmp4=y2*lsize+x2*3;
int X1,X2;
X1=p[tmp1+_]*radio11+p[tmp2+_]*radio12;
X2=p[tmp3+_]*radio11+p[tmp4+_]*radio12;
return X1*radio21+X2*radio22;
}
else
return 0;
}
void OutPutImg(BYTE *pOUT,int nw,int nh,char *name)
{
char postfix[10]=".bmp",tmp[100];
strcpy(tmp,name);
strcat(tmp,postfix);
FILE *fp=fopen(tmp,"wb");
thisBmpFileHeader.bfSize=thisBmpFileHeader.bfSize-psize+nh*nw*3;
thisBmpInfo.biWidth=nw;
thisBmpInfo.biHeight=nh;
thisBmpInfo.biSizeImage=nw*nh*3;
writeFileHead(fp);
fwrite(pOUT,nw*nh*3,1,fp);
fclose(fp);
printf("%s",name);
printf(" finised!\n");
thisBmpInfo.biWidth=pw;
thisBmpInfo.biHeight=ph;
thisBmpInfo.biSizeImage=pw*ph*3;
}
void Rotation2(BYTE *p,double theta)//对图像旋转theta角度
{
int nh,nw;
GetRolHW(&nw,&nh,theta);
int *fMinX=(int *)malloc(nh*sizeof(int)),
*fMaxX=(int *)malloc(nh*sizeof(int)),//记录旋转后图像每一行最大、小的横坐标
*fMinY=(int *)malloc(nw*sizeof(int)),
*fMaxY=(int *)malloc(nw*sizeof(int));//记录旋转后图像每一列最大、小的纵坐标
int *flag=(int *)malloc(nw*nh*sizeof(int));
BYTE *pOUT=(BYTE*)malloc(nh*nw*3);
int dX=(nw-pw)/2,dY=(nh-ph)/2; // 由于图像放大,计算坐标后需要移动一定距离
int i,j,tmp;
for(i=0;i<nh;i++)
{
fMinX[i]=INF;
fMaxX[i]=-1;
}
for(i=0;i<nw;i++)
{
fMinY[i]=INF;
fMaxY[i]=-1;
}
for(i=0;i<nh;i++)
for(j=0;j<nw;j++)
{
flag[i*nw+j]=0;
int _;
tmp=i*nw*3+j*3;
for(_=0;_<3;_++)
pOUT[tmp+_]=0;
}
for(i=0;i<ph;i++)
for(j=0;j<pw;j++)
{
tmp=lsize*i+j*3;
int tx=(int)GetRolX(j,i,theta)+dX,
ty=(int)GetRolY(j,i,theta)+dY,
Ntmp=ty*nw*3+tx*3;
pOUT[Ntmp]=p[tmp];
pOUT[Ntmp+1]=p[tmp+1];
pOUT[Ntmp+2]=p[tmp+2];
if(tx<fMinX[ty]) fMinX[ty]=tx;
if(tx>fMaxX[ty]) fMaxX[ty]=tx;
if(ty<fMinY[tx]) fMinY[tx]=ty;
if(ty>fMaxY[tx]) fMaxY[tx]=ty;
flag[ty*nw+tx]=1;
}
for(i=0;i<nh;i++)
{
int lastj=-1;
for(j=0;j<nw;j++)
{
tmp=i*nw*3+j*3;
if(j<fMinX[i]||j>fMaxX[i])
{
int _=0;
for(_=0;_<3;_++)
pOUT[tmp+_]=0;
continue;
}
if(flag[i*nw+j]==1)
{
lastj=j;
continue;
}
else
{
int nextj=j;
while(nextj<nw&&flag[i*nw+nextj]==0) nextj++;
int _,tmpl=i*nw*3+lastj*3,tmpn=i*nw*3+nextj*3;
if(nextj>=nw)
{
if(lastj==-1) break;
else
for(_=0;_<3;_++)
pOUT[tmp+_]=pOUT[tmpl+_];
}
else
{
if(lastj==-1)
for(_=0;_<3;_++)
pOUT[tmp+_]=pOUT[tmpn+_];
else
{
float radio1=(j-lastj)*1.0/(nextj-lastj),
radio2=(nextj-j)*1.0/(nextj-lastj);
for(_=0;_<3;_++)
pOUT[tmp+_]=radio2*pOUT[tmpn+_]+radio1*pOUT[tmpl+_];
}
}
}
}
}
int x=theta*180/pi;
char s1[100]="Rotation2",s[20];
sprintf(s,"_%d_degree",x);
strcat(s1,s);
OutPutImg(pOUT,nw,nh,s1);
}
分析
坐标系统和存储系统
代码中像素点的颜色信息存储在 $P$ 数组中。$P$ 是个一维数组,我们可以把它排列成二维数组,通过计算可以获取下标。$P[tmp]\rightarrow P[i][j] ,(tmp=iwh+j*3)$
代码中的坐标系统是把下标为$P[0][0]$ 的像素点作为坐标原点,每一行下标增长的方向作为 $X$ 轴的正方向。每一列下标增长的方向作为 $Y$ 轴的正方向。
即 坐标为 $(i,j)$ 的像素点对应的下标为 $P[j][i]$
bmp图像的数据扫描顺序是,每一行从左向右扫描,而行则是从下往上扫描。即数组中的第一行是图片中最后一行的数据。这样我们代码中选取的坐标系统与图片中常用的坐标是相符的
全局变量的含义
1
2
3
4
5
6
7const double pi=3.14; //定义常量pi
int ph,pw, //原始图像的高度和宽度
psize, //原始图像的字节数
lsize, //原始图像每行的字节数
cx,cy; //原始图像的中心点的坐标。
BMPFILEHEADER thisBmpFileHeader; // 定义一个 BMP 文件头的结构体
BMPINF thisBmpInfo; // 定义一个 BMP 文件信息结构体主函数中读入in.bmp,将文件头信息存储到 thisBmpFileHeader 和 thisBmpInfo 中。从文件头信息中读取 ph,pw 的值。
根据公式 $\begin{cases}psize = pwph3\size=pw*3\cx=pw/2\cy=ph/2\end{cases}$ 计算出各个变量的值,作为全局变量供所有函数使用。
函数的作用及参数表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//fp指向的文件的文件头读入到thisBmpFileHeader和thisBmpInfo中。
void readFileHead(FILE *fp);
//thisBmpFileHeader和thisBmpInfo中的信息写入到fp指向的文件中。
void writeFileHead(FILE *fp);
//进行平移操作,dx,dy为坐标移动的增量。flag=0时,画布增大,flag=1时,画布不增大。
void Translation(BYTE *p,int dx,int dy,int flag);
//对图像逆时针旋转theta角度(双线性插值法)
void Rotation(BYTE *p,double theta);
//对图像逆时针旋转theta角度(先旋转,后补全空缺的像素点)
void Rotation2(BYTE *p,double theta);
//对图像投影theta角度,flag=0 -> X轴方向投影,flag=0 -> y轴方向投影。
void Shear(BYTE *p,double theta,int flag);
//对图像放缩变换,a,b分别表示横纵坐标方向的比例。
void Scale(BYTE *p,float a,float b);
//对图像进行对称变换,flag=0时x轴,flag=1时y轴
void Mirror(BYTE *p,int flag);
//获取原图中(x,y)像素点绕图片中心逆时针旋转theta后的坐标。
double GetRolX(int x,int y,double theta);
double GetRolY(int x,int y,double theta);
//获取原图经旋转后得到的新的宽度和高度,分别存储在x,y指针中
int GetRolHW(int *x,int *y,double theta);
//(x,y)在原图中双线性插值后的结果。 _ 变量表示RGB三个分量之一
int Interpolation(BYTE *p,double x,double y,int _);
//输出图像,图片名称为 name
void OutPutImg(BYTE *pOUT,int nw,int nh,char *name);文件读入输出。
1
2
3void readFileHead(FILE *fp);
void writeFileHead(FILE *fp);
void OutPutImg(BYTE *pOUT,int nw,int nh,char *name);这三个函数的功能与之前实验中功能相同并且实现起来很简单,不做赘述。
双线性插值算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22int Interpolation(BYTE *p,double x,double y,int _)
{
int x1,x2,y1,y2;
x1=floor(x);y1=floor(y);
x2=x1+1;y2=y1+1;
if(x1>=0&&x1<pw-1&&y1>=0&&y1<ph-1)
{
float radio11=x-x1,radio12=x2-x,
radio21=y-y1,radio22=y2-y;
int tmp1,tmp2,tmp3,tmp4;
tmp1=y1*lsize+x1*3;
tmp2=y1*lsize+x2*3;
tmp3=y2*lsize+x1*3;
tmp4=y2*lsize+x2*3;
int X1,X2;
X1=p[tmp1+_]*radio11+p[tmp2+_]*radio12;
X2=p[tmp3+_]*radio11+p[tmp4+_]*radio12;
return X1*radio21+X2*radio22;
}
else
return 0;
}利用公式 $$C’(x’,y’)=radio_{21}*(radio_{11}*C(x_1,y_1)+radio_{12}C(x_1,y_2))\\qquad\qquad+ radio_{22}(radio_{11}*C(x_2,y_1)+radio_{12}*C(x_2,y_2))$$
进行模拟即可平移 Translation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25Translation(BYTE *p,int dx,int dy,int flag)
if(dx%4) dx=(dx/4+1)*4;//保证x轴方向的偏移量为4的倍数
int i,j,tmp,n;
if(flag==1)
for each pixel (i,j) in new img
x:=j-dx
y:=i-dy
if(x>=pw) x:=x-pw;
if(x<0) x:=x+pw;
if(y>=ph) y:=y-ph;
if(y<0) y:=y+ph;
pOUT(i,j) := p(y,x)
end
end
else
Get the width nw and height nh of new img.
Get the coordidates of the pixels which are border.
Assume (x0,y0) is the right-top border
for each pixel (i,j) in new img
x:=j-x0-dx
y:=i-y0-dy
if (x,y)in the old img
pOUT(i,j) := p(y,x)
end
end当flag的值是1时,我们做的平移操作不改变图像的尺寸。按照公式$$ \left{ \begin{aligned} x = x’-dx\y = y’-dy \end{aligned} \right. $$ 计算得到 $(x,y)$ 的值,如果 $(x,y)$ 超出了图片的范围,我们把它移动到图片另一侧。
当flag的值是0时,我们做的平移操作会改变图像的尺寸。首先获取新的图像的尺寸。其次,根据不同的平移情况,获取有图案的部分位于画布的位置,也就是在画布上的四个边界顶点的坐标。设左上点的坐标是$ (x_0,y_0)$ 。则坐标公式为$$ \left{ \begin{aligned} x = j-x_0-dx\y = i-y_0-dy \end{aligned} \right. $$ 。最后进行对应的赋值。
旋转(插值法)
1
2
3
4
5
6
7
8
9
10
11Rotation(BYTE *p,double theta)
GetRolHW(&nw,&nh,theta);
dX:=(nw-pw)/2,dY:=(nh-ph)/2;
for each pixel (i,j) in new img
a:=j-dX-cx+cx*cos(theta)-cy*sin(theta);
b:=i-dY-cy+cy*cos(theta)+cx*sin(theta);
x:=b*sin(theta)+a*cos(theta);
y:=b*cos(theta)-a*sin(theta);
pOUT=Interpolation(p,x,y,_);
end
end$\begin{cases}a=x'-dx-cx+cx*cos\theta-cy*sin\theta\\b=y'-dy-cy+cy*cos\theta+cx*sin\theta\end{cases}$ $$ \left\{ \begin{aligned} x = b*sin\theta +a*cos\theta \\y = b*cos\theta-a*sin\theta \end{aligned} \right. $$
首先利用函数 GetRolHW 获取新图像的宽度和高度。
然后根据上面的公式计算得到每个像素点对应的初始坐标(x,y)。
然后用函数 Interpolation 做双线性插值。
旋转(补空法)
首先利用函数 ***GetRolHW*** 获取新图像的宽度和高度。 遍历原始图像中的各个像素点,然后进行旋转操作得到其对应的新图像的坐标,赋值后标记这些点。 遍历新图像中的各个像素点,如果其在图像边界之内,并且没有被标记,则用新图像中其周围的像素点的信息来计算像素点的值。 如果这个像素点之前的所有点均未被标记,则这个像素点的值为其后第一个被标记的点的像素点。 如果这个像素点之后的所有点均未被标记,则这个像素点的值为其前第一个被标记的点的像素点。 如果这个像素点前后均有点被标记,则这个像素点的值为其前第一个被标记的点的像素点和其后第一个被标记的点的像素点的加权平均。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void Rotation2(BYTE *p,double theta)//对图像旋转theta角度
GetRolHW(&nw,&nh,theta);
dX=(nw-pw)/2,dY=(nh-ph)/2
Get the value of fMinX、fMaxX、fMinY、fMaxY //图像的边界
for each pixel (i,j) in origin img
get (x,y) rotate (i,j)
Mark (i,j)
end
for each pixel (i,j) in new img
if (i,j) is not marked
if (i,j) is the last marked pixel in the row
(i,j) = the pixel before it which is marked
if (i,j) is the first marked pixel in the row
(i,j) = the pixel after it which is marked
if (i,j) is in the middle
(i,j) = radio1*pafter + radio2*pbefore
end
end
end```
放缩、对称和投影
这三个函数的操作非常简单,利用原理部分的公式,计算得到(x,y)后,进行插值算法。
其代码和旋转操作代码十分相似,只有坐标计算公式不同。
结果展示
平移
平移
X轴方向对称
Y轴方向对称
逆时针旋转60度
放缩
水平投影30度
垂直投影30度
两种不同算法的旋转操作的结果比较
对原图和两种算法得到的旋转图像分别作出直方图如下
根据直方图我们可以得出结论两种算法得到的旋转图像均保留了图像的样貌。
四、心得体会
对图像变换的理解
通过这次实验,我明白了图像的几何变换就是对像素点的坐标进行几何变换,可以借助矩阵运算来更加方便的计算坐标。
对插值法的理解
我们插值的目的在于,通过几何变换计算坐标时,并不总是能够得到整数的值。当我们计算得到的值是小数的时候,就需要利用这个点周围的像素点RGB值来求其对应的值。很明显距离越远的的点对它造成的影响应当越少。自然而然可以想到按照距离进行加权平均,这也正是双线性插值的公式写代码过程中的问题
坐标到数组下标的转化
在根据坐标计算其在数组中对应的下标时,由于新老图片尺寸等问题,在计算时犯了很多低级错误,导致出现奇怪的bug。思考之后有两个改进方案。其一是改进存储方法,不使用一个数组来存储整张图片信息,而是用一个结构体数组来存储,每个结构体含有R,G,B三个成员。这样在计算坐标的时候,就不会出现问题。第一个方法则是把这个计算关系写成一个函数。
最开始写的时候没有对程序有好的架构。导致不同的几何变换中出现了大量重复的操作,代码显得十分冗长。后来进行了一次重构,将重复的操作提炼成函数,这样使代码更加简洁明了。