0%

之前学了不少东西,今天以学习笔记的形式分享出来。学习笔记不适合作为入门的教程。

Git/Github使用方法学习笔记

从例子直观理解git

git是一个开源的分布式版本控制系统。如果你去网上搜git的定义,一定会搜到不少这样精确又令人看不懂的定义。今天,我准备根据我的笔记,来友好地介绍一下git相关的知识。

假设你要玩一个流程很长的RPG,你打了一天,打到了半夜。这个时候该做什么呢?没错,存档。不然下次就要从头开始玩游戏了。

除了继承之前的游戏进度,存档还有一个好处:如果你不小心输了,可以从上次存档的地方开始重新玩。甚至你玩着玩着发现进错了结局,可以从好早的档重新开始玩。存档,除了保存进度,还能记录下游戏发展向不同分支的关键节点。

现在再做一个很不现实的假设:你要玩一个无聊又冗长的RPG游戏,但是通关了之后你会得到一大笔钱。为此,你只开始关注能否尽快通关,而不去体验游戏的内容了。你在单位的电脑玩,在网吧的电脑玩,还在家里的电脑玩。你把所有好友都动员起来一起帮你玩游戏。但是每个人的进度不一样就没意义了。于是,你们想了个办法:大家玩完后,把存档放到网络上,下一个人就可以用最新的文档来了。

大家决定按贡献度分钱。同时,大家还想方便地交流每个人玩的具体情况,碰到了什么问题。于是,大家扩展了存档功能,不仅让存档保存游戏内容,还保存了每个人的保存时间、进行了多少游戏进度、想分享的游戏攻略等有意义的信息。

这就是git用法的一个形象的解释。在我的理解里,能够保存项目进度关键节点,并记录有意义的日志信息,就是一个版本控制,不同的人能在不同的设备上同步所有其他人的进度,就是分布式。源代码给你看,就叫开源。合起来呢,就变成了开源的分布式版本控制系统。

这个例子基本上是我自己想出来的,我在想这个例子的时候会主动思考git有哪些性质,这些性质与另一个通俗易懂的事情的哪些性质比较类似。通过这个过程,我反而加深了自己对git的理解。如果读者在学完git后,能够真正看懂这个例子,并且举出一个自己的例子,那就说明你基本理解了git。

这里推荐个比较有名的博客,上面有git的教程。我是跟着这篇博客上的教程入门git的。该教程举了个日志的例子(如下图)。该日志记录的内容,代表了git的大部分操作。可以暂且认为git就是一个自动生成这样格式的版本日志的程序。说得那么好听,git就是个自动记事本。

git的操作

一定要自己开一个项目去学习git,光看不动手很难学会

为了节约写作时间,让我能够写更多的学习笔记,最大化造福他人,我决定牺牲博文质量,偷懒地放上之前做的ppt。

Hello git!

完成上图中的操作,你就可以像写完了一门新语言的hello world程序一样,说自己已经掌握git了。

这么说有点夸张,但也不假。上图介绍了一次完整的git使用流程。首先,需要使用git init初始化一个git仓库,呼叫git这个记录员来帮你管理版本,不然你的文件夹只是一个普通的、无法进行其他git操作的文件夹。之后,随便新建并写入一个文件(假设它名字叫readne.md。用git add readme.md告诉git你加了这个文件。最后用git commit -m "改动信息"来让git把加的文件给记录下来。

我学到这里的时候十分疑惑:为什么要先add,再commit才能把文件记录下来。为什么不一次性把文件记录下来呢?听说还有个叫github的东西,我’commit’完,还要再进行一步操作才能把文件传到github上面。为什么我想在网上存一份文件要这么多步操作啊!

这里,考虑到知道git的人都知道github,而对git操作有着许多的疑惑,我打算用一张简单的数据流图(该图及后文的称呼并不严谨,仅供学习理解)来介绍git、github的关系,以便于接下来的理解。git与github的具体关系还是放到后面介绍。

其实啊,git init这条语句看上去没干什么,实际上在你的电脑里新建了一个“文件夹”(逻辑上的,而不是实际的)。这个“文件夹”是本地git仓库。而这个仓库呢,又分成两个分区:暂存区和实际的版本库。可以认为git是一个管家,add进缓存区,就是叫管家把事情记在脑子里;commit进版本库,就是叫管家用水笔把事情记在本子上——这样这条记录就确实储存下来,不好销毁了。那么,使用add把文件放入暂存区的好处就显而易见了:有的时候我们要改/加的文件很可能出错,我们再提交之前,需要把文件检查一遍,这样出了错就能及时更改。一旦文件commit了,更改起来就麻烦了,之前的错误记录也难以消除。

而github一类的托管平台,则又可以视为一个文件夹。这些文件夹可以看成是网盘,它们由运营平台提供,以便于你能用互联网在不同设备上访问自己的仓库。目前对于git、github之间的关系,add与commit之间的关系理解到这个程度就行了。

git提供了一个十分使用的命令——git status。该命令不会对git仓库进行任何的修改,而仅仅会输出当前git仓库的状态。如上图所示,修改已有文件,add,commit后,git status都会输出不同的信息。要查看具体的修改细节,可以使用git diff

如最开头所说,我们使用版本控制程序的一大原因,就是希望像RPG读档一样,能够让所有文件回到之前某个保存下来的状态中。这样的操作在git中叫做版本回退。上图给出了一个例子,我们不小心在文件里做出了错误的修改,还commit了。这个时候该如何把文件回退呢?

首先,可以使用git log,查看所有提交记录。之后,可以使用git reset --hard <要回退的版本>指令来进行回退。最新的版本叫做HEAD,上一个版本叫做HEAD^,上上个叫做HEADT^^……依次类推,可以快速地回退到上一个版本。除了用这样的方法指定版本号外,还可以用commit的id来指定版本(commit id在log中查找)。指定id的时候不需要把id输全,只需要输入前几位,保证不冲突就行。

除了commit后发现错误想回退,还有一些程度较轻的版本回退情况:不小心修改了文件而没有add,或者已经add了。对于这两种误修改,都有对应的解决情况:如上图所示,如果文件没有add,使用git checkout -- <filename>就可以把对应文件恢复到暂存区的最新状态(最后一次git commit或者git add的状态)。如果文件add了,使用git reset HEAD <filename>就可以把git暂存区中的文件恢复到没有add之前的状态(最后一次git commit的状态),这等于回到了上个问题,文件没有add该怎么恢复。再一次执行git checkout -- <filename>就可以把文件的内容也彻底恢复回去了。

git add是一个很神奇的指令。它的名字虽然叫add,但实际上不管是新建文件、修改文件、还是删除文件,都需要用git add来更新文件在暂存区中的状态。因此,与其叫做add,不如把这个命令称作update比较好。那么,现在问题来了:如上图所示,有一个文件,我不小心让它进入了git暂存区,我想在文件系统里保留它,但不想让它进入git仓库,该怎么操作呢?这个时候就要使用git rm --cache <filename>。这个叫rm的操作是真正意义是上的remove了,而不像add一样有一点歧义。如果想让文件从文件系统中也消失 ,可以用git rm -f <filename>

刚刚我们碰到了这样一个应用场景:有一个文件,我想在文件系统保留它,却不想让它进入git仓库。这些文件可能是我们为了调试而编写的脚本文件,我们不希望这些乱糟糟的文件上传到公开的仓库中。如果我们不做任何处理,每次输入git status都会看到这些在文件夹中存在却没有进入git仓库的untracked files. 该如何让这些碍眼的提示消失呢?这个时候,可以在git仓库的根目录下创建一个.gitignore文件。该文件用于描述不被git跟踪的文件,描述的形式可以是直接描述文件的全名,也可以用*来模糊匹配,还可以直接忽略一整个文件夹。上图的例子中,我们如果在.gitignore中加入test.py,再输入git status就不会提示有文件没有加入git仓库了。

使用github初始化仓库的话,可以自动创建一个.gitignore文件。这个文件也可以参考别人的仓库里的来写。

讲了这么久的git,终于可以来讲github了。git本身是分布式的,每台电脑都存储了所有的git仓库版本信息。但是,实际上使用的时候,为了在互联网上同步信息,我们一般需要托管平台的帮助。github就是这样一个平台。先直接看一看和github有关的操作吧。

不管怎么样,所有git操作都是基于一个git仓库。这个git仓库可以是我们本地的,也可以是github上面创建的。

如果本地有了一个仓库,那只需去github上创建一个空的同名仓库,再用图中的第一条命令把远程仓库和本地仓库关联起来。

但是,使用https的方法通信,很容易出现超时的问题。如果之前不小心对https格式的地址进行了绑定,可以先remove掉。

与github关联的正确方式是使用ssh,这是一种更安全高效的连接方式。先像上图一样再本地生成ssh密钥。

再把公钥放到github上。

这样就可以正常把本地仓库和github的仓库关联起来了。如果本地没有仓库,而是去克隆github上面的仓库,则可以使用git clone指令。

这里特别要说一句。如果只是用别人的代码,clone仓库本身就行了。如果想要对别人的仓库进行贡献,则最好先去github上面点一下fork,分支出一个完全一样的,属于你自己的仓库,再clone这个属于自己的仓库。因为对别人的仓库是无法进行修改等操作的。

和github的仓库进行关联后,还需要学习一些和分支有关的知识,才能开始使用github。以上图为例,在git里创建分支并在不同的分支开发,就好比在同一台电脑,同一款游戏里,不同的玩家玩到了不同的进度上。当前电脑(HEAD)由玩家dev控制,他玩的进度比较超前。之后轮到玩家master来玩,他看dev的进度很前,于是直接拷贝了dev的游戏存档,这样两个人的游戏进度就完全一样。之后master把dev的存档删了,因为这份存档已经没用了。在这个例子里,两个分支都是线性进行,因此只需要把旧的分支向新分支同步就行。但是,实际开发时,很可能两个分支往不同的方向发展,之后这两个分支还需要进行同步。这个时候就需要对两个分支的信息进行细心的比对,才能把两个分支的代码给融合起来。

开始那副图片描述的内容,可以用上图中的操作来实现。

这页ppt做得不好,它实际包含了两类不太相关的内容:

首先是对开始那些操作的收尾:把副分支向主分支合并后,副分支一般没什么意义了,这个时候可以直接删除。

有了分支的知识,我们就可以理解本地git仓库如何与github上的仓库交互了。我们本地仓库和github上的仓库其实是同一个仓库的两个分支,我们需要不断令这两个仓库同步。

如果本地仓库较新,应该用push,等于是把本地的内容向github上的内容合并。

如果github上的仓库较新,应该用pull,等于把github的内容向本地更新。

这里的命令写得不够具体。理论上两个操作的用法都是git pull/push <remote-repo-name> <branch>这样的格式的。远程仓库名一般叫做origin。但是可以通过--set-upstream可以指定push和pull的默认远程分支。直接git push/pull的时候命令行里会报错,按照提示设置upstream即可。

不管是开发自己还是别人的开源项目,都建议创建一个新的分支开发。开发完一项功能后,把新分支推送到github上,再利用github的pull request把分支合并到主分支上。这种开发方式使得在添加新功能时,所有人都能看到哪些代码进行过修改,有利于多人协作。

更新github上的分支的几分钟内,github会主动提示你要不要进行pull request。或者是点击pull request一栏主动进行”New pull request“,都可以开启一个新的PR。

PR的标题和内容一般都有格式要求。如果是向别人贡献代码,一定要向别人的要求对齐。输入完对PR的描述后,就可以正式创建一个PR了。

(这里没截图)PR的审阅者可以在PR的file changes里面查看有哪些代码被修改,确认无误后可以点击approve,并在整个pr的最后面点击merge。这样,一个带有新功能的分支就正式被并入主分支了。

我之前就接触了github,一直用github gui进行个人项目的同步。但是push/pull/commit等概念一直没有弄明白。通过这次彻底的学习后,我建立起了git及github的知识框架,才算是彻底理解了这些工具的使用方法。这个知识框架可以总结为上图。如果一个人学完git的相关知识后,能在脑中建立起类似上图这样的数据流图,那么就可以说是基本理解了git什么多命令究竟是在干什么。

首先,要理解先得存在本地git仓库,在线github的git仓库才有意义。通过add命令,可以把文件夹里的文件托付给git系统进行管理;通过commit命令,可以让git把当前的内容”存档“。

为了使用github,需要把本地仓库和远程仓库关联。如果是从现有的远程仓库开始构建代码,则用git clone;如果是本地仓库已经有了代码,仅需用git remote add关联起远程仓库。后续所有的git pull/push操作都是在同步本地和远程的git仓库分支。

为了对别人的代码进行贡献,需要fork别人的仓库,形成一个自己的仓库。对自己的仓库进行更新后,再以pr的形式请求更新别人的仓库。但开发个人/私有项目时,也建议使用pr的形式更新代码。

git的笔记差不多就这么多。还有很多知识没有在笔记里提及。比如如果合并存在冲突的代码。其实在有了整体的概念后,这些细枝末节的知识反而都很好理解了。

我反正在学完了git的基础知识后,就再也不用命令行,而是用一些其他工具来进行git仓库的管理了。我现在在使用vscode的git管理系统。里面add、commit都可以方便地进行操作。冲突合并也就是点一点按钮的事情。

我的博客的分类/标签管理方法

之前我把博客里所有博文的分类、标签进行了重构,一直没有提及。这里简要介绍一下重构的内容,及重构后分类和标签的使用依据。

之前的分类和标签貌似是英文。我也不知道自己怎么想的。既然博客的内容全是中文,那么读者也全是会中文的人,用英文的分类/标签没什么意义。当然,我是发现自己想给新分类取名,却怎么也不知道如何用英文来表达某个中文词语的意思时,才准备把所有分类/标签改成中文的。

现在的分类是按照文章的性质进行分类。目前分类仅有两级,直觉上分三级可能更好一点,不知道以后还有没有改进的空间。

目前有学习/杂谈/经验分享/记录这几大类。”学习“就是分享一些客观正确的事情。”工具用法指南“包括软件的安装使用方法,这些是最没有技术含量、原创性,最死板的内容‘;”知识“是我对某一学科的知识的再表达,主要是巩固我自己的学习,内容有一定的原创性,但根本目的还是复述别人的知识,这个名字取得不好,改成”知识分享“或许更合理一点;这篇文章所属的”学习笔记“可以说是劣化版的”知识分享“,原创性稍高一点,但完成度低,内容连贯性差,不适合作为第一手学习资料,适合作为参考,或者用于我口头讲授的素材。

”杂谈“就是中小学生作文。有的文章是有一些深意的,有的文章是随便写的,”趣文“里的就真的是写来玩的。

”经验分享“和”杂谈“一样,是原创性极强的作品,主要是我个人的见解。但提出系统性的个人见解非常困难,还需要保证不与客观事实或参考资料有出入,目前文章较少。”经验分享“这个名字也不是很恰当。说不定以后”经验分享“专门来放一点原创性强,但是不够有深度和完成度,随便写的一些经验分享文章。专门开一个”我的理论“来放最具原创性和深度的文章。

”记录“里包含了专门对我完成的某件事做描述的文章。这里有些东西是为了好玩而写,有些东西是为了丰富个人简历,专门写给别人看的。

分类只能给所有文章从性质这一个维度上进行一个大概的分类,但我感觉用标签检索文章更加合适。标签比分类更加灵活,可以像分类一样描述文章性质(日记),也可以描述文章涉及的内容(数学),还可以标识一类专题(未来可能出现的”博士毕业论文“)。目前标签我是想到什么放什么。

用标签的时候会有一个问题:理论上标签越多越好,但是如果某一天突然想出一个新标签,但以前有些文章可以套入这个标签,却还没有加,我该怎么给以前的文章加标签呢?或者说我想出了一个新标签,却忘了以前想出了类似的标签,取了个不同的名字,这样标签该如何对齐呢?感觉标签使用起来非常麻烦。一个简单的方法是怠惰处理,以前的内容不管了,只管现在和未来的标签是正确的。

写这篇学习笔记的感想

确切来说,要分成对”学习笔记“的感想和对写这篇文章的感想。

我之前一直不肯写学习笔记。一个人真要学会一个东西,不一定要像”学霸“一样记下花花绿绿的笔记,只需要在关键处进行顿悟式的理解,再在脑中建立知识框架即可。在学一个东西的时候(尤其是学习需逻辑思考而非记忆事实的知识),很可能什么都不用记,或者只是对关键知识进行了演算或者分析,留下一些草稿。记录笔记,其实应该是一个向他人表达知识的过程,应该把这个过程用于巩固知识的理解。在这一过程中,把问题讲清楚一般是十分困难的,因为我们的大脑此时都用于如何让脑中的知识变通顺,而无暇顾及他人能否理解我们在讲什么。只有完全学懂了,才可能写出漂亮的笔记或教程;若是对知识的理解更高一层,才可以写教材。我之前一直认为我写的学习笔记没用,因为这些学习笔记属于我个人学习的副产品——正如我们呼出的二氧化碳一样。要把它们转换成能给人看的东西,需要花很多心血。我自己学东西的时候,经常去网上查资料,总能查到这样的一些学习笔记:这些学习笔记缺乏逻辑性,完全不能用来进行自学,看它们就是在浪费时间。我不希望自己的发出来的学习笔记干扰他人的学习。但最近我开始愿意发出自己写的学习笔记了。

这里再次强调一遍学习笔记所属的性质。我之前”知识“(未来将改成”知识分享“)中的文章,是以现有教材为素材,记录我个人分析与总结的文章;而学习笔记,则应该算是自己写出来的学习素材,我加上了一些描述以使得它们勉强可读。这两类文章本质上来说都是我自学的时候用的。

现在开始讲我愿意发学习笔记的原因。首先是因为最近我有分享知识的需要,制造了一些用于讲授的学习素材。这些素材只要加工一下就能发出来让别人看了。有了积累,不发表出来,实在太亏了。我是不愿亏的人,很想有了东西就发表出来。

发学习笔记的最大问题是需要加入一些描述性的文字。写这篇学习笔记之前,我的想法是随便写点文字描述一下就得了。但是,写着写着,我发现不多写点字是不可能把抽象的知识描述清楚的。最后,我只好在文章质量上进行妥协。文字优美与否、通顺与否我已经关心不了了,我只能尽可能让文章可读,并且赶快把文章写完。赶快做出了一个能用的东西,比憋半天什么都做不出来要好。

我之前不想发学习笔记的另一个原因是,我把学习笔记和教材搞混了。教材需要对知识进行面面俱到而精准无误的描述。这样的工作量的巨大的。现在我想通了:学习笔记就是参考资料,我也强烈建议读者不要把学习笔记当成自学的资料来用。学了一会儿,或者知识掌握得已经差不多后,再来以批判的眼光看学习笔记,哪里写得好,哪里有疏漏。这样看待学习笔记才是对的。

再吐槽一下写这篇文章的感想。

我讲这几页ppt的时候感觉十分轻松。我感觉随便写点内容就能把ppt串起来了。但实际写文章的时候却不是这样的,我感觉ppt缺斤少两,逻辑混乱,得加大量的文字才能把话说清楚。最后,我花了比预计多得多的时间才把这篇博文完成。

可见,基于语音的授课的教学效果还是很好的。加上答疑机制的存在,理想情况下教师能够把要讲的知识完全传授给学生。只要学生稍微会学习,老师有强大的表达能力,就能把知识通过上课这种方法传授出去。可惜,大部分人不会教学,逼得学生通过阅读的方式自学。阅读式学习对教授者和学生的要求都更高。真的要感谢那些写出优秀教程/教材的人。

再对我自己和读者强调一句:凡是以实践为目标的技术,一定要在实践中学习,不然学不进去。我们从小到大写那么多作业,就是为了利用实践来加强对知识的理解。对于一种编程技术,学习并验证学习情况的最好方法就是把这个技术用出去。

写这篇学习笔记太累了。如果还要保持产出,我只能进一步降低学习笔记的文章质量了。

星期五的下午

星期五的下午。

教室里的气氛逐渐热闹了起来,老师也放慢了语速。

下课铃一响,就像刚煮开的水一样,教室里顿时充满了吵闹声、桌椅的碰撞声。

几个同学笑嘻嘻地朝我冲了过来。

“去你家玩电脑吧!”

“可是,我爸爸说上周来的人太多了,还骂了我。”

“没事,今天我们不会那么吵的。”

“好吧,那你们可要早点回去啊。”

尽管带着一丝畏惧,我还是急不可耐地带着同学回到了家里。


星期五的下午。

虽然老师不在,教室里依然鸦雀无声,连下课铃也没有打破这份沉静。

我慢悠悠地收拾着卷子,一位好友前来搭话。

“今天你考得怎么样?”

“还好吧,后面大题也错了不少。”

“唉,明天又要考一天的理综。”

“是啊。”

寒暄几句后,好友先行离去。桌面收拾干净后,我一边想着晚自习和周末的计划,一边拖着脚步离开了教室。


星期五的下午。

吃过午饭,宿舍里的四个人都懒洋洋地躺坐在椅子上。

“你们论文写得怎么样了?”

我发起了午后的闲聊。

“马上就要交了,我的导师都不理我,很烦。”

”我还在写。“

“我的论文快写完了。对了,晚上继续打lol吧?”

“额……好吧。”

稍作思考后,我还是勉强答应了下来。


回过神来,现在是星期五的下午。

开完冗长的周会后,楼道里地响起了接连不断的脚步声。

“周五办公室里的气氛都欢快了不少啊!”

“是啊。”

只听一位同事在向旁边的人吐槽着。

“明天就是周末了,今天加班的心情肯定是最差的了。大家都想着早点回去啊……”

未听完他们的谈话,我就关掉电脑,背起书包,急匆匆地离开了办公室。骑上山地车后,我飞快地蹬着踏板,追过了一辆又一辆电动车,赶回了家里。

桌上不是装着XP系统,内存只有1G的老台式机,而是一台崭新的PS5。

今天的班上完了,再也不用想着如何准备考试了。想到这里,我轻快地打开了游戏机。

下个月也没有什么毕业论文要交。下周一别人吩咐什么,我做什么就是了。这个周末是属于我的。我满怀期待地畅想起周末的娱乐计划。

多好啊!没有人会担心我的视力,不让我玩游戏。

多好啊!没有令人压抑的考试,没有做不完的试卷。

多好啊!没有迷茫而焦虑的每一天,没有为远离压力而强作欢笑的逃避。

我过着之前梦寐以求的生活。平时高效率地工作、学习,周末无拘无束地玩耍。我发自内心地享受着现在的时光。

我可以像儿时一样,把全部时间投入到游戏里。

我可以像儿时一样,创造并沉浸于自己的内心世界。

我可以像儿时一样,不去担心明天的到来。

——可惜我不是小孩子了。

写博文的感想

用简练的文字把意思表达清楚,这是写文章必要而又需要花时间练习的事。熟练之后,文章简练了,写文章的时间也会缩短。恰好我最近常常有很多感想,却又担心时间不够,不敢下笔,我有着锻炼写短文的需要。正逢今天时间有多,我可以快速地写一篇小短文,抒发一下心里的感想,练习一下写作。写博客又不是什么特别的任务,没必要非得长篇大论。我争取尽快习惯简明扼要地表达思想,多多记录一些所见所闻、所思所想。

文章很简短,我却边想边写,写了很久才写完。恐怕是因为文章中融入了不少回忆与感情吧。蕴含在文字中的微妙的感悟,只有有经历的人才能看得出来吧。

Pytorch安装教程(包含对齐cuda、cudnn版本,VSCode中Anaconda的设置)

安装一个编程环境/编程库的过程是无比烦人的。安装环境时不可避免地涉及一些和该环境有关的知识,而配环境的人多半是新手,面对未知的知识时总会碰到许多麻烦。比如第一次配和C++有关的库的时候,新手就不知道.dll应该加入环境变量,.h应该加入include。而在写教程的时候,也一般都是新手在写教程,老手们对配环境这一“简单”的事情不屑一顾。因此,我们一般只能在网上看到新手写的配环境教程,上面都是按部就班地介绍一步一步的操作,讲不出每一步操作为什么要这样做,那些操作可以更改或者跳过,碰到了问题又怎么样。教程的不详细同样是配编程环境令人感到烦人的原因。

基于上述情况,配编程环境考察的是人利用搜索引擎的能力。在配环境时,应该多参考几篇文章,有英文阅读能力的应该去参考官方给的配置教程。出了问题把问题的出错信息放到搜索引擎上去查。一般多踩几次坑,多花点时间,环境总能配好。

也正是基于这些原因,我觉得写一篇环境配置的文章很没有意思。没有技术性和原创性、繁琐而无趣。但恰逢我第三次配置pytorch环境,一切都轻车熟路了。我准备在等待文件下载安装之余截几张图,水一篇文章。

准备

去搜索引擎搜索pytorch安装 你的操作系统。比如我这次是在win10上安装,就选择输入”pytorch 安装 win10”.当然,一般会用到Anaconda来安装python上的一些库。在搜索中加入”anaconda”关键字可以搜到更多的教程。

当然,官方网站上也已经给出了不少信息了:https://pytorch.org/get-started/locally/

我的博客是随手写的,看这篇文章可能解决不了所有问题。正如前言中所说,应该多参考几篇教程。

安装Anaconda

Anaconda可以让用户更好地管理python包,配置pytorch需要先安装Anaconda。我说不出它的优点,反正大家都在用,我也一直在用。

https://www.anaconda.com/products/individual#Downloads 这里可以下载Anaconda

如果使用的是Windows,安装以后,点击任务栏中的搜索框,搜索Anaconda,打开Anaconda Powershell Prompt (Anaconda)或者Anaconda Prompt (Anaconda)。Ubuntu貌似进入命令行进入了Anaconda。

在conda命令行中输入conda create --name env_name python=3.7

env_name是虚拟环境的名字。装Python库,就好像在一个新房子里放家具一样,时间久了东西越来越乱。Anaconda给了你无限创造新房子的机会。你可以在一个完全没有“家具”,甚至连Python都没有的虚拟环境里,重新配置库文件。env_name就是虚拟环境的名字,我一般命名为pt,这样输入起来很快。后面的python=3.7是可选的,python版本可选,这句话加不加也可选。没加的话晚点安装python也行。

创建完环境之后,输入conda activate pt就可以进入新的虚拟环境。前面的(base)应该会变为(pt)

搞成上图那样就算配得差不多了。

CPU版本的Pytorch

Pytorch等深度学习框架分CPU版和GPU版。GPU版速度快,但装起来有点复杂。如果只想稍微学习一下编程框架,以后用GPU服务器跑正式代码,可以只装CPU版。

这个时候,为了避免GPU版教程的干扰,应该在搜索引擎中搜索Pytorch CPU 你的操作系统

一般输入conda install pytorch-cpu torchvision-cpu -c pytorch就可以安装好了。但这个时候,一般有一个坑会碰到:下载速度奇慢无比。

这个时候,应该去网上搜索conda 下载慢类似的关键字。网上一般会给出以下几个办法:换源、直接下载安装包、用pip下载。源就是下载的来源网站,在国内的话最好切换到国内的下载网站比较快。我选择换conda源的方法下载。明确了方法后,要继续换搜索的关键字,应搜索conda 换源。我搜索到的办法是:输入conda config --set show_channel_urls yes。之后在用户根目录(比如C:\Users\Yifan Zhou)下编辑.condarc,加入以下内容:

1
2
3
4
5
channels:
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
ssl_verify: true

我发现换了源还是下载得很慢,于是直接挂了个VPN再下载(哈哈哈)

比较好的方式开始用换了源的pip

下好之后稍微验证以下。比如安装之后,继续在Anaconda的虚拟环境中输入以下内容

1
2
3
python
import torch
print(torch.__verion__)

GPU版本的Pytorch

确认显卡驱动

在安装GPU版本Pytorch之前,还要保证电脑中有版本正确的cuda和cudnn。cuda是一套GPU上的编程库,cudnn是基于cuda的深度学习库。Pytorch依赖这两个库,而cudnn版本依赖cuda版本。而由于cuda貌似更新比Pytorch快,出于向下兼容的考虑,应该先在电脑支持的CUDA版本和Pytorch支持的CUDA版本中选择一个较小者,再下载该版本的CUDA和CUDNN。

但是,在安装这些东西之前,还可能碰到一个坑——显卡没装驱动。这里默认电脑使用的是N卡。在控制台中输入nvidia-smi,如果出现了类似下面这种信息:

就说明驱动已经安装好了。如果没有,请搜索显卡驱动,适当加入”Nvidia Cuda Cudnn pytorch tensorflow python”等关键词,多看几篇教程总能把驱动装好。

输出信息中的CUDA Version就是电脑显卡最高支持的CUDA版本。到网上下CUDA也应该不超过这个版本。

CUDA

记录下nvidia-smi命令输出的CUDA版本(比如我的是11.1),再在 https://pytorch.org/get-started/previous-versions/ 查Pytorch是否支持该CUDA版本。如果不支持,就选一个网站上有,且版本小于命令输出的CUDA版本的。之后去 https://developer.nvidia.com/cuda-toolkit-archive 找到合适的CUDA版本,选择版本后选择自己对应的操作系统。下载了可执行文件后直接运行安装。

CUDNN

打开 https://developer.nvidia.com/rdp/cudnn-archive 这个网站。注册账号并登录。根据CUDA版本,找到合适版本在合适操作系统下的CUDNN。(注意是下载cuDNN Library,比如[cuDNN Library for Windows10 (x86)]

正如前面所描述的,CUDNN是个库,不需要安装,只需要把其文件复制到CUDA文件夹下即可使用。要把CUDNN中的bin,include,lib都复制到CUDA的对应目录下。

注意,CUDA的bin文件夹应该被添加进环境变量。用默认设置安装CUDA时一般该文件夹会自动被加入环境变量。

Pytorch

https://pytorch.org/get-started/locally/ 中,选择合适的操作系统和CUDA版本。参考CPU版本的Pytorch这一小节的内容,用conda一键下好pytorch。如果网速慢,请利用搜索引擎解决问题。

一切都下载完后,在环境中输入以下内容,看到True就是胜利。

1
2
3
4
python
import torch
import torchvision
torch.cuda.is_available()

风雨夜行记 ~ Adventure in a Rainy Night

初到上海,老天爷就隆重地请来嘉宾”烟花“迎接我的到来。一场突如其来的台风席卷了城市,大雨倾盆而下,把人们关在了家中,连多家知名餐馆都停止了外卖服务。傍晚,我在生活用品尚不齐全的新房间里,摸着空空如也的肚子暗自叹息。怎么办?是吃点零食,全身脏兮兮地回床上避难?还是一往无前,出门购买食物和所需的生活用品?向来喜欢追求成果上限的我自然是选择后者。

出门之前,我先在脑子里进行了任务计划。我的主要目标是吃饭,次要目标是购买生活物品。既然出了门,就要吃得好一点,并且去大超市买东西。我的住处附近恰好有一个大商场,附近必然有不错的餐馆。我在地图上看好了路线,背上用于运货的书包,带上心爱的遮阳挡雨两用自动伞,就头也不回地出门了。

刚一出门,我就意识到了今晚行动的困难性。屋外风雨交加,骤雨把漆黑的天空和昏暗的街道融为一体,雨点落地声甚至盖过了地铁启动时的轰鸣。今天晚上,我的行动恐怕和在RPG里做主线任务一样充满挑战吧。

果不其然,我很快就碰到了第一道关卡。在我的住处和大马路之间,有一条必经的小道。小道没什么人维护,平日里看起来幽雅宁静,下雨时还因积水形成了一条小溪,真多亏了它糟糕的排水系统。我把第一个场景命名为”积水巷道“。在积水巷道上,电动车驾驶员都得小心翼翼地行驶着,生怕把水溅到身体上,我一个撑着伞的行人又该怎么通过呢?我倒是不慌,想着:凡是游戏里的关卡,必然设置了通关的方法。很幸运,我发现路对面有一条较窄的人行道,上面没有积水,可以安稳地通过这条小道。想来这条路的建设者并不是没有注意到路面会有积水的现象,还非常贴心地建了一条下雨专用人行道。看得出这个建设者以前应该是个程序员,碰到解决不了的BUG,就给用户另一个更复杂的操作步骤来绕过BUG。凭借着细心的观察和对程序员的理解,我顺利利用人行道离开了积水巷道关卡。

离开了小道,来到大马路上,我立刻见识到了台风的威力。本来几阵夹杂细雨的微风就让我撑伞撑得十分吃力了,忽然的一阵狂风让雨伞飞舞起来,整个伞面都翻转了过去。不仅是我,街上其他撑伞的行人都受到了不小的影响。只见几个行人万般无奈地收起了伞,在雨点的敲打下叹息着。我碰到了新的挑战:“狂风事件”。狂风事件会在行动时随机发生,令行人失去撑伞能力,被迫淋雨,降低行动能力与士气。如果不是有不得不去做的事情,一般人遇到狂风事件都会退缩,立马跑回家去了。然而,我却十分冷静地面对着突然刮起的大风,使用我独创的“相对论遮风挡雨法”消除了狂风事件对我的影响。

什么是“相对论遮风挡雨法”呢?这里就要介绍一下背景信息了。之前出去旅游的时候,一向不喜欢撑伞的我,被太阳晒得没办法,买了现在这把遮阳挡雨两用自动伞。我发现遮阳的时候如果和遮雨一样正着撑伞,有的时候遮阳效果并不好,因为太阳的光往往是斜着照过来的。然而,不管你人朝着哪个方向走路,都把伞正对着太阳,就能起到非常好的遮阳效果。因此,我独创了一种“相对论遮阳法”,根据太阳的方位,在走路的时候固定手和伞的朝向,而不管自己朝着哪个方向走。今天碰到了台风后,我瞬间领悟了“相对论遮风挡雨法”,以应对狂风事件。这种撑伞法相比之下更加复杂,需要分两个步骤进行。在第一步中,需要用手无力地把伞举起,通过伞的运动方向感受风的方向;在第二步中,用类似相对论遮阳法的手法,把撑伞朝向固定为风吹来的方向。凭借这种方法,无论风有多大,只要握紧伞把,伞就不会翻转,还能把雨滴全部挡下来。使用相对论遮风挡雨法后,我自由自在地在风雨中走着。

由于我肚子实在太饿,我决定改变计划,立刻吃饭。马路对面有几家店还开着,我满怀期待地过了马路,希望能找到一家合适的餐馆。结果这边还在开门的只有几家装修平庸、看上去就不靠谱的餐馆,剩下还在开门的店就只有日式便利店、药店,还有一家写着“内有被褥”的杂货便利店。这些店都不能解决我的饮食问题。我有些低落地向前走了几步,突然眼前一亮,看到一家装修精美的餐厅。原来是一家看起来质量还不错的咖啡馆。但是这种店又过于正式了一些,不适合一个人随便解决一餐。一个人吃顿饭还是有些难啊,这样的店就适合几个人一起去,尤其是情侣。情侣?哼!这种店就是为了提供这样的氛围才装修得这么好的,太不为单身的人考虑了。一想到这,我就气饱了,也不管肚子饿不饿了,义愤填膺地从店门口晃了过去。

又走了一段时间,我发现店面越来越少,路也越来越窄,最后竟被一棵吹倒的大树拦住了去路。这感觉不对啊,是不是走错了路?我破天荒地打开手机看了下地图,反复确认了我走的路是正确的。这棵树是怎么回事?我很快给了自己一个合理的答案:RPG游戏里经常会出现第一次能走通的路在游戏后期被拦住的情况,玩家需要进入一个新的场景,通过更难的关卡,来绕过障碍物。想到这里,我就摩拳擦掌,对即将面临的新事件期待起来:我是不是要自己找一条路出来?结果,我又看了一眼地图,发现地图导航提示我在此处应下台阶。我这才发现,这条路本来就是死路,我应该走地下通道绕过这里。我刚刚提起的兴致一下就被浇灭了。

终于,商场的大楼近在眼前了,我只需要过一条比较宽的马路就行了。可是,我又碰到了新挑战:马路上有不浅的积水,穿凉鞋倒是可以不管不顾地走过去,可我穿了运动鞋出来。看来我不得不面对这“无伤通过积水公路”的挑战了。细心的我发现斑马线是有一定厚度的,踩着斑马线走几乎不会沾到水。绿灯一亮,我就使出”轻功水上漂”绝技,大踏步地踩着斑马线走。最后一条斑马线离人行道还有一段距离,这之间还有不少积水。为了保证自己是“无伤通关”,我一扭头,令积水恰好能反射路灯的灯光。这样,我就掌握了所有的危险地带和安全地带的位置。踏过几个较干的路面,我成功抵达了马路对面。

地图上显示商场的关门是十点,而现在是八点多,时间还算充裕。我看商场对面开着的店铺很多,决定先找个地方吃饭。有好几家店看起来都不错,该去哪一家呢?正当我准备开始分析时,突然发现一个外卖员从一家面馆走了出来。能做外卖,这个点还有人点餐,说明这家店应该还不错。特别是我发现这家店里还有一个正在用餐的人,这让我对这家店的质量更加放心了。进店之后,我直接点了最贵的红烧牛肉面。面端上来用了七、八分钟,这对饥肠辘辘的我来说就像45分钟的语文课一样漫长。加几滴醋,用筷子微微搅拌,捞出一大把筋道的面条,配上大块牛肉,我享受这绝妙的用餐体验。一想到吃一辈子红烧牛肉方便面也吃不到这么多牛肉,我就吃得更香了。我吃得过于投入,以至于身上的雨水渐渐变成了汗水。吃完面条,我没有休息,立刻走出店门,吹了下风才发现身上出汗了。

马上,我的旅途就要抵达终点了。在明亮的商场里挑选商品,而不必四处躲雨,这就是我即将获得的通关奖励。买完东西之后,我还要打出租车回家,结束这冒险之旅。一边想着这些幸福的事情,我很快来走到了商场门口。商场里面倒确实是十分明亮,可大门却是锁着的。十点没到,商场怎么关门了呢?看来今天台风的影响确实很大,出于人道主义的考虑,商场的工作人员都早早下班避雨了。这么大的雨,也没人来买东西,商场也赚不到钱啊。想到商场关门的种种原因,我顿时觉得自己十分草率,竟然出门之前不去从商场的角度考虑,忽视了商场有提早关门的可能。但也不亏,我的主要任务——吃饭已经完成,买生活用品的支线任务就放一边吧。站在商场前的路灯下,我偶然抬头一望,针状的雨在光线下清晰可见,猛烈地扎在我伞上。看见无色的雨本身算是一件有趣的事,现在的我却只能感叹今天雨真大。

忽然,我发现自己似乎错过了什么东西。购买生活用品这一任务还不能宣告失败!我又再次充满了动力,沿回家的路走去。

回程时,我走在马路的另一侧。非常烦人,我又碰到了新的挑战:马路这侧的人行道中间的路砖因修地铁而全部挖掉了,人行道外的马路又全是积水,马路上时不时有汽车经过,走马路中间也不太好。我唯一的选择,就是走人行道外侧残留的石砖。石砖非常狭窄,仅能恰好放下我的鞋子。石砖的一边是积水的马路,一边是坑坑洼洼的泥土——还好不是掉下去就死的万丈深渊,起码给我留了一些容错率。于是,我开始挑战“平衡木”关卡了。虽然我天生缺乏运动细胞,但我使用了极佳的前进策略:我一只手使用相对论遮风挡雨法撑伞,为了平衡,另一手略微向外伸出。我出乎意料地顺利通过了平衡木挑战,虽然中间有几次失去平衡踩到了泥土上。

路过“倾倒之树“景点,我再次感叹起了今天行程的不易。仔细想来,今天好像遇到了不少有趣的挑战。于是,我一边走路,一边构思起了这篇文章。

走到了”积水巷道“的入口,我没有进去,而是又过了一次马路,走到了那一排仅有少数亮光的店面前。原来,在发现大商场关门后,我的潜意识一直没有停止思考,一直寻找着购买生活物品的方法。那时,我忽然想起这里有一家便利店写着”内有被褥“。为什么一家便利店要在牌子写这些东西?是真的提示顾客里面卖被子吗?不是这样的,真相只有一个!其实”内有被褥“的意思是,这家店不仅是一个卖饮料零食的普通便利店,还是一个卖生活用品的杂货店!!虽然我没有进店确认,但我利用心理学思考推理出了这一结论。有了这一发现,我才满怀希望地踏上了回程的路,觉得宣告任务失败还为时尚早。

那么结果如何呢?我走进杂货便利店一看,果然,在被子旁边,还有洗衣液、碗筷、垃圾桶这些生活用品,甚至还有电饭锅。我的推理完全正确!我最近一直在看柯南,感觉学到的推理能力一直没有用武之地。但非常巧,今天恰好有一个舞台,让我把推理能力淋漓尽致地展现了出来。不仅是看柯南学到的推理能力,我在之前参与的很多项目、经历中已经掌握了极强的逻辑思考和求解问题的推理能力,并且在玩LOL时锻炼了从对手的角度反向思考的”千层饼“心理分析能力。今天我把这些能力都用了出来,实在值得吹嘘一番。

考虑到台风可能一时半会儿不会走,我不仅把日常要用的清洁用品买了,还买了个电饭锅准备回去自己做东西吃,买了一大堆东西。最后,我一只手撑伞和拎锅,另一只手抱着刚买的装得满满当当的垃圾桶,吃力地走出了杂货店。虽然拎这么多东西很麻烦,但我看着这些”战利品“,感到十分充实与开心。

雨势渐小,积水巷道里的水却依然没有消退。和出来的时候不同,我熟练地走上了人行道,回忆起了今天的奇遇。今天我的行动称不上旅行,用”冒险“来形容恐怕更恰当一些。正如我以前写道的,旅行的意义在于旅行的体验。其实今天吃饭、买东西的任务都在其次,最令我难忘的是一路的见闻、经历。虽说老天不是很给面子,让台风来迎接我的到来,但也多亏了这风雨交加的夜晚,给了我这独一无二的体验。这换成其他任何人,或者其他任何时候,都很难有这样的情况出现吧。再往大了想,人生的快乐也不是那么奢侈的事情。都说”偷得浮生半日闲“,再平淡的生活,亦或是再艰苦的生活,也是能通过一些方法找到乐趣的吧。

满载着战利品,我回到了家中。今天采取了这么多应对下雨的策略,应该取得了不错的效果。结果我一坐下,屁股一凉,才发现我的短袖裤子和短袖T恤都湿了,鞋子也湿了个透心凉。不能说我的撑伞法、过街法不好,只能说雨下得太大,要挑战的难度太大。今天我的主要任务和次要任务都完成了,虽然没有做到无伤,但也是不小的成就了。今天过得非常好,希望以后我在自我挑战中能够再接再厉。

初夏,一片绿叶悄然飘落

一片绿叶

六月,北京的气温开始升高。风和日丽的春天突然离去,闷热的空气充斥了整栋宿舍楼。傍晚,热气依然没有离开宿舍的意思,我只好先它一步,跑到宿舍外散步乘凉。

散步时无事可做,我只好欣赏起校园里那些不起眼的花草树木起来。经历了漫漫长冬后,在第一阵春风的引导下,植物开始竞相生长。大树又一次长满了树叶,和草坪一起,构成了一幅放眼望去只有绿色的单调画面。好在有各色的花朵在路旁点缀,给枯燥的以理工科为主的大学勉强增添上了一分色彩。

经历了一个春天的滋补,初夏的植物都茂盛地生长着。我偶然低头看去,突然发现树下竟然有几片落叶。与秋天落木萧萧的美景不同,此时地上的落叶只有零星的几片,树上也看不到有任何树叶要落下的迹象。想必这几片落叶只是不成器的失败者,看它们枯黄的颜色就知道,和正在生长的绿叶相比,它们并没有得到足够的养分,最后体力不支被风吹到地上。

忽然,一阵风吹过,又一片树叶从树上落下。我的内心一阵欣喜,因为自己的猜想算是得到了证明:哪怕在夏天,依然会有些发育不良的树叶被吹落。但我定睛一看,却又是一阵震惊:树叶从大树深处的阴影中落下,却在灯光下显得绿得发亮;明明是从高处跌落,最终必将驻留在地上,却在风中迟迟不肯落地。

这片落叶是与众不同的吧。和地上那些已经一动不动,几近腐烂的落叶相比,它绿得那么鲜艳,在空中飞舞得那么轻盈。明明马上就会落地,失去活动的机会,却依然奋力地展示着自己的生命力。

和树上那些长得正旺的树叶相比,它也是不同的。它本来应该可以继续在树上生长,用最好的姿态去迎接盛夏的骄阳的吧。那它是不小心从树上跌落,还是自己想离开大树的呢?它是见惯了花开花落,厌恶了大多数植物同样的命运?还是只是想随风飘摇,看一看树外不同的景色?它最好的归宿是哪里呢?

还未等那片绿叶落地,又刮起了一阵更强的风。眨眼间,绿叶就从我的视野里消失了。我想,大自然已经给了我最好的答案:那片绿叶心向远方。我的视野之外,就是它最好的去处。

迷茫与挣扎

说来丢人,马上就要毕业了,我还没有一个明确的去向。

过去的一年半里,我都是在同样一种不安的心情下度过的。我没有踏上迈向未来的“正轨”,我担心自己的未来会陷入迷茫、找不到方向的境地。事实上,到了即将毕业的今天,我的一切担忧变成了现实。

在这种担忧之下,我很难完全集中于眼前的事情,无论它是否会对我的未来产生功利的影响。我在各种事情上犯了很多错误,或者说有很多做得不好的地方。这些错误或失误长久地驻留在我的脑中,自责不断地刺痛着我,让我在做类似事情回忆起这些错误时都倍加折磨。在正反馈调节下,自责引发进一步的错误,事情向越来越糟糕的方向发展。

我很早就意识到了我的所有问题的原因,并尝试去解决。我通过一系列思考分析清了自己的现状,一直保持着十分自信的心态。但是,我只是发现了问题并分析了问题,却没有解决问题,并付出行动。

我一直在迷茫中挣扎,明白了自责不是解决错误的方法。只有从错误中学习,并提取经验为未来服务,才是利用错误的最好方法。在找寻未来方向的这段经历中,我学到了什么呢?经过一段时间的发现和思考,我得到了这样的结论:人生本身就是混沌的,需要自己去寻找方向。我能找到大致的方向,却缺乏主动为方向铺路并沿着道路前进的能力。

温室里的流水线

两年半前,我参加了某个留学中介的讲座。主讲人提到了一个案例:有一个计算机专业的学生,大学前三年都对专业知识一窍不通,但是在大三暑假依然找到了一个和艺术、信息技术相关的暑研,最后大四申请到了该方向上的硕士。主讲人想通过这个例子说明,留学中介可以极大地帮助想留学的同学,哪怕专业能力再差,也能申请到学校。主讲人把这个故事当成了宣传中介的绝佳示例,却让我彻底断绝了向留学中介寻求哪怕一丁点帮助的想法:一个这样的人都能成功留学,那这种形式的留学的意义在哪,又有何含金量可言呢?这样是“没有意义”的。我宁可自己申请不到学校,也不想通过留学中介“成功”申请到某所学校。

我一直坚持着自己的观念,由于种种原因我也确实没有申请到学校。一开始,我还没有相同自己潜意识里想表达的“没有意义”是什么意思。现在,我总算想通了自己申请留学的意义所在:出国留学,是对自己未来的一次主动选择。这其中有许多信息、方向上的困难,而不是思考、智力上的困难。这个过程考验人对于方向的掌握,并为某个方向努力的能力。留学中介帮你完成了许多中间步骤,让你失去了这次锻炼自己掌控自己未来的机会。将来,或许是留学毕业后,肯定还会再次碰到未来方向的选择问题。侥幸绕开了一次考验,只会把问题全部积攒起来滞留到下一次考验上。因此,当时的我就在潜意识中认识到了:选择未来,掌控未来是一种重要的能力,比能否在毕业那年成功留学重要得多。我要利用这次机会主动选择未来的方向并朝着方向迈进。

我在一所还不错的大学里上学。仔细回想,我和周围的人一样,从小到大不过是在温室里的流水线上,在他人的呵护下推动着移动罢了。绝大多数出生普通的孩子都被灌输“万般皆下品,唯有读书高”的信念,巴不得鲤鱼跃龙门,一朝金榜题名改变命运。我和周围这些幸运的同学,在家长和老师的照料下,十二年考场厮杀搏斗,千军万马中闯到名牌大学。我们人生的轨迹是早早地就决定好了的,人生的价值甚至是可以被量化的:用那光彩照人,却又污秽不堪的考试成绩来表示。考试越来越难,周围的人越来越强,流水线上的人越来越少,我们嬉笑地看着跌落出流水线的人,暗自骄傲。

到了大学,父母的呵护少了些,世界那狰狞的本性逐渐向我们展露出来。我们慢慢离开了温室,却依然没有离开流水线,而是逐渐被机器划分到了不同的支流上。有的支流叫做“保研”,有的叫“出国”,还有叫“工作”、“考研”和“创业”什么的。有人会跟你说,每条支流上的风景都很好,有哪些优秀的零件获得了许多奖项,保持着顶尖的成绩,收获了“精彩”的人生。大家趋之若鹜,纷纷在流水线上赛跑着,继续争夺着那几条看起来更体面的支流。

但是,人生绝对不是只有铺好的道路,或是说那些自动运行的流水线的。人生的大部分地方是混沌、黑暗、未经探索而充满危险的。在混沌中前行,会迷茫、犹豫;会陷入深坑,难以挣脱;会因害怕和痛苦而回头逃避;会因找不到同行的人而寂寞惆怅;会对着无人的夜空哀嚎。但是,正是在无人探索的深处,有光明,有宝藏,有能让你忘却路上所有的苦难,并且骄傲地向全世界炫耀的事物。危险而困难,却充满了令人向往的事物,这才是人生的全貌。流水线,只是人生的一隅,一个被前人故意建好的设施而已。

每个人在人生中的某个阶段,总会走下这流水线,被迫去用自己的双脚探索这个世界。亦或是流水线的终点已到,仅需在附近寻找光照,就可以停滞不前却又安稳地生活着;亦或是从某处落下,无法立刻安定,在担惊受怕地瞎逛一阵后,建立起一个勉强供自己生活的根据地;亦或是凭自己的意志离开,不断前行,根据自己脑中的印象,去寻找世界上最危险、最美丽的宝藏。

正是意识到了这些,我对流水线积怨已久。我想凭自己的双脚走路,却又对远处的黑暗感到恐惧,只好暂且跟随有光照的流水线行走。我畅想有一天能逐渐远离流水线,去往更有趣的地方。但十分不巧,一座高山出现了。我的面前是一座垂直的悬崖,高不见顶,给人高山仰止之感。我屡次伸手想借突出的石块攀岩上去,却又屡屡跌落。摔伤的次数越多,我越恐惧,离下次尝试的时间就越久。我看到附近有不少人登上了山:有人乘着流水线,缓缓上了山;有人沿着铺好的、略有陡峭的台阶,很快跑上了山;哪怕是荒凉的周围,也有人攀登上了较矮的山峰,踏上了登山之旅。我开始责问自己为什么没有长得更高一点,手脚为什么没有更有力气一点,上次攀登的时候为什么没有再小心一点。最终,我开始驻足不前。

在温室的流水线里呆久了,我不过是一个大号的婴儿而已。凭借叛逆的本能,逃离了流水线,却发现自己根本没有掌握足够的运动能力。不肯低下头,看着路前行;倒下后,不肯治疗伤口,躺着逃避下一次挑战;不肯向周围张望,挪动双脚,换一条上山的道路。周围的一切都看得清清楚楚,却就是因为种种原因毫无行动。

现在,我不仅看清了周围,还看清了自己。既然向往远处的宝藏,那么再高的山、再陡的崖也无法阻止的我前行。体能不行,就去锻炼身体;一个石块被我抓断了,就去抓旁边那块;跌倒了,就尽快投入下一次尝试。凭借着正确的战略,征服眼前的山只是时间问题。

后记

在外面转了一圈后,我不仅身上凉快了,脑子也清醒了,回宿舍后立刻把所见所想记录了下来。

我又想起那片树叶了。它的颜色那么鲜艳,不是春风赏赐给它的,而是它从树的枝干里汲取营养,自己长出来的。既然它心向远方,哪怕暂时落到地上无法移动,风儿或许也会因欣赏它的美丽而响应它的呼唤,载着它继续飞翔。

人在前进时需要动力,谨以此文自勉。

真·后记

写完这篇文章后,我觉得内容有些羞耻,就立刻把文档关掉,再也没打开过。一个多月后重读这篇文章,还是觉得内容有点不太适合发表出来。不过仔细分析了一下后,我觉得这篇文章里并没有过多地反映我个人的信息,而是很好地刻画了一个人的心理活动,并且表达出了一些思想,对于他人来说还是有一些阅读价值的。照相记录画面,录音记录声音,那么文字则是能把思想、情感保存下来,供愿意欣赏的人品味。我最终还是准备把这篇文章发表出来。

如今我已经有了毕业的去向。我不会嘲笑自己之前的焦虑,毕竟这些都是宝贵的人生体验;我也不会因有了去处就得意忘形,因为这本来就是我应得的。这件事其实不是本文的重点。最重要的是文章中提及的对人生的态度,即如何决定自己未来的发展。不论何时,我都会坚持这样的态度。

“悬梁刺股”不如“乐此不疲”

正文

大学四年中,我有了许多新的感悟。其中,我认为最重要的一项收获是:与其“悬梁刺股”般督促自己艰苦地奋斗,不如在任务中寻找令自己快乐的成分,从而乐此不疲地达成目标。我在大学中印象最深刻的两段经历印证了这一道理。

一、因为热爱,所以成功

第一段经历是我在大一暑假参加的算法竞赛集训。当时,我有幸受到了学长学姐的邀请,提前加入了集训队。我既要和其他大一同学一样,学习新的竞赛知识,又要和其他高年级集训队的同学一起参加模拟赛。作为大一的学生,我要每两天上一次课,每次完成数十道习题;作为集训队的一员,我每周要参加四场训练赛,每场比赛持续五小时,赛后还需要回顾比赛中没写出来的题目。在旁人眼中,我每天都要完成大量的题目,日子过得十分辛苦。然而,我自己知道,我从小就不是一个刻苦的人。我之所以能每天坚持写完那么多题目,是因为我乐在其中。当我思路深陷泥潭,绞尽脑汁也想不出解题之道时,屏幕上一行行的“答案错误”就仿佛是对我的付出所发出的一声声嘲笑;而当我柳暗花明,总算把题目解出来时,屏幕上那一行“答案正确”就是对我最好的盛赞。这句对我的赞美,胜过了之前所受的所有嘲笑,让我收获到了极大的成就感。我享受学习新算法时的恍然大悟,享受想题时的冥思苦想,享受解题失败后的心烦意乱,享受解题时的灵光一闪,享受题目最终通过时的开怀大笑。我热爱算法竞赛的一切。

正是因为我对算法竞赛的这份热爱,让我不知不觉中完成了大量的习题,竞赛水平突飞猛进。在暑假集训前,由于我在高中没有竞赛基础,我的水平在同年级中算不上拔尖。而集训结束后,我的水平得到了质的飞跃,在大大小小的比赛中都取得了不错的成绩。暑假结束后,在程序实践课的期末考试中,所有同学要求组队在两个小时里完成五道题目。我单人参赛,50分钟不到就以全年级最快速度写完了所有题目。之后,我首次参加正式的算法比赛,我们团队获得了银牌。一个月后,我参加国际大学生程序设计竞赛亚洲区域赛。凭借着我通过的一道中等难度的题目,我们队伍拿下了北理2018年第一块算法竞赛金牌。

我在算法竞赛中获得的金牌,为我带来了许多利益上的收获。但是,在参与算法竞赛中最令我难忘的经历,不是登上金牌领奖台的那一刻,而是我大一暑假集训时,那乐在其中的每一天。我的热爱为我带来了成功,但我不是为了获得成就而去热爱,而只是单纯觉得参加竞赛很有趣而已。在参加算法竞赛的过程中,我不仅每天都过得十分开心,更是学会了一个道理:热爱自己的事业,能够取得成功。

二、抛开压力,寻找目标

第二段经历是我在大三暑假开始参与的研究项目。我和另一位同学一起,在本校明振军老师和与本校有合作关系的Farrokh,Janet教授夫妇的指导下进行科研。两位外国教授不仅学术成果斐然,更是善于教学,他们的许多学生已经成为了教授。现在,这两位教授年事已高,却依旧思路清晰、逻辑缜密,还心胸宽阔,毫无保留地指导着各个国家的学生。他们不仅在科研上指导我们,还常常分享生活和学习的心得。有一次,我们向他们请教,在和同龄人的竞争中感到压力太大怎么办。Farrokh教授回答道:“时常保持快乐。成功的人不一定快乐,但快乐的人总能成功。我喜欢我的事业,我做着我喜欢的事情,别人的成就又与我何干呢?我每天勤恳地工作,是因为我热爱我做的事情啊!”

教授的一番话点醒了我,让我开始重新审视自己的行为。在遇到这两位教授之前,我就已经参与了一些科研项目。但是,我感觉自己付出了很多时间与心血,却一直没有什么收获。回过头来一想,我之前参与科研,只不过是为了发论文,提升自己的背景,获取利益而已。我根本没想清楚为什么要去参与科研,只会关注有没有产出,所以参与科研时感到十分辛苦,却又毫无收获。我根本不喜欢我正在做的事情。

我参与的暑研课题离我的专业比较远,导致我对这个课题并不是很感兴趣。但是,在这次的科研项目中,我开始转换思维,试图去发掘这个科研项目中令我感兴趣、能让我有动力去奋斗的目标。结果,还是两位教授的教诲帮助了我。他们告诉我,科研不一定是要做出什么惊天动地的成就。发掘一些新的知识,能给其他研究者带来启发,这就够了。因此,我确立了我在这次科研中要达到的目标:做为本科生,想做出太大的创新十分困难。不如提出一些简单的创新点,并尽可能给别人的研究提供一些灵感。确立了能让我获得成就感的目标后,我在毫无压力的心态下投入到了这个科研项目中,最终完成了一篇论文,该论文目前正在投稿中。在这段经历中,我并没有像算法竞赛一样那么热爱我正在从事的工作,但我依旧去挖掘任务中能让我感到兴奋、愿意为之奋斗的因素。最终的结果证明,在压力下为了功利匍匐而行,不如看向远方,慷慨高歌着踏步而行。

三、不再“努力”,快乐人生

我的算法竞赛经历,让我明白从事自己所热爱的工作,能够既享受过程,又收获成功。我的暑研经历,让我明白哪怕是面对自己不是很喜欢的事情,艰苦地奋斗依旧比不过为了任务中某个热爱的成分而奋斗。但是,我的收获和大部分人的认知有所出入。从小到大,我们都被灌输了要努力学习的概念,也听过种种因努力而成果的例子。诚然,是有很多人凭借着过人的意志,在艰苦的奋斗后取得了成功,比如“头悬梁,锥刺股”的故事。但是,又有多少人倒在了正在努力的路上呢?我们不得而知。从另一个角度来讲,什么是成功呢?在人生的一站中凭借辛苦的努力取得成功,是否又能在下一站成功呢?什么时候能停下来,欣赏人生的大好风景呢?努力,本来是表示尽力去完成一件事,引申为一种积极的工作态度。然而,在诸多艰苦奋斗事迹的渲染下,努力成了抑制自己的欲望、让自己在一种十分不情愿的态度下,依然拼尽全力去做事的代名词。这种对于“努力”的阐释,完全是自相矛盾的:自己都感到辛苦,说明自己的内心根本不认同自己正在做的事情,那哪里来的动力去把事情做好,取得更高的成就呢?因此,在目前从事的事情中感到辛苦的人,要么是因为无法失败而勉强自己

,把任务以最低的标准达成,要么是为了诸如功名的其他目标而强迫自己奋斗,在达到短期目标后就会立刻抛弃当前这项令自己辛苦的工作。正是因为这种对努力的错误阐释,导致有些人盲目努力,倒在了半路上;有些人通过“努力”获得了一时的成功,却在下一件事情中顺从自己远离压力的本能,放弃了努力;有些人持续地“努力”,一直小有成就,却发现自己的努力是没有尽头的;有些人深谙事故,“努力”一时得到了自己想要的事情后,开始鼓吹“努力”,通过他人的努力获利。不管每个人经历如何、结果如何,这些所谓压制自己的欲望进行艰苦奋斗的人,在“努力”时是不幸福的。

“努力”的反面,是快乐地从事一件事。保持快乐的心态能否取得世俗上的成功,这难以下一个定论。但是,保持这种心态的人,有着其他层面上的优势。这种优势体现在整个人生的意义上。人生的意义是什么呢?简单来讲,就是要让自己过得开心。与“努力”相比,热爱自己所从事的事情,能让人持久地获得动力,同时获得享受。更进一步地,不管当前从事的工作是否热爱,都应该从中寻找一些值得奋斗的目标。人生中,除了成绩、背景、金钱、地位、粉丝、异性缘这些世俗、公认的目标,还有着更多精彩的事物。视频游戏胜利时的一幅画面、沙滩上的一块贝壳、茂密草丛中的一束红花、父母的一声叮咛、久别重逢时的一阵寒暄、对视时躲闪的一缕目光、半夜的一段深思、聚光灯下的一场演讲、求知者抛来的一句提问、万千人幸福的笑脸。这些事物,远比世俗的目标重要得多,更能让人士气高昂地前行。“乐此不疲”的人,不需要在人生的路上休息,因为他们一直是快乐的,更是有意义、无悔的;甚至因为他们对于事情有着更多的投入,反而更容易收获其他人眼中难以企及的成功。

“悬梁刺股”不如“乐此不疲”,这是我从大学中印象最深刻的两段经历中得到的收获。未来,我依然会保持这份信念,在自己热爱的领域中取得令自己满意的成就。“悬梁刺股”不如“乐此不疲”

吐槽与评价

其实,现在我有很多时间了,应该静下心来反思一下,并发一些博文了。我有好多东西想总结,并且表达、分享出来的。可惜自己太懒了。

这篇文章是为了学校最终的德育答辩而写的。正常来说,我随便写一下糊弄过去就完事了。但是,这篇文章可能可以发表到学校的”前辈经验分享“书刊上,供新生阅读。我十分想向别人分享一些想法,尤其是对于比我年轻、可能会重新走我的一些弯路的人。考虑到我的文章可能会帮助别人,甚至改变他人的人生,我决定认真写一篇文章。

写文章之前,我的潜意识里就已经制定了一些写文章的注意事项(我的大脑的思考能力实在太强,以至于我不需要刻意地思考,就已经制定了完成事情的策略)。一些策略是为了能让文章被顺利录用,一些策略是为了更好地帮助后辈们。

  • 立意不能过于反常理。显然,抨击学校的不合理制度、告诉大家在大学不应该做些什么的是不允许发表的。但是,如果写一些常见甚至错误的立意,就对他人没什么帮助,我写起来也没劲。因此,我选择了我演讲时最常用的,有点反常理却能让人接受的一个立意:努力学习不如快乐学习。
  • 让审稿的人知道你很厉害。我必须吹一下我取得的成就,告诉审稿人我很强,这篇文章很有参考性。这是我擅长的领域,我在文章中也花五成功力吹嘘了自己一番。
  • 告诉后辈们一些重要的信息。讲大道理谁都会讲,但我特意在文章中写了一些十分具体的东西。聪明的新生会利用文章中提到的信息来提升自己。

同时,文章的字数要求在3000字左右。为了不让自己受到字数限制的影响,我特意没有用word和其他软件,用了看不见字数的记事本来写这篇文章。可谓是殚精竭虑,面面俱到。

制定完了策略后,我就开始写文章了。一开始,我写得非常不顺利,一方面是因为这是篇正式文章,虽然没有题材要求,但还是不能写起来太奔放的;另一方面,文章要花一些笔墨来写事,我只擅长写理和对事情客观而准确的描述,单纯写事对我来说有点不熟悉。结果文章写完,导入word一看,只有2200多字。没办法,我要说的话就这么多,该怎么办呢?

没办法,我开始顺从本心,在文章的总结阶段加入自己擅长的分析说理的文字,瞬间提升了文章的深度,也让文章从各种意义上看起来不够自然了。写完之后,导入word一看,3000字,刚好。我把想说的话说完了,发到博客上了。最终能不能被学校录用我已经不在意了,我已经把我在这几件事上想说的东西都说出来了,我的目的已经达到了。这篇文章中,我已经对我的这种态度进行了一定的说明。我讨厌无意义的结果,讨厌为了不纯粹的目的花费时间。我的目标是分享有用的经验,我认为这篇文章对后辈有着非常大的帮助。如果因为文章的某些观点不太能够被理解,导致学校不发出来,那我也不愿意去为了迎合审稿人而修改内容。

讲完了我写文章的心路历程,现在来评价一下这篇文章。评价之前吐槽一句,我发现自己的文学素养实在太差了。我有着灵活的思维,天马行空的创意。这不仅仅体现在其他方面,还体现在文学创作上。可惜,我没有足够的文学积累,无法做到用巧妙的方式来表达我的意思。但我清楚地知道,只要我有一定的文学积累,我一定写出具有文学性的文章,即用词优美华丽,却不失内涵,有深意的文字。有时间我会去尝试养成阅读文学书籍的习惯。

文章中前几段描写的是我的两段经历。其中第一段经历确实是我印象最深刻的经历,我也时常与别人分享,两次演讲中都分享了这个经历。第二段经历有点生搬硬凑的感觉,与实际情况有些出入,但为了写文章举例子不得不歪曲了一下真实的情况。这几段没什么出彩的地方,不过没有这些经历的人看完这些经历,应该能体会到一些东西。

后面三段总结段本来是很短的,最后我在放开手脚后往里面加入了很多内容。倒数第三段批判努力的内容我本来想单独用一篇文章来写,从本质上分析原因。可惜我实在太懒了。这篇文章浅谈了努力的部分内容,有一点点深度。当然,写这一段时我非常爽,因为我讽刺了非常多的东西。如果稍微对生活有一些观察,就能看懂这一段具体在讲生活中的哪些现象。

倒数第二段虽然比前一段短,却包含了更多的深意。这里的内容其实已经和本文没关了,应该在另一个主题里提及。但为了让自己写得爽一些,我直接就开始把想写的东西全写了。这一段的内容不知道读起来是不是很奇怪,但内容绝对不是乱写的,而是有逻辑,有一些文字背后的深意的。

总结来看,这篇文章是一篇要提交上去的文章,虽然我认为质量已经足够,但缺点还是很多。文笔不够好,这是自然的。文章后面有点违和,这是由于我为了凑字数,转换了写作思路导致的。评价起任何一种艺术作品来何其容易,但真做起来还是很难的。比如这篇文章,我知道有哪些地方不够好,但就是不好改,甚至我没有能力去改。不管怎么说,我认为这篇文章还算是合格,可以提交上去,也可以发到博客上分享一下。

“Poisson Image Editing”论文方法实现

上上篇博文中(现在是上上上了),我介绍了一下这篇图像融合的经典论文。今天,我将记录一下这篇论文的C++代码实现。我已经三个多月没碰C++了,手很生。但是,我会凭借着我高超的编程底力和过人的天赋,三小时内完成代码实现。稍微对工作量有一点了解的人都会产生疑问:“你在开玩笑吧?论文实现不比写算法题,你要先去看懂论文,再调一调别人的库函数,写代码之前有很多准备工作要做。你这不可能完成。”肯定有人这样想。但是,我可以说,包括学习前置知识在内,我可以三小时完成实现。这是一个七年编程王者应该有的自信。这篇博文的正文和会按之前的格式对项目的实现做一个比较全面的记录,最后一个章节会写下我完成此项目的实况。

代码仓库:https://github.com/SingleZombie/Gradient-Domain-Image-Processing-Cpp

由于种种原因,在记录了编程的实况后,我就把这篇博文搁置下来了。但马上要写毕业论文了,这篇博文和论文的内容有很多重合之处,我打算先把博文写完。

知识准备

在代码实现前,我们先整理一下实现方法的整体思路,再提取出实现中涉及的知识点,对每个知识点有关的实现方法和具体的实现技术进行介绍,最后对方法的核心——结果图像求解进行一个详细的介绍。

方法思想

整个方法的总流程图如上。和普通的图像复制类似,方法需要输入一幅源图像、一幅目标图像及复制区域(步骤1),输出一幅融合好的图像(步骤4)。为了得到最终的图像,需要先计算出源图像的梯度(步骤2),再根据目标图像在复制区域边缘的像素值和源图像复制区域的梯度值对结果图像求解(步骤3)。实际上,其他步骤都十分简单,方法的关键就在于第三步结果图像求解。

方法中设计的知识点有:

  1. 图像输入/输出:这项操作的内容很显然:把操作系统中的图像文件读入到程序的一个数组中,再把一个数组输出成一个图像文件。这个可以通过OpenCV库来轻松实现。图像输入输出是OpenCV最基本的操作,网上随便搜一搜OpenCV的教程就能找到。我自己也有一篇博文讲了这项知识。
  2. 图像梯度计算:所谓图像的梯度,就是每个像素与它左边和上边像素的颜色值之差,某处的梯度值表示此处颜色变化速度的快慢。由于后续操作需要源图像的梯度,在实际进行结果图像求解前需要先预处理出图像梯度。实现的时候用一个滤波器对图像做一次滤波即可,OpenCV包括了滤波功能。我的这篇博客简单介绍了OpenCV滤波函数fillter2D的用法。
  3. 结果图像求解:图像融合方法的思想是在融合区域边缘的颜色和目标图像一样的前提下,让源图像的梯度尽可能不变。这是一个最优化问题,最后问题可以转换成求解线性方程组。因此,这一步的目的就是通过求解线性方程组来得到我们想要的结果图像。这是本程序最关键、困难的一步,C++的Eigen库提供了高性能的矩阵运算函数。
  4. 图像拼接:最后输出前,要把处理过的图像区域和整幅目标图像拼起来。OpenCV提供了方便的图像区域覆盖函数。

本程序设计的图像处理操作都十分基础,用一些简单的OpenCV库函数即可。如前面多次强调的,该程序的关键是第三步。下一节将对第三步进行详细介绍。

结果图像的问题建立与求解

(导向插值标记,来自论文[1])

一般来说,用公式描述一件事是方便描述者,而折磨倾听者的。但是,为了把问题准确无误的描述出来,有时不得不使用严谨的公式标记。

在此问题中,我们把图像看成一个函数。如图所示,$S$是一个二维点的集合,即一堆可以用$(x,y)$这样的坐标描述的点。集合$S$就是图像函数的定义域。图像的值域呢?自然是图像的像素值了,这取决于实际情况,比如RGB像素值的范围是$[0, 2^{24})$。

现在,理解了图像其实可以表达为定义域是二维点,值域是颜色空间的值域的一个函数后,就可以继续理解符号标记了。令$g$是源图像,$f^*$是目标图像,$f$是我们把源图像复制到目标区域后,经过图像融合,得到的结果图像。按上一节的话说,$g$和$f^*$是输入,$f$是输出的结果图像。我们还有一个输入,就是进行图像融合的区域$\Omega$。既然我们把图像看成了函数,那么区域$\Omega$就是整个定义域$S$的一个子集。

$v$是$g$的梯度,$\partial\Omega$是区域$\Omega$的梯度,这两个量都是计算得到的。

我们还要引入一个符号——梯度算子$\triangledown$。$v$是$g$的梯度,可以写成$v=\triangledown g$。

重申一下结果图像求解的思想:在融合区域边缘的颜色和目标图像一样的前提下,让源图像的梯度尽可能不变(最小化差值)。用数学语言就是:

如果理解了求解的思想,那么看懂,或者说推导出这个公式是十分简单的。

这个公式是理想情况:图像在一个连续的二维数集上定义。但实际上,我们图像是离散是,只有在整数的位置处有值。我们要把问题用离散的形式表达出来。这里又要提出一些标记,对于$S$中的每个像素$p$,令 $N_p$ 为其4邻域中仍在 $S$ 的像素集合。也就是说,这个符号表示的某个像素上下左右这四个像素的集合,在这些像素没有跑出整幅图像的前提下。令 $(p,q)$ 为一个像素对,其中 $q \in N_p$ 。

再令 $v_{pq}$ 表示 $pq$ 两点间的像素值之差,即p到q方向的梯度值。则上面那个离散化的式子可以被转换成:

能看懂之前的式子,看懂新的式子也不难。

经过一系列数学推导,上面这个离散域的最优化问题可以被转换成一个线性方程组:

这个方程组就比较难懂了。不过,可以用一个直观的方法描述出来:待求解的结果图像的每个像素与周围四个像素的像素值之差(梯度)的求和等于源图像与周围四个点的像素值之差(梯度)的求和。公式中结果图像的每个像素与周围的梯度的求和分了两部分,是因为如果这个像素来自图像边界,则这个值是指定的(来自约束条件,边界像素值等于目标图像的像素值),在等式右边;如果像素不来自边界,则这个值是未知的,在等式左边。

有了联立的线性方程组,问题就变成了纯粹的数学问题,有很多的工具和方法来求解。

程序实现

程序结构图

在理想情况下,程序的结构图如下:(目前的代码很乱,没有严格按照这个图来)

整体分成输入、输出、和图像处理模块,非常简明。

如方法思想中所介绍的,图像处理可以分成图像梯度计算和目标图像求解。而求解又可以分成方程组建立和方程组求解。

这里仅介绍图像处理模块的实现细节。该模块整体的伪代码如下:

这份伪代码只能算是把整个流程用英文表达出来了而已,前文的梯度计算、方程组建立、方程组求解等内容均有体现。接下来几节介绍这三个重要步骤的细节。其他诸如获取图像的一个区域、填充图形一个区域的操作十分简单,可以用OpenCV轻松实现。OpenCV用Rect表示一个矩形区域,假设其有实例rect。令mat为任意一个图像,则mat[rect]就是图像的一个区域,可以执行读写操作。

图像梯度计算

梯度计算是学数字图像处理时的一个基本作业,由于有库函数的帮助,实现起来很方便,其伪代码如下:

拉普拉斯滤波器就是

1
2
3
[[0, -1, 0],
[-1, 4, -1],
[0, -1, 0]]

这样一个3X3的滤波器。手动建立一个滤波器后,调用OpenCV的filter2D就可以完成滤波了。

方程组建立

问题的方程组就是“结果图像的问题建立与求解”一节中最后一个公式所表示的方程组。只有彻底理解了那个公式中每一项的由来,才能把方程组建好。方程组求解时用到了矩阵,这其实就是把方程组的所有有关参数塞入了一个二维的存储结构中,没有太深奥的东西。

方程组主要有3项:系数矩阵项,边缘像素值项和梯度项。系数矩阵是方程组的左端项,后两者之和就是方程组的右端项。这一步的伪代码如下:

系数矩阵项lhs只由融合区域的大小决定。设矩阵的第i行表示第i个像素有关的方程,则第i列就是表示i像素自己的系数,由公式可知系数值是$|N_p|$;而其他至多4个在这个像素上下左右,且在融合区域内部的像素的系数是$-1$。

边缘像素值来自原问题的限制条件:融合区域的边缘像素值等于目标图像在此处的像素值。因此,这一部分的值要根据目标图像及融合区域得到。由于边缘项的出现没有什么规律,实际实现时可以在计算系数矩阵的同时计算边缘项:对每个像素四周判断,如果这一项在边缘上,则更新返回的边缘项;否则更新系数矩阵。

梯度项则完全来自源图像区域的梯度,甚至每个元素的位置都没有变,只要把它的形状改成列数为1的矩阵。

方程组求解

有了所有参数,调库求解方程组就是几行的事情了。下面是Eigen求解Ax=B方程组的代码:

1
2
3
4
5
6
7
Eigen::SparseLU<Eigen::SparseMatrix<float>> solver;
solver.analyzePattern(A);
solver.factorize(A);
Eigen::VectorXf tmpRes = solver.solve(b);Eigen::SparseLU<Eigen::SparseMatrix<float>> solver;
solver.analyzePattern(A);
solver.factorize(A);
Eigen::VectorXf tmpRes = solver.solve(b);

我只有在这一部分贴了代码,因为其他步骤都是比较灵活的,每个人都有自己的实现方式。只有这一步调库的写法是固定的。

求解完方程组,程序其实就基本写完了。

UPD:SparseLU并不是求解此问题的最佳方法。SimplicialLDLT适用于正定对称矩阵,在此问题中有更高的性能(我并不知道其中的原因,但是官网上明确写出这个方法适用于2D泊松问题,我就把结论拿来用了,哈哈)。只要把上述代码的SparseLU替换掉即可。

1
2
3
4
5
6
7
Eigen::SimplicialLDLT<Eigen::SparseMatrix<float>> solver;
solver.analyzePattern(A);
solver.factorize(A);
Eigen::VectorXf tmpRes = solver.solve(b);Eigen::SparseLU<Eigen::SparseMatrix<float>> solver;
solver.analyzePattern(A);
solver.factorize(A);
Eigen::VectorXf tmpRes = solver.solve(b);

结果展示

如图,(a)是源图像,(b)是目标图像,(c)是直接复制的结果,(d)是图像融合的结果。从视觉上来看,图像融合的效果还是不错的。如果能够用套索工具代替方框工具选择区域。用套索工具的话,难点在于实现套索工具本身,这已经脱离了本程序的主要任务了。有了套索的选定区域后,就是获取边缘项烦了一点,大部分的步骤还是一样的。

UPD:

程序里发现了一个BUG!!!方程右端的边缘项应该是相加,我写成了直接赋值。上面的结果图片是错的,正确的图片如下:

后续工作

在下面的”实况记录“一节中,你会发现,我5个小时不到就完成了没有bug的程序,基本完成了整个毕业设计。得知了这一事实,你能得到哪些结论?

首先,你会说:”哇,你好强啊,这么快就把一个看上去那么复杂的项目写完了。“确实,这说得很正确。但这对于我来说是基本操作,甚至我还对自己有些不够满意,写代码的时候急躁了一点,不然可以更快写完。比起学东西,写论文和博客等文档的时间,实际编程的时间确实占比很小。

层次高一点的人,会评论道:”你这项目太水了吧!你干了什么?不久求个线性方程组?高中生都会解这种问题,叫一个大一新生学一学也能写出求解线性方程组的程序。“这说得不错。确实这个项目实现起来很简单。但是,你要注意到,这是一个科研性质的毕业设计。什么是科研?写代码是科研吗?不是。科研是要针对一个问题,提出某些解法,或者说改进现有的解法。重点是思路。我刚刚也说了,我的大多数时间其实花在了看文章、学习上面。这十分合理。

对科研有一点经验的人,又能找到我的漏洞了:”你刚刚说科研的定义,说得很好。但你做了什么呢?你复现了别人的方法。这是科研吗?这有任何创新吗?有任何改进吗?“能说出这种话的人,一定是高手。确实,本文的内容是不太够的。我的创新点在其他地方。这篇文章,只是我毕业设计的核心部分,还有一些内容没有展示出来。敬请期待以后的博客。

真正善于见微知著的人,还能得到其他的结论:”这就是本科教育吗?本科论文就这么水吗?“我只能说:“是的。”本科教育基本没有给学生们提供接触科研的机会,而学校为了方便大家毕业,不会在毕业设计上为难大家。没有了GPA,奖学金这些功利的东西,大家也没有认真做这个项目的理由了。本科毕业设计,就是一个从学校、导师到学生,基本没有人会认真对待的东西,从动机的角度来说,十分合理。

要是你再想得透彻一点,会发现这一切都是不是事。还是我最喜欢强调的,重点是你学到什么,你能得到什么。对于我来说,本科毕设强化了我的科研能力,让我找回了一点写代码的熟练度,锻炼了逻辑表达能力。虽然我花费的很多心思没有任何功利的作用,没有人会注意到我做的那些东西,但我知道我能得到什么就够了,不需要得到别人的认可。本科教育也是这样。本科究竟是为了什么呢?说来可笑,一部分去就业,一部分人继续科研。无论是工业界的知识,还是科研能力,本科一概不教。对于大学来说,能通过高考把优秀的人才招进来,本科设立的意义就已经达到了。既然如此,毕业设计怎么搞不都一样了吗。

实况记录

3.22 18:13

现在,游戏开始。先稍微讲一下我已经做好了哪些准备:我已经建立好了VS项目,导入了之前了OpenCV环境,并成功实现了普通的图像复制。我下好了别人实现论文的代码,以及代码中将用到的Eigen数学库。我还没有学过Eigen的用法,也没有把整个方法的实现流程完全想好。

18:26

理解了此方法中如何构建最后的求解矩阵。现在学习如何用Eigen求解稀疏矩阵。

18:36

知道如何用Eigen创建矩阵,求解矩阵了。可以考虑写代码了。先想办法把Eigen导入项目,并且测试一下Eigen解方程代码。

18:39

很烦!Eigen还要编译,计划中断。我准备一边编译一边盲写代码(写的代码无法进行测试)。

18:57

一边复习OpenCV,一边写完了拉普拉斯滤波代码。同时Eigen似乎编译完了。

19:09

Eigen编译到一半,出了问题,开始向搜索引擎求教。最烦人的配环境时间开始了。

19:21

气死了,这个库可以不编译直接使用。重新开始Eigen的使用测试。

19:36

写到一半,OpenCV配置又出错了。原来是之前写的OpenCV配置文件少包含了一个库文件。批评一下过去的自己,太坑了吧。

19:47

OK,Eigen测试完了,游戏结束。现在所有不确定的东西都搞定了,前置知识学习完毕。接下来我可以展示王者级别的写代码能力了。

20:26

潇潇洒洒写了几十行,突然发现我不会对OpenCV和Eigen两个库的矩阵进行互相转化,又开始查东西了。时间还很充裕,问题不大。

20:30

原来OpenCV提供了转换函数。好,工作继续。

20:37

虽然我没有花半秒钟在程序的软件工程设计上,但我潜意识里已经把代码的结构、实现顺序给想好了。凭借着强大的功底,我本能地使用了自底向上的思想,先把每个细节的实现函数写好了。现在我准备把整个程序拼起来。

20:46

好,程序拼完了,已经开始编译了。说实话,第一遍能不能过(没有BUG)?肯定过不了。无论多强的高手,都会写BUG,都有大意的时候。BUG本身并不可怕,可怕的是不会调试BUG。编程能力展示结束,现在展示我发现BUG的洞察力和解决BUG的决断力。

20:54

代码还是倒在了OpenCV和Eigen两个库的互相兼容上。三个小时快到了,你会不会觉得我很慌?你会不会觉得我已经写不完了?不好意思,现在我才刚刚登上展示实力的舞台。只有在逆境中绝处逢生,才能体现出一个人的过人之处。

20:58

突然发现代码漏写了!图像有RGB三个颜色通道,该方法要对每个颜色通道单独处理,我忘了拆开颜色通道了!

21:03

OpenCV和Eigen相互转换的库函数不能用,我就自己动手!总算,稍加调试之下,代码可以运行了!只不过代码的运行效果明显不对……这下调试起来就麻烦了。很好,最尴尬的情况出现了。我不会放弃的!

21:17

时间已经过了。但我有大约10分27秒花费在了写博文上,这些时间应该不算。继续计时。

21:45

我发现我对论文方法的理解出现了问题。

22:25

更改对论文理解上的逻辑BUG后,又发现8位颜色通道溢出了。

22:34

历经千辛万苦,程序总算是成功实现了。总耗时约260分钟。多花费了约50%的时间,你肯定说我很菜,说我写代码写得烂。但是这种说法并不对。仔细看一看这份文字实况,你能看出什么?没错,我一直在强调我纯粹的编程实力,并没有强调我有快速把论文转换成代码的能力,也没有强调我有快速学会调用那些坑爹的公共库的能力。我写代码写了多久?19:47~20:26,39分钟;20:30~20:46,16分钟,一共55分钟。第一版本的代码,我花55分钟就写完了。后面又调试了一下,20:54~22:34,100分钟,减去重看论文的21:17~21:45这28分钟,又花了72分钟调试,最后也就改了5%不到的代码。我说这么多数据是为了什么?是为了说明,恰恰是这份实况记录,强调了我编程能力之强,强调了时间其实都耗费在一些编程之外的时间上。虽然没有在规定时间内写完,但我感觉虽败犹荣。

当然,你要说我每个方面的表现都很好吗?也不是这样的。我自己也承认,理解论文的时候急躁了一些,学库函数的时候学的不仔细了一些,导致我这个第一版的程序,出现了一点小小的偏差。不过真要说起来,我今天确实有点急于求成。因为立下了3小时完成的flag,所以我做事火急火燎,忽略了工作质量,结果败给了自己。如果一开始没有这个flag,我能不能3小时内完成呢?你可能会认为我会说:“会”。好,实际情况是,我不立这个flag,我今天会玩一个晚上,不会来做这个项目,哈哈。

参考文献

[1]. Pérez P, Gangnet M, Blake A. Poisson image editing[M]//ACM SIGGRAPH 2003 Papers. 2003: 313-318.

最近买了台新电脑,所有编程都需要重新配置,可以写很多水博文了。

Visual Studio 2019(VS2019)安装教程(解决VS scanf问题)

准确来说,这是VS2019在Windows 10下为了配置C++编程环境而写的教程。

在下载前,请务必保证C盘的空间比较充裕,因为不论安装在哪,VS都会挤占C盘的很多空间。

下载

  1. 打开官方网站https://www.visualstudio.com/vs,点击网站中间那个醒目的“下载 Visual Studio”按钮,再选择”Community 2019”(即社区版)。社区版是免费的,其他几个版本都要付费。作为普通的程序员,使用社区版就够了。1

  2. 由于我们点击了下载按钮,为VS贡献了宝贵的+1下载量,我们得到了来自微软公司的感谢。浏览器此时会自动开始下载。

    2下载很快就会结束。规模这么庞大的编程软件,几分钟就下载完了?其实我们现在只下载了一个下载器,待会才会正式开始下载VS的本体。

安装

  1. 下载完成后,在文件夹里找到下载好的可执行文件并点击执行。处理掉操作系统弹出的烦人的提示后,我们可以看到如下界面:

    3

    这说明安装正式开始了。

  2. 点击继续后,下载器会读一个进度条。读条结束后,可以看到如下的安装配置界面:

    4刚看到这个界面,第一次用VS的人肯定会有点懵:哎呀!怎么这么多选项啊!该点哪个呀?

    但如果你是一个有推理能力的人,你看到这个界面,哪怕不去网上查询VS的使用方法,也能猜出这个页面的意思:VS是一个集成开发环境(IDE),可以进行不同语言的开发。这个界面是用来让用户选择要开发的语言的。

    5所以,我们应该根据自己的需要选择要开发的语言。只进行C++的开发的话,只点击”使用C++的桌面开发“即可。上图中,我多点了几个选项。

    6

    下载之前,如果C盘空间吃紧的话,可以点击“安装位置”选项卡,把VS的部分文件装在其他盘上。不过在右下角可以看到,VS的大部分依赖文件还是得装在C盘的。

    点击安装按钮后,VS就会开始下载安装了。

  3. 7

    下载结束后,就可以正式使用VS了。使用之前,会弹出如上的登录界面。可以点击“以后再说”不登录直接使用程序,也可以创建账户并登录。创建账户的好处是,你在一个账户上配置了VS的界面布局后,无论在哪台电脑上都可以直接使用自己这套界面布局。

    通过了登录界面后,还有一个主题选择界面,可以按自己的喜好定义VS的“外貌”。

使用VS写C++的Hello World

第一次打开VS2019,应该会看到如下的界面:

8

又是琳琅满目的按钮。我们不必感到慌乱,只需点击最后一个选项“创建新项目”即可。

9

10

要在一堆选项中选择项目模板。作为一个初学者,我们要从零开始学习。因此,我们先在所有语言下拉栏中选择”C++”,再选择“空项目”.

11

终于,激动人心的编程就要开始了。再次之前,我们要给项目取一个好听的名字。这里我们取名叫做”Hello World”。

12

好了,总算正式进入VS了。代码需要写到文件里,而我们现在还没有创建文件呢。所以,我们要创建一个源代码文件。右键点击右边的“源文件”文件夹,点击添加->新建项。

13

在新窗口里点击”C++文件(.cpp)”,然后在下面的“名称”输入框中输入一个响亮的名字。按照传统,我们给第一个源代码的文件取名为”main.cpp”。

14

接下来,第一份源代码文件就打开了,我们可以编辑这份文件了。无论你是否学过C++,可以往文本编辑器中输入以下代码,达到和上图一样的效果:

1
2
3
4
5
6
7
8
9
#include <iostream>

using namespace std;

int main()
{
cout << "Hello World" << endl;
return 0;
}

按下F5就可以编译并调试这份代码,如果得到了下图的结果,就说明VS能成功编译运行C++程序了。

15

当然,如果使用老的VS版本,或者因为其他一些原因,按下F5后控制台会一闪而过。这个时候。你需要在return 0这一行左边的灰色部分点一下,加一个断点,让程序运行到这行的时候停下来。这样就可以看清控制台的输入了。

16

在VS中使用scanf

理论上,这篇教程已经写完了,大家可以快乐地用VS写C/C++程序了。但是,有些人会用VS写纯C语言代码。为了保险起见,我们不妨测试一下一份C语言的“A + B“程序。输入以下代码并按F5调试运行:

17

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main()
{
int a, b;
scanf("%d %d", &a, &b);
printf("%d\n", a + b);
return 0;
}

结果编译器会报错,不让这份代码通过。

18

这是为什么呢?事实上,VS不支持纯C语言的编写,只支持C++的编写。很多早期C标准库有安全问题,VS不允许使用这些库函数。但在学习C语言,尤其是要把C语言代码提交到在线评测平台(Online Judge)的时候,不得不去使用这些C标准库的函数。为了解决这个矛盾,我们需要对VS的设置进行一些更改。

19

在右边”解决方案资源管理器“窗口中,在项目名”Hello World”上点击右键,再点击”属性”。

20

在左侧点击C/C++,把右边的SDL检查改成“否”。这样就可以使用C语言中scanf等不太安全的标准库函数了。

用Mendeley进行文献管理

在做一个有关科研的项目时,我常常把该项目相关的论文全部放到同一文件夹下。一个pdf文件刚下载下来的时候,我还能分辨出这个文件是哪篇论文。但过了一两天,我还想找这篇论文时,就只能望着文件夹里乱七八糟的pdf文件名而不知所措了(论文pdf的命名方式五花八门)。我深知对论文进行管理的重要性,但由于之前的项目涉及的论文数量较少,我勉勉强强没有在搜索文献时碰到太多困难,出于懒惰,我没有对文献进行管理。最近,我要开始写毕业论文了,恰好有了一个让自己开始学习文献管理的理由。

我用极快的速度在网上搜索文献管理软件有关信息,在知乎上稍微浏览一会儿后,选择了Mendeley这款软件。我没用过其他软件,无法给出不同软件之间的优劣,只能根据别人的信息,学习一下Mendeley这款文献管理软件的用法,并把这些有用的信息分享出来。本文将包括以下内容:

  • Mendeley基本安装使用方法
  • 如何用Mendeley达成自己的文献管理目标

Mendeley下载安装

开始使用Mendeley前,要完成以下几件事:

  • 注册账号
  • 下载软件
  • 安装word引用插件

直接在搜索引擎中搜索“Mendeley”即可找到官网(https://www.mendeley.com )。官网上有一个很大的“DOWNLOAD”,可以跳转到软件版本选择界面,有各个平台的软件版本以供下载。官网上还有一个很显著的“Create Account”,设置邮箱密码,不需邮箱验证即可注册。

软件下载安装输入账号密码后即可开始使用软件。

初次点开软件后,有一个添加word文献引用插件的提示,可以在关闭了所有word文档后点击安装。

Mendeley基本使用方法

Mendeley包括以下功能:

  • 文档层级管理
  • 文档导入
  • 文档重命名
  • 文档辅助标记
  • 文档同步
  • 文档搜索

文档层级管理保证了文档能按一定逻辑结构进行存储,这是一切后续文档操作的基础。文档经导入后,可以按照你熟悉的方式进行重命名,也可以对文档添加一些标记,方便从文献的检索和管理。文档同步保证了一台电脑上导入的文档能够在其他设备上方便地访问。

文档层级管理

Folder

软件左上角My Library就是文件夹列表。你可以自己创建文件夹并重命名,并在文件夹中再创建文件夹,实现对论文的分项目、分类管理。

除了自定义文件夹外,还有一些预定义文件夹,从这些文件夹的名字基本就可以知道它们的用途。比如Recently Added文件夹,就和win10系统下资源管理器左上角的”快速访问“一样,能找到最近访问的文档。

文档导入

Import

为了把pdf格式的论文导入,可以点击左上角Add在操作系统的资源管理器中添加文档,也可以直接在资源管理器(此电脑)中框选一堆文档拖入中间的空白处。注意到中上处有一个文件夹的提示(本图中是Undergraduate Capstone),表明论文是被导入了一个具体的文件夹中。

文档导入不仅仅是让软件”知道“了你有哪些论文,在文档导入后,软件还会根据pdf自动分析出论文的发表时间、作者、期刊。

文档重命名

重命名功能能够把重新把文档在操作系统里的文件名按一定格式进行修改,而不只是在软件中取了一个虚拟的名字。当然,为了保证重命名的正确运行,需要关掉正在打开的pdf文件。重命名的具体操作如下:Rename

选择一些文档后,点击右键,可以在选项中找到“重命名文档”这一功能。

Rename2

重命名功能能让每篇论文的文件名以任意的顺序包含期刊、发表年份、作者、标题这四种信息。比如在上面的截图中,我是按“年份-作者-标题”这样的格式对所有文件重命名。

重命名结束后,可以再次右键点击文档选择”打开文档所在文件夹“(Open Containing Folder),在操作系统里查看这些文件。

文档辅助标记

右键点击文档,可以看到一个叫做”标记为“(Mark As)的选项,该选项能给每个文档一些标记,包括是否读完,是否喜欢,是否需要回顾。

界面右端有三个选项卡,其中笔记(Note)一栏可以对每一个文档做一些简要的笔记。

界面右端Details选项卡里有一个标签(Tags)选项,可以给文章添加标签。

文档同步

界面上方有一个循环箭头图标,点击该图标即可同步。理论上只要是登录同一个账号,就可以在不同的电脑上访问同样的内容。

文档搜索

界面右上角有一个搜索框,输入东西就可以搜索。

理论上,不管是论文名、文章内容、还是添加的一些辅助标记,都可以通过搜索功能搜索到。这也是添加辅助标记的意义所在。

使用文献管理软件的目标

使用工具一般有两种出发点:知道有哪些需求,而去寻找工具;得知了工具有哪些功能,再去看这些功能能做什么。前几节只介绍了Mendeley这个工具本身能做什么,而这一节将按照第一种使用工具的出发点,忽视工具而只谈论文献管理有哪些需求。下一节将结合工具和需求分析一下该怎么使用Mendeley。

我的阅读论文经验尚浅,这里仅能给出两个文献管理的需求:快速检索、快速预览。

在读某一篇论文的时候,经常会碰到一个子问题已经有了一个成熟的解决方法,作者直接贴了一篇参考文献的情况。这个时候,你肯定很希望能够立马看到这篇参考文章,和刚刚阅读的内容关联起来。如果这篇参考文献没有下载下来,你还可以发出一两句怨天尤人的抱怨:”世界上论文怎么这么多啊“,”下载个论文怎么这么麻烦啊“。但如果这篇论文已经下载了,你在电脑里找不到,你只能怪自己道:”我之前下载论文的时候怎么没有好好整理一下啊!“我们希望脑中想到一篇论文,屏幕上就能立刻出现哪篇论文。

还有一些情况,我们看到一篇以前读过的论文,突然忘记了它讲了什么。打开文档、浏览全文、关闭文档可要花费不少时间。一个最简单的方法,就是对每一篇文章做一些自己的笔记。看着自己对论文的描述,一般就能想起论文的内容。问题是,电脑里有那么多论文,存在文件夹的各个角落里,该把笔记写哪里,怎么写呢?

使用Mendeley完成文献管理

有了Mendeley,完成文献的快速检索和快速预览讲不再是梦想。Mendeley本身提供了多种给文档添加标记的方式,利用搜索功能能方便快捷地定位到你想要的文章。同时,利用软件的笔记功能,能够给每篇文章添加自己的描述,配合上搜索功能,达到快速找到文章,并快速想起文章的”combo“。

根据我现在对Mendeley的认知,一个合理的使用该软件的流程如下:

  1. 为当前项目创建新的文件夹,让自己有一种”我要开始做大事了“的仪式感
  2. 找一堆论文,下载并导入Mendeley
  3. 重命名所有论文,使得哪怕不打开Mendeley也能快速在文件系统中找到文章
  4. 每看完一篇论文后,添加一些能让自己看得懂的简短笔记,视心情添加标签
  5. 如果对该项目相关的论文有了一定的认识,建立子文件夹,对文献进行一级分类
  6. 动态维护添加这个文件夹,根据需要对文献进行搜索

总结

我向来觉得写一篇大部分都是信息(比如软件安装方法)的文章没什么意义。网上参考资料那么多,何必要再写一些原创性较低的东西呢?但最后我还是完成了这样一篇”工具使用指南“的文章,介绍了Mendeley这个文献管理软件的用法。其原因是,我写这篇文章的目的是为了让我自己有动力去学习这个软件的使用方法,并通过费曼学习法,利用向他人重述来加深自己对信息的记忆。

这篇文章主要面向的是没有用过Mendeley的读者,尤其是从未用过文献管理软件的读者。读者通过文章前半部分,在没有下载软件的情况下,能大致知道Mendeley有哪些功能;下载了软件后,可以知道如何使用这些功能。文章后半部分对科研经验尚浅的人较有帮助,提出了一些简单的文献管理需求,并探讨了如何结合软件达成这些需求。

Poisson Image Editing 图像融合/图像无缝编辑经典文献阅读

想不想把自己的头P到别人的图片中?

想不想把别人图片里的彩虹放进自己拍的风景照中?

想不想自己修掉脸上的皱纹?

掌握图像融合技术,以上的一切图像编辑任务都可以轻松完成。

fig1

(图片来自论文)

如图所示,利用图像融合技术,可以把一幅图片的部分区域复制到一幅图片(可以是同一幅图片)上,并且让复制过去的图片块保持之前的色彩风格。因此,这种技术有时可以叫做图像无缝复制(image seamless cloning)

论文整体结构

该论文介绍了一种图像编辑的方法,其主要作用是图像边缘的无缝拼合。该方法可以应用到图像融合和图像风格变换等任务中。论文的结构如下:

fig2

从结构图可以看出,这种方法的输入是待修改图像、待修改区域及待修改区域的目标梯度,输出是编辑后的图像。基于该图像编辑方法,能产生各式各样的图像融合结果,产生不同结果的方法是修改输入中目标梯度的获取方法。

具体介绍一下这种图像融合方法。这种方法的思想是让图像编辑后的梯度域尽可能和源图像(图像编辑是从源图像复制一块到目标图像)一样,并且编辑区域边缘的梯度值和目标图像一样。图像的梯度可以理解为图像相邻像素的变化量。有研究指出,比起绝对的像素值,人眼对梯度值更敏感,因此维持编辑区域内部梯度不变,边界和目标图像“接轨”能让图像融合的质量更高。梯度值尽可能不变,就是让图像编辑前后梯度域的差值尽可能小,问题就变成了一个纯数学问题。通过相关的数学工具,可以求解出编辑后的图像来。

论文细节

优化目标的建模与求解

该论文方法的核心是建立了梯度域的优化目标后,如何用数学方法对这个问题求解。

首先,稍微介绍一下这个优化问题是如何建模的。输入图像、输出图像都可以看成一个定义域在平面上的一个函数。对输出图像的求解即对一个未知函数的求解。求解未知函数,涉及泛函的知识,需要用到欧拉-拉格朗日方程,有一篇非常好的博客[2]介绍了有关的数学知识。

有了对问题的建模后,论文直接给出化简后的待求解方程。该方程是一个泊松方程,即与函数二阶导数相关的偏微分方程。对该方程求解,即可得到最终的图像。

图像的数值都是离散的,对一个图像的求解,最终可以转化成对该图像每个像素值的求解,即求解一个多元方程。此问题中,该方程是一个线性方程。利用求解多元线性方程的方法即可解出此问题。

事实上,该问题的建模和求解中涉及了很多数学定义,这些数学的细节是否理解对整篇文章的理解、方法的创新不是十分重要。对于一个研究计算机技术的人来说,优化目标的设定才是最重要的。

利用方法产生不同的编辑效果

论文的所有图像效果都是基于同一种方法。也就是说,只要理解了最基本的把一幅图片无缝融合进另一幅图片的原理,其他图像编辑效果都可以通过同样的原理推导而来。

如果把源图像的待修改区域的梯度做为目标梯度直接输入进方法,由于图像边缘梯度和目标图像一样,内部和源图像一样,复制过去的图像可以无缝地对接到目标图像上。利用该图像融合方法,可以实现“图像隐藏”:比如把脸上没有皱纹的地方复制到有皱纹的地方,达到去皱纹效果。

只用源图像的梯度,而忽视目标图像待修改区域的梯度,则会完全抛弃待修改区域原来的信息。在复制一些半透明的图像(如彩虹)时,如果抛弃掉目标图像待修改区域本身的性质,就会产生很差的效果。论文中提出,可以把源图像梯度和目标梯度的较大值做为方法的目标梯度,这样经过图像编辑后图像的待修改区域能融合两幅图像的性质。

通过上述例子,可以得知目标区域的梯度是如何决定图像编辑效果的。对目标区域的梯度进行一些“魔改”,就能得到不同的结果,比如结构图中提到的扁平化、光照修改和地砖无缝拼接等。

论文实现

理论上,我会对论文进行实现。以后我会更新此文,加入实现的代码仓库链接。

其实随便一搜就能找到很多该论文的实现,毕竟这篇论文是图像融合的一个非常经典的方法。很多matlab代码技术性较弱,很多东西都写好了,因此我附上了一个该论文的C++实现[3]。当然,用C++的话也要调用不少库函数,最烦人的线性方程组求解是必须得调一下库的。

总结

本文对论文“Poisson Image Editing”进行了一个简要介绍,其本意是通过总结论文大意来加深我自己对论文的认识,并且方便之后对其他图像融合领域的对比阅读。

当然,这篇文章对读者来说也有一定可读性。本文并没有对原文进行死板的翻译,而是从整体到局部,大致介绍了论文中的主要内容。对于只是想浏览该文章的读者,可以学到一种新的P图方法,图一乐;对于在精读论文之前看到这篇文章的读者,可以大致知晓文章是在讲什么,解决什么问题,用了什么方法,产生什么结果;对于读完论文之后的读者,可以把这篇文章和自己的收获进行对比,看看哪些内容是读到了的,哪些内容文章没有提到,甚至哪些地方写错了,写得不够好,以加深自己对论文的理解。当然了,如果你觉得我讲的你都读到了,你也可以写出一样的东西,那说明你是个高手,学得很不错,毕竟想要掌握我这种概述能力是十分困难的。

参考资料

[1] Pérez P, Gangnet M, Blake A. Poisson image editing[M]//ACM SIGGRAPH 2003 Papers. 2003: 313-318.

[2] https://www.cnblogs.com/bigmonkey/p/9519387.html

[3] https://github.com/cheind/poisson-image-editing


2023.4 更新:
最近在重新温习Poison Image Editing,看了看自己以前写的文章,发现这篇文章写得一塌糊涂。写得这么简单,谁看得懂啊?或许我当时为了便于理解,没有在文章里用数学符号。但这文章也写得太不清楚了吧。我现在来稍微重新总结一下这个方法。

泊松图像编辑要解决的基本问题是图像融合:输入源图像、源图像上的编辑区域、目标图像、目标图像上的编辑区域,我们想把源图像的编辑区域「贴」到目标图像的编辑区域上。相比于最朴素的复制粘贴,我们希望图像贴过去后看起来没有那么突兀,就像文章展示出来的一些融合结果一样。

作者把这种图像融合看成了一个优化问题:在编辑区域边界处的像素值应该保持不变(保持原来目标图像在此处的像素值)的前提下,令编辑区域内部的梯度值与源图像该处的梯度值尽可能相似。求解这个优化问题,就能得到编辑区域内的像素值。

归根结底,泊松图像编辑的输入是一幅目标图像、一个梯度值、一个编辑区域。图像融合只是应用之一。通过修改输入的来源,可以完成多种任务。

泊松图像编辑的原理就是这么简单。更详细的介绍在那篇代码实现的博文里有介绍。

这篇文章确实是一篇实实在在的「博文」:只有我自己能看懂,别人都看不太懂。可见,我当时的描述能力比现在差远了。反过来说,没过几年,我的描述水平就大有提升。这真是太可怕了。