坐标系统与坐标变换(转载)
by iwaswzq 2005/1/22
经常有朋友提问关于编程过程中遇到的坐标变换问题。我抽了点时间从msdn摘译了一些东西,并加了一些自己的理解,希望能有助于对程序中坐标变换的理解。鉴于我水平有限,可能某些概念的理解有些错误或者解释不够准确,欢迎指正。不足的地方,以后有时间会继续丰富此文。
win32程序使用坐标系统之间的变换完成图形的缩放、旋转、平移等输出操作。win32下面总共使用四个坐标空间:世界坐标系、页面坐标系、设备坐标系和物理坐标系(包括客户区、桌面或打印纸等)。每个坐标空间都是一个线性空间,用两个相互垂直的坐标轴定位两维的物体。
我们把改变一个物体的大小、方向和形状的算法称作“变换”。一个图形物体从一个坐标空间映射到另一个坐标空间的过程就是一个变换。最终,物体显示在一个物理设备上,通常是屏幕或者打印机。
1、四个坐标系的定义
坐标系 描述
世界坐标系 可选,用于图形转换的起始坐标空间。最大尺寸是 2^32单位高和 2^32 单位宽。
支持缩放、平移、旋转、变形、投射等转换操作。
页面坐标系 作为世界坐标系之后的第二个坐标系使用,也可以作为变换的起始坐标系。
最大尺寸是 2^32单位高和 2^32 单位宽。可以设置映射模式。
设备坐标系 用于页面坐标系之后。仅仅允许平移操作。保证设备坐标系的原点位于正确的
物理设备空间中合适的位置上。最大尺寸是2^27单位高和 2^27单位宽。
物理坐标系 图形变换后的最终的输出空间。通常指程序窗口的客户区。也可以是整个桌面、
整个窗口区域或者打印机、绘图仪的某一页,取决于程序获得的DC的句柄。物
理设备的尺寸取决于显卡、打印机等的设置。
页面空间和设备空间一起工作,在这两个空间下,程序可以使用设备相关的单位,例如毫米和英寸。但是在世界坐标系和页面坐标系下,都认为是逻辑坐标系,单位是逻辑单位1。
任何程序的绘图代码中使用的坐标都是从世界坐标系开始,直到物理坐标系,最后得到(看到)输出结果。每两个坐标系之间,系统都采用一种变换方法,从前面的坐标空间复制(或者映射)一个矩形区域到下一个坐标空间。为了便于处理,如果程序调用了SetWorldTransform函数,则这个映射过程从世界坐标系开始,否则从页面坐标系开始。用图形表示如下:
+-----------+ +------------+ +------------+ +------------------+
| 绘图代码 | | (窗口) | | (视口) | | (屏幕/打印机等) |
| 世界坐标系| ---〉 | 页面坐标系 | -----〉| 设备坐标系 | ----〉| 物理坐标系 |
| | | | | | | |
+-----------+ +------------+ +------------+ +------------------+
举个例子说,如果我们在DC上面有个画线函数:
MoveTo(hDC, 0, 0);
LineTo(hDC, 10,10);
则如果我们没有使用SetWordTransform函数,可以认为我们的画线操作就是在页面坐标系下面,0,10都是页面坐标系下的坐标。如果我们使用了SetWorldTransform函数,则画线操作是在世界坐标系下面。页面坐标系中对应的这个线段是世界坐标系下面的线段(0,0) -> (10,10) 通过两个坐标空间之间的变换矩阵变换得到,结果可能还是(0,0)点到(10,10)点,也可能是(5,9)点到(-20,14)点,这取决于两个坐标系之间的变换矩阵。
同样道理,对于上述四个坐标系,当系统从一个坐标系中复制指定矩形区域内的某个点到下一个坐标系时,它使用这两个坐标系之间的变换算法,根据点的原坐标计算得到点的像坐标。因此,一个图形在不同坐标系下,其尺寸、方向和形状都可能不同。注意一点,虽然这个变换是两个坐标系之间的,是针对物体整体而言的变换,但在系统在操作的时候,是逐点、逐行操作的。
虽然很少用到SetWorldTransform函数,但是应该掌握最基本的坐标系之间的线性变换矩阵,形式如下:
| eM11 eM12 0 |
| eM21 eM22 0 | 即: x' = x * eM11 + y * eM21 + eDx,
| eDx eDy 1 | y' = x * eM12 + y * eM22 + eDy,
函数采用逻辑单位,缺省的变换矩阵是单位阵:
| 1 0 0 |
| 0 1 0 | 变换关系就是 x' = x ; y' = y ; 相当于没有变换
| 0 0 1 |
四个eM参数给出旋转和缩放变换系数,eDx/eDy给出平移变换系数。
注意SetWorldTransform函数要求DC的图形模式是GM_ADVANCED,可以用SetGraphicsMode设置,仅Windows NT/ 2000下面支持。缺省图形模式是GM_COMPATIBLE,兼容16位windows,这个模式下不能使用该函数。下面简单介绍这两个坐标系之间的变换矩阵。
2、世界坐标系到页面坐标系的变换
(1)平移。物体上每个点进行水平和垂直的移动,eDx 和eDy 参数分别给出移动的尺寸。具体算法是:
x' = x + Dx
y' = y + Dy
其中 x',y' 是新的坐标, x,y 是源坐标。 Dx是水平移动距离,Dy是垂直移动距离。
平移矩阵是:
|1 0 0|
|x' y' 1| = |x y 1| * |0 1 0|
|Dx Dy 1|
(2)缩放。组成物体的每个水平行或者垂直行进行拉伸或者压缩。算法公式是:
y' = y * Dy
x' = x * Dx
其中 Dy,Dx是缩放系数。用矩阵表示为:
|x' y' 1| = |x y 1| * |Dx 0 0|
|0 Dy 0|
|0 0 1|
(3)旋转。组成物体的每个点都相对于坐标原点旋转一个角度。算法公式是:
x' = (x * cos A) - (y * sin A)
y' = (x * sin A) + (y * cos A)
A表示绕原点逆时针旋转的角度,用矩阵表示如下:
|x' y' 1| = |x y 1| * | cos A sin A 0|
|-sin A cos A 0|
| 0 0 1|
(4)变形。分为水平变形和垂直变形两种,举个例子说,一个矩形通过变形成为一个平行四边形。算法公式分别是:
x' = x + (Sx * y)
y' = y + (Sy * x)
其中Sx, Sy分别是变形系数。用矩阵表示为:
|x' y' 1| = |x y 1| * | 1 Sx 0|
| Sy 1 0|
| 0 0 1|
(5)镜像映射。例如水平翻转的公式是:
x' = –x
用矩阵表示是:|-1 0|
|0 1|
线性变换可以是上面几种变换中任意若干种的组合。最终的变换矩阵是一个3x3的矩阵。可以调用CombineTransform进行两种变换的组合,也可以自己按照公式计算出变换矩阵。
强调一点,虽然可以通过SetWorldTransform函数调用设置世界坐标系到页面坐标系的变换矩阵,但通常情况下,在我们的程序中,图形图像的变换并不是通过系统来完成的。而是程序自己完成的,因为自己做,可以更加灵活,容易控制,效率更高。因此不推荐使用SetWorldTransfor函数,如果我们要做图形变换的显示,建议自己先用变换算法计算好,然后直接在页面坐标系下面作图。
3、页面坐标系到设备坐标系之间的变换
这个变换决定了与特定DC相联系的映射模式,影响该DC上的所有图形输出。映射模式本身就是一个缩放变换,决定了画图操作中一个单位的尺寸,映射模式也可以用于平移变换,某些情形下,映射模式会改变x,y轴的坐标原点。首先来了解几个映射模式:
(1)映射模式说明
-------------------------------------------------------------------------
映射模式 描述
-------------------------------------------------------------------------
MM_ANISOTROPIC 每个页面空间的单位映射为程序定义的设备空间的单位。两个坐标
轴的缩放尺寸可以不一致(例如,一个世界坐标系下面的园在指定
设备上可能显示为椭圆)。坐标轴的方向也是程序定义的。
MM_HIENGLISH 每个页面空间的单位映射成设备空间中的0.001英寸。x轴向右,y轴向上。
MM_HIMETRIC 每个页面空间的单位映射成设备空间中的0.01毫米。x轴向右,y轴向上。
MM_ISOTROPIC 每个页面空间的单位映射为程序定义的设备空间的单位。两个坐标轴
的缩放尺寸一样。坐标轴的方向由程序定义。
MM_LOENGLISH 每个页面空间的单位映射成设备空间中的0.01英寸。x轴向右,y轴向上。
MM_LOMETRIC 每个页面空间的单位映射成设备空间中的0.1毫米。x轴向右,y轴向上。
MM_TEXT 每个页面空间的单位映射成一个像素。就是说无缩放。如果也没有平移变换,
则本映射模式下的页面空间和物理设备坐标空间等价。x轴向右,y轴向下。
MM_TWIPS 每个页面空间的单位映射成打印机点的1/20(1/1440英寸)。x轴向右,y轴向上。
------------------------------------------------------------------------------
要设置映射模式,调用SetMapMode,要获得当前的映射模式,调用GetMapMode.
页面空间到设备空间的变换涉及到窗口或者视口中的点,从这个意义上讲,窗口反映了页面空间中的逻辑坐标系统,而视口代表设备空间的设备坐标系统。窗口和视口都包含一个坐标原点和x/y轴。窗口中使用的参数是逻辑坐标,视口中使用的参数是设备坐标(像素)。系统根据坐标原点生成变换矩阵。这就意味着,窗口和视口分别负责给出从页面坐标空间到设备坐标空间映射变换矩阵的一半参数。
根据窗口和视口的坐标轴尺寸(最大坐标值),可以建立一个比例或者缩放系数,用于页面空间到设备空间的变换。对于上面六种预定一映射模式,当调用SetMapMode函数的时候,坐标轴的最大尺寸是由系统设置的,无法更改。其他两种映射模式MM_ISOTROPIC和 MM_ANISOTROPIC下,需要定义坐标轴的最大尺寸,因此调用SetMapMode之后,必须调用SetWindowExtEx和SetViewportExtEx进行设置。特别是在MM_ISOTROPIC映射模式下。必须注意先调用SetWindowExtEx然后调用SetViewportExtEx。
根据窗口和视口的坐标原点,可以建立页面空间到设备空间的线性变换的平移关系。通过函数SetWindowOrgEx和SetViewportOrgEx来设置原点。坐标原点和坐标轴的尺寸没有关系,因此任何映射模式下都可以设置坐标原点,改变映射模式也不会影响当前的坐标原点。由于这两个函数是相关的,所以通常使用一个即可,不必两个都调用。记住一点,无论是否调用这两个函数,设备坐标(0,0)永远是左上角。也可以用函数OffsetWindowOrgEx或者OffsetViewportOrgEx改变坐标原点。下面的公式给出了页面坐标空间到设备坐标空间之间的点的映射关系:
Dx = ((Lx - WOx) * VEx / WEx) + VOx
Dx 设备空间中的点(或者说单位)
Lx 逻辑单位 x (或者说页面空间中的单位)
WOx 窗口的 x 原点
VOx 视口中 x 原点
WEx 窗口的 x轴尺寸
VEx 视口的 x轴尺寸
对于y方向的公式也是一样的。
函数LPtoDP 和 DPtoLP可以用来完成两个坐标空间点的变换的计算。
(2)关于预定义映射模式
6种预定义映射模式中,只有MM_TEXT是设备相关的,其余都是设备无关的。缺省的映射模式是MM_TEXT,即一个逻辑单位等于一个像素。逻辑到设备的映射仅仅是一个平移关系,和程序设置的窗口和视口的坐标原点有关。视口和窗口的坐标轴尺寸都设置成1,从而形成一一映射。如果程序要显示准确的几何图形,可以使用MM_LOENGLIST模式,保证图形的形状在任何显示器和打印机下面都是一样的。如果仍然使用MM_TEXT,则可能在VGA显示器上面显示的一个圆,到了EGA显示器下就变成椭圆了,如果用300dpi的激光打印机输出,则结果是很小的一个圆。
(3)自定义映射模式
MM_ISOTROPIC 和 MM_ANISOTROPIC两个映射模式用于自定义。MM_ISOTROPIC可以保证逻辑单位在x和y方向是一致的。MM_ANISOTROPIC下允许设置成不同。例如一个 CAD 或者绘图程序可以用MM_ISOTROPIC模式,将逻辑单位设置成工程常用的1/64英寸,代码如下:
SetMapMode(hDC, MM_ISOTROPIC);
SetWindowExtEx(hDC, 64, 64, NULL);
SetViewportExtEx(hDC, GetDeviceCaps(hDC, LOGPIXELSX),
GetDeviceCaps(hDC, LOGPIXELSY), NULL);
4、设备坐标空间到物理设备的变换
设备空间到物理设备的变换在很多情况下是唯一的,是由系统控制的,主要的目的是保证设备坐标系的原点准确映射到物理设备的合适位置。比如屏幕上面显示的某个程序,窗口显示的位置要和窗口矩形在显存中的位置对应起来,移动窗口就是改变这个控制窗口输出的矩形在显存中的起点,反映在显示器屏幕上面就是程序窗口的移动。如果是在打印机dc上面画图,则物理设备就是纸张了。这个变换通常是由系统负责控制的。因此没有函数用于设置这个变换,也没有函数获取相关的变换数据。
5、缺省的变换
如果程序创建了一个DC,并立刻开始调用GDI绘图函数,使用的变换过程就是:
缺省的页面空间 -〉设备空间 -〉客户区(物理设备空间)的变换。
除非程序调用SetGraphicsMode 和SetWorldTransform函数,否则世界坐标空间-〉页面坐标空间是单位变换,可以认为没有变换。
页面空间-〉设备空间在MM_TEXT模式下是一一映射,即给定页面空间中的一点,对应设备空间中的相同点。前面已经指出,这个变换不是通过一个矩阵,而是通过用视口的宽度/窗口的宽度,视口的高度/窗口的高度两个公式来计算的。缺省情况下,视口的尺寸是1X1像素。窗口的尺寸是1X1页。
设备空间-〉物理设备(客户区,桌面或者打印机)的变换通常也是一一映射,即设备空间中的一点对应客户区或者打印机输出中的一个单位,目的是保证无论程序窗口如何在屏幕上面移动,最终的输出始终准确地反映设备空间中的图形。
注意MM_TEXT模式比较特殊,它的Y坐标轴是向下的,其它映射模式的Y坐标轴都是向上的。
6、下面用一个例子考察每个坐标变换函数的意义。用classwizard生成一个sdi工程,视类选择从CScrollView派生,然后添加如下代码:
void CSdiscroView::OnDraw(CDC* pDC)
{
//用绿色填充一个圆形区域(中心[200,200],半径150)
CRect rect;
rect.SetRect(50,50,350,350);
CBrush bru;
bru.CreateSolidBrush(RGB(0,127,0));
CBrush *pBrushOld = pDC->SelectObject(&bru);
pDC->Ellipse(&rect);
pDC->SelectObject(pBrushOld);
bru.DeleteObject();
//输出坐标原点
pDC->TextOut(0,0,"(0,0)");
//画出坐标轴
pDC->MoveTo(0,0);
pDC->LineTo(500,0); //x轴
pDC->LineTo(490,5); //箭头
pDC->MoveTo(500,0);
pDC->LineTo(490,-5);
pDC->MoveTo(0,0);
pDC->LineTo(0,500); //y轴
pDC->LineTo(5,490);
pDC->MoveTo(0,500);
pDC->LineTo(-5,490);
}
void CSdiscroView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
CSize sizeTotal;
//设置整个窗口尺寸为1000x1000
sizeTotal.cx = sizeTotal.cy = 1000;
SetScrollSizes(MM_TEXT, sizeTotal);
}
实际上我们所有的画图操作都是在世界坐标系下面,由于没有使用SetWorldTransform,所以世界坐标系和页面坐标系等价,可以认为我们的画图操作就是在页面坐标系中。下面分别添加相应的函数调用,考察每个函数对输出的影响。
1、SetWindowOrg / SetViewportOrg (CDC类成员函数,对应api函数SetWindowOrgEx / SetViewportOrgEx
void CSdiscroView::OnDraw(CDC* pDC)
{
pDC->SetWindowOrg(-100,-50);//或者 pDC->SetViewportOrg(100,50);
//
... //原来的代码
}
窗口没有滚动之前,视口坐标系的原点和窗口坐标系的原点重合,如果滚动窗口,相当于改变视口原点的位置,因此显示在屏幕上面的部分(视口里面的东西)就发生变化。现在我们不滚动窗口,而是调用SetWindowOrg改变窗口原点的坐标,看看发生的变化。
SetWindowOrg(-100,-50)函数调用的意思是把窗口(页面坐标系)中的(-100,-50)点设置成窗口的原点,由于窗口和视口的原点永远是重合的,所以视口的(0,0)点现在就和窗口的(-100,-50)重合,而视口的(0,0)点就是程序客户区的左上角,因此设置的后果就是:绘图的输出向x/y轴的正方向移动了。编译运行以后可以看到,字符串"(0,0)"向右下平移了,好像向上、向左滚动了窗口一样。同样道理,如果SetWindowOry里面使用的是(100,50),则效果等同于向下、向右滚动了窗口。
对应图形如下:
/-----视口的原点
(-100,-50) +-------------+
/-----视口的原点 | (视口) |
+-------------+-------+---->页面坐标系x + +--------+-----------+----->页面坐标系x
|(0,0) | | | | (0,0) | |
| | | | | | |
| (视口) | | | | | |
| (屏幕上的) | | | | | |
| (部分 ) | | 设置SetWindowOrg后 | | | |
| | | ---------------------〉+----+--------+ |
+-------------+ | | |
| | | |
| 整个窗口比视口大 | | 窗口 |
| 有些部分需要滚动 |
| |
| 才能显示出来 | | |
+---------------------+ +--------------------+
| |
\/ Y \/ Y
从效果上SetWindowOrg(-100,-50)和SetViewportOrg(100,50)是等价的。但是使用一下就会发现SetViewportOrg(100,50)以后如果滚动窗口的话,窗口的刷新有些问题,所以在CScrollView里面用SetWindowOrg比较好,对于非滚动形式的窗口,使用SetViewportOrg比较直观一些。窗口对应的就是页面坐标系,也就是逻辑坐标系,视口对应的是设备坐标系。
2、关于映射模式,上面的例子用的是缺省的映射模式MM_TEXT,现在改变一下映射模式,看看有什么变化。去掉设置原点的代码,加上:
void CSdiscroView::OnDraw(CDC* pDC)
{
pDC->SetMapMode(MM_LOMETRIC);
//用绿色填充一个圆形区域(中心[200,200],半径150)
... //原来代码不变
}
运行一下看看,怎么Y坐标轴和圆都不见了,原来这个模式下面,Y轴是向上的。把程序里面的Y坐标都改成负值:
void CSdiscroView::OnDraw(CDC* pDC)
{
pDC->SetMapMode(MM_LOMETRIC);
//用绿色填充一个圆形区域(中心[200,200],半径150)
CRect rect;
rect.SetRect(50,-50,350,-350);
...
//用蓝色输出坐标原点
pDC->TextOut(0,0,"(0,0)");
//画出坐标轴
...
pDC->MoveTo(0,0);
pDC->LineTo(0,-500); //y轴
pDC->LineTo(5,-490);
pDC->MoveTo(0,-500);
pDC->LineTo(-5,-490);
}
运行一看,OK都出来了,但是尺寸比原来小多了。原来每个逻辑单位被映射成0.1毫米。那么圆的直径是300,应该对应30毫米,用尺子在屏幕上面量一下吧,几乎就是30毫米啊:)。不相信,设置成MM_HIMETRIC,天啊,看不到了,可能太小了?把圆的半径加大:
rect.SetRect(50,-50,1350,-1350);
嗯,出来了,直径好像是1300*0.01 = 13毫米。既然这样MM_HIENGLISH和MM_LOENGLISH以及MM_TWIPS就不测试了。需要注意一点,SetMapMode函数调用后,仅仅影响后续的画图函数,而它前面的画图函数仍然按照原来的映射模式输出。所以同一个绘图函数中,可以调用多次SetMapMode改变映射模式,比如绘图单位在英寸和厘米之间,绘图的精度在0.01厘米和0.1厘米之间可以时刻根据需要进行切换。
3、SetWindowExt 和 SetViewportExt。由于它们仅仅在MM_ISOTROPIC 模式下有效,所以这样做:
void CSdiscroView::OnDraw(CDC* pDC)
{
pDC->SetMapMode(MM_ISOTROPIC );
CSize sizeOrg = pDC->SetWindowExt(200,100);
//查看返回值可以发现SetViewportExt返回的是当前屏幕设置的分辨率1024 x 768,不过y是负值
//因为MM_ISOTROPIC模式下,Y轴是向下的。所以记得所有画图代码中的Y坐标用正值!
sizeOrg =pDC->SetViewportExt(200,100);
...//画图代码
}
通过改变两个Set函数中的参数值,发现系统自动管理x/y的比率关系,使圆形保持正确形状。而且图形的大小和参数有关:
假设SetWindowExt(xWin,yWin); SetViewportExt(xView,yView);则系统使用xView/xWin , yView/yWin 两个比值中较小的一个作为x/y两个方向共同的压缩比例。最后图形的大小仅仅和这个缩放系数有关。如果两个系数都大于1,则系统使用1:1比例,并不放大图形。
4、DPtoLP 和 LPtoDP 。这两个函数用于逻辑坐标和设备坐标之间的转换。在MM_TEXT模式下两个坐标是一样的。现在设置成MM_LOMETRIC模式,看看它们的作用。
void CSdiscroView::OnDraw(CDC* pDC)
{
//每个逻辑单位对应0.1毫米设备单位
pDC->SetMapMode(MM_LOMETRIC );
CPoint p(100,200);
pDC->DPtoLP(&p,1);
CString str;
str.Format(" Use DPtoLP : (100,200) -> (%d,%d)\n",p.x,p.y);
TRACE(str);
p.x = 100; p.y = 200;
pDC->LPtoDP(&p,1);
str.Format(" Use LPtoDP : (100,200) -> (%d,%d)\n",p.x,p.y);
TRACE(str);
}
//调试窗口输出
Use DPtoLP : (100,200) -> (313,-625)
Use LPtoDP : (100,200) -> (32,-64)
可见设备坐标系中的(100,200)对应的是逻辑坐标系(窗口)中的(313,-625)一点,逻辑坐标系下面的(100,200)对应设备坐标系下面的(32,-64)点。注意这个变换结果是设备相关的。对于不同的dc得到不同的结果。设置相同的都用屏幕dc,用不同计算机测试,不同的显示器,不同的显示模式设置也会得到不同的变换结果。
这是什么意思呢?就是说窗口中,逻辑坐标(313,-625)在MM_LOMETRIC模式下,对应设备坐标系中X方向距离原点31.3毫米,Y方向距离原点62.5毫米的一个点,那个点在设备坐标系中坐标是(100,200),其实就是MM_TEXT模式下,逻辑坐标系下面的那个(100,200)点。同样道理,逻辑坐标(100,200)点,映射到设备坐标系中,是x轴方向距离原点10mm,y轴方向距离原点-20mm的点(注意方向),那个点的逻辑坐标是(32,-64)。也就是MM_TEXT模式下逻辑坐标系中的(32,-64)点。
最后要说明一点,OnEraseBkgnd(CDC* pDC)里面的DC和OnDraw(CDC* pDC)里面的DC有所不同啊。窗口的滚动对前者没有影响,也就是说无论窗口如何滚动,在OnEraseBkgnd函数中输出的东西永远在视口固定的位置上,不受滚动影响。所以画图的时候,不要把背景和前景混淆了,什么函数就是干什么工作。
-------------------------------------------------------------
End. iwaswzq 2005/1/22
-------------------------------------------------------------
链接:http://blog.163.com/ssb_it/blog/static/44876165200901494359572/