1.了解二维图形基本变换的原理;
2.用利用C++语言编程实现二维图形的平移变换、比例变换、对称变换、旋转变换、错切变换五种基本变换;
3.了解二维图形复合变换的原理;
4.利用C++语言编程实现二维图形自定义点作为放缩点的比例变换、二维图形自定义点作为放缩点的旋转变换、以直线Ax+By+C=0为轴的对称变换。
Visual Studio 2022版
Windows 10/11
对于二维图形变换,其算法最核心的思想就是利用与变换对应的变换矩阵对图形中的每一个点进行变换,最终得到变换后每一个点的坐标,组成完整的多边形。变换矩阵是3×3的矩阵,而每一个点的平面坐标可以表示为1×3的向量[x,y,1] 的形式,因此与变换矩阵相乘以后会得到一个1×3的向量[x',y',1] ,这就是变换后的点坐标。对于多边形而言,其由顶点连接并填充而成,因此无需去求每个点变换后的坐标,只需要求出顶点变换后的坐标,再对其进行连接和填充,就可以得到变换后的多边形。而不同变换的不同之处就在于变换矩阵不同,由此,只需要知道相应变换对应的变换矩阵,即可求得变换后的点坐标和多边形。
对于基本变换而言,变换矩阵只有一个,基本变换的类型为平移变换、比例变换、关于原点和坐标轴的对称变换,旋转变换,错切变换;而对于复合变换而言,其一定是由许多基本变换组成的,需要将其涉及到所有基本变换矩阵按照顺序相乘,即可得到复合变换的变换矩阵。
1.平移变换
平移变换将指定多边形从一个坐标位置移动到另一个坐标位置的过程,Tx 和T
分别为x轴和y轴方向平移的距离,所以平移变换可以表示为:
2.比例变换
比例变换是指一个点相对于某一个参考点横纵坐标分别变为原来的一定倍数,Sx 和Sy 分别为x轴和y轴方向的比例系数,所以比例变换可以表示为:
3.对称变换
求出一个点关于某条特定直线的对称点。对称变换可以表示为:
其中,a、b、d、e都是对称变换矩阵的系数,其取值不同对称变换的效果也不同。
有以下特殊情况作为基本对称变换:
(1)当b=d=0,a=-1,e=1时,关于y轴对称;
(2)当b=d=0,a=1,e=-1时,关于x轴对称;
(3)当b=d=1,a=e=-1时,关于原点对称;
(4)当b=d=1,a=e=0时,关于直线y=x对称;
(5)当b=d=-1,a=e=0时,关于直线y=-x对称。
4.旋转变换
基本旋转变换指将某个点绕着原点旋转一定角度θ 。其可以表示为:
当θ 为正时,是顺时针旋转,而θ 为负时,是逆时针旋转。
5.错切变换
错切变换是指按照一定的比例对图形的每个点到某条平行于该方向的直线的有向距离做放缩,用于弹性物体的变形处理。错切变换可以表示为:
以上五种基本变换最核心算法原理就是变换矩阵,这也决定着发生变换的种类。
而对于复合变换,其根据不同基本变换矩阵组成而成,种类繁多,千变万化,可以根据需要进行相应的设计。而本次上机实验主要实现以下三种复合变换:
1.以自定义的点作为放缩点进行比例变换,而其对应的基本变换操作为以下三个步骤:
(1)将原点平移到自定义的点的位置;
(2)进行基本比例变换;
(3)将原点重新平移回原始位置。
设自定义的点坐标为x0,y
,横纵坐标轴方向的比例系数分别是Sx 和Sy ,将这三个步骤对应的变换矩阵按顺序相乘,可以得到如下表达式:
可以得到变换后x' 和y' 的值。
2.以自定义的点作为放缩点进行旋转变换,而其对应的基本变换操作为以下三个步骤:
(1)将原点平移到自定义的点的位置;
(2)进行旋转变换;
(3)将原点重新平移回原始位置。
设自定义的点坐标为x0,y0 ,旋转角度为θ ,将这三个步骤对应的变换矩阵按顺序相乘,可以得到如下表达式:
可以得到变换后x' 和y' 的值。
3.以任意直线作为对称轴对称,其对应的基本变换操作主要为以下五个步骤:
(1)将原点平移到直线与x轴交点位置;
(2)将x轴旋转到与直线重合;
(3)将图形关于x轴进行对称;
(4)将x轴复原回旋转之前的位置;
(5)将原点平移到初始位置。
操作的示意图如下:
设直线的方程为Ax+By+C=0 ,则可以求出直线的斜率为k=-A/B ,则旋转角的值也就是直线斜率对应的倾角,其值为θ=arctan(-A/B)
,则将这五个步骤对应的变换矩阵按顺序相乘,可以得到如下表达式:
可以得到变换后x' 和y' 的值。
在C++中实现时,为了实现用户交互,采用switch-case分支语句,将前面的八种二维图形变换方法作为八个不同的分支,供用户进行选择,当用户选择某一分支时,执行相应的二维图形变换程序,对原始二维图形进行相应操作,并显示操作结果。而C++<graphics.h>库中的fillpolygon函数可以根据多边形的顶点坐标和边数填充出多边形,可以用于变换前后多边形的显示。
#include<iostream>
#include<graphics.h>
#include <conio.h>
#include<cmath>
using namespace std;
const double pi = 3.1415926;
void Translation(int* x, int* y, int n, int tx, int ty) {
initgraph(1000, 800);//建立1000×800窗口
setfillcolor(YELLOW);//多边形填充颜色
POINT* p = new POINT[n];//为顶点结构体数组分配内存空间
for (int i = 0; i < n; i++) {
p[i] = { x[i],y[i] };
}//将顶点坐标存入顶点结构体数组中
fillpolygon(p, n);//绘制出平移前多边形
_getch();//按任意键继续
for (int i = 0; i < n; i++) {
p[i].x += tx;//x坐标加平移参数
p[i].y += ty;//y坐标加平移参数
}
fillpolygon(p, n);//绘制出平移后多边形
_getch();
}//实现多边形的平移,并绘制出平移前后的多边形,输入参数为多边形顶点坐标数组,边数n,x方向平移参数tx、y方向平移参数ty
void ScaleChange(int* x, int* y, int n, double sx, double sy) {
initgraph(1000, 800);//建立1000×800窗口
setfillcolor(YELLOW);//多边形填充颜色
POINT* p = new POINT[n];//为顶点结构体数组分配内存空间
for (int i = 0; i < n; i++) {
p[i] = { x[i],y[i] };
}//将顶点坐标存入顶点结构体数组中
fillpolygon(p, n);//绘制出比例变换前多边形
_getch();//按任意键继续
for (int i = 0; i < n; i++) {
p[i].x *= sx;//x坐标乘比例变换参数
p[i].y *= sy;//y坐标乘比例变换参数
}
fillpolygon(p, n);//绘制出比例变换后多边形
_getch();
}//实现多边形的比例变换,并绘制出比例变换前后的多边形,输入参数为多边形顶点坐标数组,边数n,x方向平移参数sx、y方向平移参数sy
void Symmetry(int* x, int* y, int n) {
initgraph(1000, 800);//建立1000×800窗口
setfillcolor(YELLOW);//多边形填充颜色
POINT* p = new POINT[n];//为顶点结构体数组分配内存空间
for (int i = 0; i < n; i++) {
p[i] = { x[i],y[i] };
}//将顶点坐标存入顶点结构体数组中
fillpolygon(p, n);//绘制出比例变换前多边形
_getch();//按任意键继续
for (int i = 0; i < n; i++) {
p[i].x = y[i];
p[i].y = x[i];//xy坐标互换
}
fillpolygon(p, n);//绘制出比例变换后多边形
_getch();
}//实现多边形的按y=x的对称,并绘制出变换前后的多边形,输入参数为多边形顶点坐标数组,边数n
void Rotation(int* x, int* y, int n,double theta) {
initgraph(1000, 800);//建立1000×800窗口
setfillcolor(YELLOW);//多边形填充颜色
POINT* p = new POINT[n];//为顶点结构体数组分配内存空间
for (int i = 0; i < n; i++) {
p[i] = { x[i],y[i] };
}//将顶点坐标存入顶点结构体数组中
fillpolygon(p, n);//绘制出比例变换前多边形
_getch();//按任意键继续
for (int i = 0; i < n; i++) {
p[i].x = int(double(x[i]) * cos(theta * pi / 180) - double(y[i]) * sin(theta * pi / 180));//x坐标变换
p[i].y = int(double(x[i]) * sin(theta * pi / 180) + double(y[i]) * cos(theta * pi / 180));//y坐标变换
}
fillpolygon(p, n);//绘制出比例变换后多边形
_getch();
}//实现多边形绕原点顺时针旋转一定角度,并绘制出变换前后的多边形,输入参数为多边形顶点坐标数组,边数n,旋转角度theta
void StaggeredCutting(int* x, int* y, int n, double b,double d) {
initgraph(1000, 800);//建立1000×800窗口
setfillcolor(YELLOW);//多边形填充颜色
POINT* p = new POINT[n];//为顶点结构体数组分配内存空间
for (int i = 0; i < n; i++) {
p[i] = { x[i],y[i] };
}//将顶点坐标存入顶点结构体数组中
fillpolygon(p, n);//绘制出比例变换前多边形
_getch();//按任意键继续
for (int i = 0; i < n; i++) {
p[i].x = x[i] + b * y[i];//x坐标变换
p[i].y = y[i] + d * x[i];//y坐标变换
}
fillpolygon(p, n);//绘制出比例变换后多边形
_getch();
}//实现多边形错切变换,并绘制出变换前后的多边形,输入参数为多边形顶点坐标数组,边数n,错切参数b、d
void AnyScaleChange(int* x, int* y, int n, double sx, double sy, int x0, int y0) {
initgraph(1000, 800);//建立1000×800窗口
setfillcolor(YELLOW);//多边形填充颜色
POINT* p = new POINT[n];//为顶点结构体数组分配内存空间
for (int i = 0; i < n; i++) {
p[i] = { x[i],y[i] };
}//将顶点坐标存入顶点结构体数组中
fillpolygon(p, n);//绘制出比例变换前多边形
_getch();//按任意键继续
for (int i = 0; i < n; i++) {
p[i].x =x0+ sx * (p[i].x - x0);//x坐标减放缩点x坐标乘比例变换参数
p[i].y =y0+ sy * (p[i].y - y0);//y坐标减放缩点y坐标乘比例变换参数
}
fillpolygon(p, n);//绘制出比例变换后多边形
_getch();
}//实现以任意一点为放缩点进行多边形的比例变换,并绘制出比例变换前后的多边形,输入参数为多边形顶点坐标数组,边数n,x方向平移参数sx、y方向平移参数sy,比例变换的放缩点
void AnyRotation(int* x, int* y, int n, double theta,int x0,int y0) {
initgraph(1000, 800);//建立1000×800窗口
setfillcolor(YELLOW);//多边形填充颜色
POINT* p = new POINT[n];//为顶点结构体数组分配内存空间
for (int i = 0; i < n; i++) {
p[i] = { x[i],y[i] };
}//将顶点坐标存入顶点结构体数组中
fillpolygon(p, n);//绘制出比例变换前多边形
_getch();//按任意键继续
for (int i = 0; i < n; i++) {
p[i].x = x0+int(double(x[i]-x0) * cos(theta * pi / 180) - double(y[i]-y0) * sin(theta * pi / 180));//x坐标变换
p[i].y = y0+int(double(x[i]-x0) * sin(theta * pi / 180) + double(y[i]-y0) * cos(theta * pi / 180));//y坐标变换
}
fillpolygon(p, n);//绘制出比例变换后多边形
_getch();
}//实现多边形绕任意一点顺时针旋转一定角度,并绘制出变换前后的多边形,输入参数为多边形顶点坐标数组,边数n,旋转角度theta,放缩点横纵坐标
void Symmetryline(int* x, int* y, int n,double A,double B,double C) {
initgraph(1000, 800);//建立1000×800窗口
setfillcolor(YELLOW);//多边形填充颜色
POINT* p = new POINT[n];//为顶点结构体数组分配内存空间
double theta = atan(-A / B);//直线倾角
for (int i = 0; i < n; i++) {
p[i] = { x[i],y[i] };
}//将顶点坐标存入顶点结构体数组中
fillpolygon(p, n);//绘制出比例变换前多边形
_getch();//按任意键继续
for (int i = 0; i < n; i++) {
p[i].x = x[i] * cos(2 * theta) + y[i] * sin(2 * theta ) + C * (cos(2 * theta ) - 1) / A;//x表达式
p[i].y = x[i] * sin(2 * theta ) - y[i] * cos(2 * theta ) + C * sin(2 * theta )/A;//y表达式
}
fillpolygon(p, n);//绘制出比例变换后多边形
_getch();
}//实现多边形的关于Ax+By+C=0的对称,并绘制出变换前后的多边形,输入参数为多边形顶点坐标数组,边数n,直线参数a、b
int main() {
int x[3] = { 300,300,500 };
int y[3] = { 100,300,100 };
int cmd;//选择操作类型
cout << "请输入您想要进行的基本变换类型(1为平移、2为比例变换(原点)、3为对称(y=x)、4为旋转(原点)、5为错切、6为比例变换(任意点)、7为旋转(任意点)、8为关于任意直线对称:\n";//输出提示命令
cin >> cmd;//输入参数
switch (cmd) {
case 1:
int tx, ty;
cout << "您选择了平移操作,请输入横纵方向平移距离:";//显示提示命令
cin >> tx >> ty;
cout << "参数输入成功,请按任意键继续绘画";//输出提示命令
_getch();
Translation(x, y, 3, tx, ty);//进行平移变换
case 2:
double sx, sy;
cout << "您选择了比例变换操作,请输入横纵和纵轴的变换比例:";//显示提示命令
cin >> sx >> sy;
ScaleChange(x, y, 3, sx, sy);//进行旋转变换
case 3:
cout << "您选择了关于y=x对称!";//显示提示命令
Symmetry(x, y, 3);//进行对称变换
case 4:
double angle;
cout << "您选择了旋转变换,请输入旋转角度大小(角度制):";//显示提示命令
cin >> angle;//输入角度
cout << "参数输入成功,请按任意键继续绘画";
Rotation(x, y, 3, angle);//进行旋转变换
case 5:
double a, b;
cout << "您选择了错切变换,请输入横纵错切系数和纵轴错切系数:";//显示提示命令
cin >> a >> b;
cout << "参数输入成功,请按任意键继续绘画";
StaggeredCutting(x, y, 3, a, b);//进行错切变换
case 6:
double sx1, sy1;
int xsc, ysc;
cout << "您选择了按指定放缩点比例变换操作,请依次输入横纵和纵轴的变换比例、放缩点横纵坐标:";//显示提示命令
cin >> sx1 >> sy1 >> xsc >> ysc;
AnyScaleChange(x, y, 3, sx1, sy1, xsc, ysc);//按任意点进行比例变换
case 7:
double angle1;
int xr, yr;
cout << "您选择了按指定放缩点旋转变换,请输入旋转角度大小(角度制)和放缩点横纵坐标:";//显示提示命令
cin >> angle1 >> xr >> yr;
AnyRotation(x, y, 3, angle1,xr, yr);//进行旋转变换
case 8:
double A, B, C;
cout << "您选择了关于Ax+By+C=0求对称,请输入直线相关参数A,B,C:";//显示提示命令
cin >> A >> B >> C;
Symmetryline(x, y, 3, A, B, C);//求对称多边形
default:
cout << "您没有输入正确的变换命令,请重新输入:";
}
return 0;
}
本次变换的图像设计为一个三角形,顶点坐标分别为(300,100),(300,300),(500,100)
1.横纵坐标分别平移100、200
(1)窗口如下:
(2)图像如下:
2.横轴纵轴比例系数分别为1.2和1.5进行比例变换
(1)窗口如下:
(2)图像如下:
3.关于y=x对称
4.顺时针旋转30°
(1)窗口如下:
(2)图像如下:
5.以0.5和0.7为横纵坐标方向的系数进行错切变换
(1)窗口如下:
(2)图像如下:
6.以点(200,200)作为放缩点进行比例变换
(1)窗口如下:
(2)图像如下:
7. 以点(100,100)作为放缩点旋转30°
(1)窗口如下:
(2)图像如下:
8.以直线-2x+3y-100=0为对称轴对称
(1)窗口如下:
(2)图像如下:
本次上机实验主要进行了二维图形的变换,每一个二维图形的变换,无论是基本变换还是复合变换,其核心操作就是通过相应的变换矩阵对每一个点的坐标进行相应变换,区别就是基本变换由一个矩阵就可以进行,而复合变换矩阵往往是数种基本变换矩阵相乘得到。而在计算机屏幕上显示的图形又是由一个个的像素点组成的,所以就可以利用变换后的点重新组成多边形。若对所有点逐一进行变换,算法效率太低,而对于多边形而言,其由顶点连接并填充而成,所以为了提高算法效率,只需要对顶点进行变换之后连接和填充即可得到变换以后的多边形。
在原理部分利用变换矩阵进行矩阵乘法的计算,可以推导出每一种变换对应的横纵坐标值的表达式,根据这些表达式,分别对多边形每一个顶点坐标进行变换,利用C++语言很容易就能够将表达式的值算出,利用循环对每一个点进行遍历,就可以求得所有顶点变换后的坐标,然后填充变换后的多边形即可,使用了C++<graphics.h>库中自带的函数fillpolygon,其输入参数分别为多边形顶点的x坐标数组以及与之对应的y坐标数组,多边形的边数,其可以实现填充由这些顶点连接而成的多边形的功能,利用setfillcolor设置填充的颜色即可。
对于算法的时间复杂度,由于每一个变换函数对应的算法核心思想都是相同的,无论是基本变换还是复合变换,不同函数相差的仅仅是变化后的横纵坐标表达式,所以其函数结构也都是一致的,而其中涉及到两个并列的循环,一个循环是为了导入原始坐标,画出原始图形,另一个是为了计算变换后的坐标,画出变换后的图形,所以每一种变换其时间复杂度均为O(n),算法效率也基本一致,每一个循环内部仅有两部基本操作,且均不复杂,所以各种二维图形变换算法效率都很高。在这个程序中,为了实现良好的用户交互性,利用switch—case分支语句进行编程,首先让用户选择二维图形变换的方式,接着在相关分支中实现变换参数的输入,并进行变换,这样也避免了同时编写多个函数分别进行编译运行,简化了编程操作流程,增强了编程的集成性,减小了内存空间的占用,也方便后续进行实际的二维图形变换。
在编程时还需要注意角度制和弧度制单位转化的问题:对于旋转变换而言,用户习惯输入的一定是角度制的角度,而C++<cmath>库中三角函数的输入参数是弧度制的,所以角度值需要*pi/180,转化为弧度制以后再输入到三角函数中;而对于后续以任意直线为对称轴进行对称的变换中,需要对直线进行旋转,旋转的角度就是直线斜率对应的角度,可以用反正切函数求出,而C++中的反正切函数输出参数就是弧度制的角度,因此不需要再进行转化,直接将算出的直线倾角输入到相应的三角函数中即可。
二维图形变换在实际生活中和专业领域均有很大的应用,在实际生活中,各种图形在计算机上的运动效果,都是一帧一帧进行变换的;在进行虚拟仿真或建模时,也需要能够让计算机中的模型随着视角不同而变化;一些模拟镜面的软件也需要用到对称变换。而在专业领域,在GIS中,在计算机上对图形进行查看时,经常会进行放大缩小,也就是比例变换;也会进行拖拽平移查看不同地物符号,也就是平移变换;在对不同坐标系进行转换时,都会存在投影变形,通常也会涉及到多边形方向和比例的转换、多边形的旋转;点状地物符号随着位置不同的不同放置方式也涉及到多边形的旋转。可以说,二维图形的变换对计算机图形的显示起着至关重要的作用。
系列总结
至此,计算机图形学上机报告系列就全部完成了,由于前一阵子博主在准备保研、完成实习,所以更新不及时。希望内容可以给更多从事计算机图像处理和计算机图形学以及各大高校学习计算机图形学课程的同学一些帮助和启发。
如果觉得内容有所帮助,一个点赞或是关注都是对博主最大的肯定,也是博主继续创作更新的动力。后续将继续针对计更多GIS、遥感、测绘方面的基础知识、科研方法和技术手段进行更新,可能涉及Python、MATLAB等语言,ENVI、ARCGIS等软件,欢迎大家关注以便及时获取干货!