接着上一篇讲
方法篇
首先先啰嗦几句。其实编程的学习是一个渐近的过程,不可能一蹴而就,不可能一口就吃个大胖子,就算你一口就吃了个胖子,那也不正常,迟早会出问题。所以大家学习不要太心急,慢慢来,从基础学起,平时多注意积累,每天积累一点新的东西,一段时间下来,你也就成为高手了。我觉得编程就是玩积木,我们平时写的一些小东西,一些零碎的代码,就是那些小积木,我们以后就用这些小积木去拼大图,去拼自己的理想。所以大家平时一定要多注意积累,和学英语一样地去积累,如果太长时间不去写程序,那么你的能力无疑就会退步。我开这么个博客也就是出于这么一个简单的目的,强迫自己每天都要去接收新的东西,也就是要做到博客几乎每天都要更新内容,这样几天可能没什么效果,但是当你坚持下来,时间长了,效果就非常明显。我博客开了有一年多了吧,博客的每天更新就象征我的知识在增长,所以看着文章越来越多,自己也挺欣慰的。
这里就涉及到另一个问题了,如果我们每天都要让自己接受新的东西,那么这些东西从哪里来呢,我们应该到哪里去找这些新的东西呢?书本上的东西肯定都是非常基础的东西,所以不是我们应该去积累的内容,应该集中时间去把它搞好搞透。除了书本,还有网络这一途径,而且我认为网络是最好的一种积累途径。那么我们应该如何在网上找我们需要的东西呢,这是重点。首先,利用好搜索引擎很重要,因为我们需要的绝大部分东西都可以在上面找到。谈到搜索技巧,那又是一个很大的话题了,这里不细说,我只提一点,我们一定要充分利用种搜索商,比如Baidu、Google、Bing、搜狗等,他们的搜索特点都不尽相同,所以当我们用某种搜索无果后,可以去尝试一下其它几种,说不一定会有惊喜。如果实在是没有搜索到,那么我们学校图书馆的数据库资源也是可以利用的。
有时候我实在找不到方向,在网上闲逛的时候也没能找到什么新鲜的好东西,这时候我就会无聊地在网上乱搜,反正没事干嘛。可是每次我乱搜都会搜出什么东西来,比如说我前几天在百度里搜“C# 范型”,然后就找啊找,还找到了不少关于范型的文章。所以当你无聊的时候也不妨去搜搜,搜什么内容不重要,只要是有点相关的就行,然后可以这点一下那点一下,说不一定就点到一个好地方去了。当你真的找到了一个好地方,比如说某个牛人的博客,那么建议你把该网址收藏起来,以后可以常常去那逛逛,看看有没有什么新的东西,我就是这么干的。根据这个说法,当你看完此文章后就应该把我博客收藏了哦,呵呵,开玩笑啦。
技巧篇
我们平时做一些编程作业的时候,多半是写一些有关算法的程序,需要用到非常多的数据,这时对数据的封装就显得非常重要了。一般说来,可以把性质相似的数据封装成一个结构体,比如点的坐标X、Y、Z可以封装成一个结构体(Delphi里面叫记录类型),再比如我们在后方交会里面有三个在键盘上不能直接输入的变量ψ、ω、κ,我们也可以把它封装到一个结构体里面去,方便我们在编写代码的时候输入这三个变量,因为封装后我们只要输入结构体名再按点号,这三个不好输入的符号就会自动地弹出来,如图:
还有一些其它的数据,我们可以选择性的把它设置成全局或函数里面的变量,这得看它的使用范围而定,后面我会用一个例子来说明。
最近学到了一个好的调试技巧。如果你在一个循环里面下断点,观察循环过程中的执行情况,比如说总共有100次循环,但是你并不关心前80次的执行情况,你只关心后面的情况,那么怎么办呢?常规的做法就是在那个位置下好断,运行80次再观察,不过这样做显得太繁琐了。我们下好断点后,可以在断点的那个红点处单击鼠标右键,这时弹出一个菜单,这以通过这里面的功能来设置我们想要的断点效果,如图:
具体怎么设置大家去操作一下就好,非常简单。
实例篇
前面讲了那么多,现在我们通过一个实例来具体讲解。就以我们摄影测量的编程作业为例子,以C#语言,VS2008为开发环境。首先原理大家肯定都知道了,我就不多说,不知道可以去看教材,我这里是严格按照教材的思路来写的。
我们先在VS里面新建一个C#的Windows窗体应用程序,然后在窗体上拖一个ListBox控件,并设置ListBox的布局属性dock为完全填充Fill,也就是占满整个窗体,窗体有多大,ListBox就有多大。设计好后如图所示:
然后我们双击Form1窗体,进入代码编辑,也就是编写窗体创建事件的代码,我们设置一下程序的标题: this.Text = “单像空间后方交会”;
我们程序采取从文本文件中读取数据的方式,那么需要在程序创建的时候从文本文件中读数据。我们可以把读数据这一功能写成一个函数,再在程序创建的事件中调用该函数,从而实现程序创建时就读取数据这一功能。这里我们先规定文本中的数据格式:点号 控制点坐标X、Y、Z 影像坐标x、y,也就是每行共有六个数据,每两个数据用空格格开。我们先声明几个结构体:
public struct KZPoint //地面控制点坐标 { public double x; public double y; public double z; } public struct YXPoint //影像点坐标 { public double x; public double y; } public struct IData //三个输入不方便的角度 { public double ψ; public double ω; public double κ; }
再在Form1类里面声明一些必要的全局变量:
double m, f, x0, y0, Xs, Ys, Zs;//m,f,x0,y0为内方位元素 int PointNum; //控制点的个数 double LowLimit; //精度控制 double a1, a2, a3, b1, b2, b3, c1, c2, c3; //R矩阵各值 KZPoint[] KZPs; //用户输入的已知控制点 YXPoint[] YXPs; //用户输入的已知像点 IData F3; //三个输入不方便的角度 NNMatrix A, X, L; //平差矩阵, X=[dXs,dYs,dZs,dψ,dω,dκ]T; A[2n,6], L[2n,1] string splitStr = @"\s{1,}"; //正则表达式的分隔符
好,现在我们来写读取数据的函数
//===========================从文本文件中读取初始数据,flilename为文本文件名===================== public void DataFromText(string filename) { StreamReader sr = new StreamReader(filename); //声明文件流类,这个我们上篇已讲到 string text = ""; //text变量用来临时保存我们读的每一行数据 int rows = 0; //rows变量表示该文本共有多少非空行 while ((text = sr.ReadLine()) != null) { if (text.Trim() != "") //Trim方法用来截取两边的空格,如果为空行,截取后就为空了 { rows++; //如果该行不为空,那么rows就加1 } } KZPs = new KZPoint[rows]; //有多少非空行就有多少个点,声明点坐标数组 YXPs = new YXPoint[rows]; PointNum = rows; //PointNum为点的个数,声明在全局变量里面 sr.Close(); sr = new StreamReader(filename); for (int i = 0; i < rows; i++) //用for循环来一行一行地读取数据 { text = sr.ReadLine().Trim(); while (text == "") //如果这行为空就一直向下读直到读到不为空的行,也就是忽视空行 { text = sr.ReadLine().Trim(); } Regex reg = new Regex(splitStr); //C#里面的正则表达式 //以空格为分隔符把这一行文本分成几个部分(我们这里是六个部分)并保存到数组中 string[] list = reg.Split(text); KZPs[i].x = Convert.ToDouble(list[1]); //实现赋值 KZPs[i].y = Convert.ToDouble(list[2]); KZPs[i].z = Convert.ToDouble(list[3]); YXPs[i].x = Convert.ToDouble(list[4]) / 1000; //注意要将单位mm化成m YXPs[i].y = Convert.ToDouble(list[5]) / 1000; } sr.Close(); sr.Dispose(); //释放文件 }
把数据读进来后我们就可以初始化数据,也就是算初值了,同样我们用一个函数来实现这一功能
//===========================初始化数据========================================= public void InitData() { double tempx = 0; double tempy = 0; F3.κ = 0; //三个角度初始值都设为0 F3.ψ = 0; F3.ω = 0; for (int i = 0; i < PointNum; i++) { tempx = tempx + KZPs[i].x; tempy = tempy + KZPs[i].y; } Zs = m * f; //按照教材上面的方法给的初值 Xs = tempx / PointNum; //平均数作为初值 Ys = tempy / PointNum; }
数据准备好了,我们就开始迭代计算吧,先看核心迭代函数
public void Calc() { int times = 0; //迭代的次数 //在LostBox里面显示每次运算的情况 listBox1.Items.Add("=======================第" + times.ToString() + "次迭代========================="); listBox1.Items.Add("Xs=" + Xs.ToString() + "; Ys=" + Ys.ToString() + "; Zs=" + Zs.ToString()); listBox1.Items.Add("ψ=" + F3.ψ.ToString() + "; ω=" + F3.ω.ToString() + "; κ=" + F3.κ.ToString()); listBox1.Items.Add(""); do { CalcR();//计算R矩阵 CalcL();//计算L矩阵 CalcA();//计算A矩阵 CalcX();//计算X矩阵 //算出X矩阵后我们把改正数加到参数里面去,作为下一次迭代的初始值 Xs = Xs + X.Matrix[0, 0]; Ys = Ys + X.Matrix[1, 0]; Zs = Zs + X.Matrix[2, 0]; F3.ψ = F3.ψ + X.Matrix[3, 0]; F3.ω = F3.ω + X.Matrix[4, 0]; F3.κ = F3.κ + X.Matrix[5, 0]; //迭代次数加1 times = times + 1; //迭代次数加1 //在LostBox里面显示每次运算的情况 listBox1.Items.Add("=======================第" + times.ToString() + "次迭代========================="); listBox1.Items.Add("Xs="+Xs.ToString()+"; Ys="+Ys.ToString()+"; Zs="+Zs.ToString()); listBox1.Items.Add("ψ="+F3.ψ.ToString()+"; ω="+F3.ω.ToString()+"; κ="+F3.κ.ToString()); listBox1.Items.Add(""); } while ((Math.Abs(X.Matrix[0, 0]) > LowLimit) || (Math.Abs(X.Matrix[1, 0]) > LowLimit) || (Math.Abs(X.Matrix[2, 0]) > LowLimit) || (Math.Abs(X.Matrix[3, 0]) > LowLimit) || (Math.Abs(X.Matrix[4, 0]) > LowLimit) || (Math.Abs(X.Matrix[5, 0]) > LowLimit)); //这个do...while循环实现迭代,其中由while条件来判断什么时候结束迭代,其中LowLimit是我们设置的精度,定义在全局变量中 }
好,核心函数里面我们只有那么几个计算矩阵的函数功能没有实现了,我们再逐一实现就好,照着书上的公式抄上去就行了,非常简单
//===========================计算R矩阵========================================== public void CalcR()//用到的是教材32页公式3-9 { a1 = Math.Cos(F3.ψ) * Math.Cos(F3.κ) - Math.Sin(F3.ψ) * Math.Sin(F3.ω) * Math.Sin(F3.κ); a2 = -Math.Cos(F3.ψ) * Math.Sin(F3.κ) - Math.Sin(F3.ψ) * Math.Sin(F3.ω) * Math.Cos(F3.κ); a3 = -Math.Sin(F3.ψ) * Math.Cos(F3.ω); b1 = Math.Cos(F3.ω) * Math.Sin(F3.κ); b2 = Math.Cos(F3.ω) * Math.Cos(F3.κ); b3 = -Math.Sin(F3.ω); c1 = Math.Sin(F3.ψ) * Math.Cos(F3.κ) + Math.Cos(F3.ψ) * Math.Sin(F3.ω) * Math.Sin(F3.κ); c2 = -Math.Sin(F3.ψ) * Math.Sin(F3.κ) + Math.Cos(F3.ψ) * Math.Sin(F3.ω) * Math.Cos(F3.κ); c3 = Math.Cos(F3.ψ) * Math.Cos(F3.ω); } //===========================计算L矩阵========================================== public void CalcL()//用到的是教材62页公式5-4 { L = new NNMatrix(2 * PointNum, 1); double JSx = 0; double JSy = 0; for (int i = 0; i < PointNum; i++) { //算近似值用到教材34页公式3-15,其中加上了x0,y0不为0的情况 JSx = x0 / 1000 - f * ((a1 * (KZPs[i].x - Xs) + b1 * (KZPs[i].y - Ys) + c1 * (KZPs[i].z - Zs)) / (a3 * (KZPs[i].x - Xs) + b3 * (KZPs[i].y - Ys) + c3 * (KZPs[i].z - Zs))); JSy = y0 / 1000 - f * ((a2 * (KZPs[i].x - Xs) + b2 * (KZPs[i].y - Ys) + c2 * (KZPs[i].z - Zs)) / (a3 * (KZPs[i].x - Xs) + b3 * (KZPs[i].y - Ys) + c3 * (KZPs[i].z - Zs))); //给L矩阵赋值 L.Matrix[2 * i, 0] = YXPs[i].x - JSx; L.Matrix[2 * i + 1, 0] = YXPs[i].y - JSy; } } //===========================计算A矩阵========================================== public void CalcA()//用到教材63页公式5-8及66页公式5-9b { double a11, a12, a13, a21, a22, a23, a14, a15, a16, a24, a25, a26, Z1; A = new NNMatrix(2 * PointNum, 6); for (int i = 0; i < PointNum; i++) { Z1 = a3 * (KZPs[i].x - Xs) + b3 * (KZPs[i].y - Ys) + c3 * (KZPs[i].z - Zs); a11 = (a1 * f + a3 * YXPs[i].x) / Z1; a12 = (b1 * f + b3 * YXPs[i].x) / Z1; a13 = (c1 * f + c3 * YXPs[i].x) / Z1; a21 = (a2 * f + a3 * YXPs[i].y) / Z1; a22 = (b2 * f + b3 * YXPs[i].y) / Z1; a23 = (c2 * f + c3 * YXPs[i].y) / Z1; a14 = YXPs[i].y * Math.Sin(F3.ω) - ((YXPs[i].x * (YXPs[i].x * Math.Cos(F3.κ) - YXPs[i].y * Math.Sin(F3.κ))) / f + f * Math.Cos(F3.κ)) * Math.Cos(F3.ω); a15 = -f * Math.Sin(F3.κ) - YXPs[i].x * (YXPs[i].x * Math.Sin(F3.κ) + YXPs[i].y * Math.Cos(F3.κ)) / f; a16 = YXPs[i].y; a24 = -YXPs[i].x * Math.Sin(F3.ω) - ((YXPs[i].x * (YXPs[i].x * Math.Cos(F3.κ) - YXPs[i].y * Math.Sin(F3.κ))) / f - f * Math.Sin(F3.κ)) * Math.Cos(F3.ω); a25 = -f * Math.Cos(F3.κ) - YXPs[i].y * (YXPs[i].x * Math.Sin(F3.κ) + YXPs[i].y * Math.Cos(F3.κ)) / f; a26 = -YXPs[i].x; //给A矩阵赋值 A.Matrix[2 * i, 0] = a11; A.Matrix[2 * i, 1] = a12; A.Matrix[2 * i, 2] = a13; A.Matrix[2 * i, 3] = a14; A.Matrix[2 * i, 4] = a15; A.Matrix[2 * i, 5] = a16; A.Matrix[2 * i + 1, 0] = a21; A.Matrix[2 * i + 1, 1] = a22; A.Matrix[2 * i + 1, 2] = a23; A.Matrix[2 * i + 1, 3] = a24; A.Matrix[2 * i + 1, 4] = a25; A.Matrix[2 * i + 1, 5] = a26; } } //===========================计算X矩阵========================================== public void CalcX()//用到了教材63页公式5-6,主要为矩阵运算,矩阵运算代码及用法见附录中的链接 { NNMatrix AT = new NNMatrix(6, 2 * PointNum);//声明矩阵A的转置为6行,两倍点个数列 NNMatrix ATA = new NNMatrix(6, 6); //ATA为A的转置乘以A NNMatrix ATA1 = new NNMatrix(6, 6); //ATA1为ATA的逆 NNMatrix ATA1AT = new NNMatrix(6, 2 * PointNum); //ATA1AT为ATA1乘以A的转置 X = new NNMatrix(6, 1); AT = NNMatrix.Transpos(A); //求AT ATA = AT * A; ATA1 = NNMatrix.Invers(ATA); //求ATA的逆 ATA1AT = ATA1 * AT; X = ATA1AT * L; }
好了,到目前为止我们所以的函数模块都完成了,现在我们来在程序创建的时候调用他们,我们按Shift+F7来到窗体设计视图,双击窗体,写如下代码
private void Form1_Load(object sender, EventArgs e) { this.Text = "单像空间后方交会"; DataFromText("data.txt"); //读取数据 //初始化参数 x0 = 0; y0 = 0; m = 50000; f = 0.0281359; LowLimit = 0.000029; //初始化数据 InitData(); //进行迭代计算并显示结果 Calc(); }
OK,到这里这个程序就算是完成了。我们将数据都填好到data.txt这个文件中,然后把data.txt拷到和我们程序一个目录下,然后我们F5调试运行,效果如图:
拓展篇
虽然我们这个程序最基本的功能都已经实现了,但还是有一些地方可以改进。比如说我们在程序创建时候在代码里面设置x0,y0,m,f以及精度LowLimit的值,这样就不太好,因为这样使得使用我们程序的人不能自己去定义这些参数,所以还是应该把这些数据放到文本文件中去,由用户自己输入,我们再调用函数去读这个数据。也就是我们需要DataFromText这个函数里面做些修改,使其能达到我们的目的,这个大家自己去写。
注意观察代码就会发现,我们程序只是从特定的文件data.txt中读取数据,那么要是我们还有其它数据呢,怎么办,难到要我们去把它名字都改成data.txt再来运行程序么,所以这个地方需要我们再改进。我们其实可以达到这么一个效果,我们把数据文件拖到程序窗体中去,然后就可以进行对这个文件进行运算。其实实现这个功能一点都不难,几行代码就搞定了。跟着我一步一步来做吧。
我们来到窗体设计视图,选中窗体,在属性那一栏中切换到事件(点一下那个闪电状的图标),我们找到DragEnter这一事件,然后双击进去写代码:
private void Form1_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Link; else e.Effect = DragDropEffects.None; }
同理我们找到DragDrop事件,双击进去写上如下代码:
private void Form1_DragDrop(object sender, DragEventArgs e) { //MessageBox.Show(((System.Array)e.Data.GetData(DataFormats.FileDrop)).GetValue(0).ToString()); listBox1.Items.Clear(); DataFromText(((System.Array)e.Data.GetData(DataFormats.FileDrop)).GetValue(0).ToString()); InitData(); Calc(); }
好了,该功能就这么简单地被我们实现了,不妨运行一下,然后拖个文件试试。咦,好像可以哦,是不是很爽?
最后我们还可以设置程序的图标,在Form的Icon属性里面,选择一个Ico格式的图标文件就好。再设置一下程序,使其运行的时候窗体居中,也就是位于屏幕正中,在Form的StartPosition属性中选择CenterScreen就好。还可以有其它的设置,比如说可以设置程序最小化显示在系统托盘等等,大家自己去想吧。