PhotoShop算法原理解析系列 - 风格化---》查找边缘。
之所以不写系列文章一、系列文章二这样的标题,是因为我不知道我能坚持多久。我知道我对事情的表达能力和语言的丰富性方面的天赋不高。而一段代码需要我去用心的把他从基本原理--》初步实现--》优化速度 等过程用文字的方式表述清楚,恐怕不是一件很容易的事情。
我所掌握的一些Photoshop中的算法,不能说百分之一百就是正确的,但是从执行的效果中,大的方向肯定是没有问题的。
目前,从别人的文章、开源的代码以及自己的思考中我掌握的PS的算法可能有近100个吧。如果时间容许、自身的耐心容许,我会将这些东西慢慢的整理开来,虽然在很多人看来,这些算法并不具有什么研究的价值了,毕竟人家都已经商业化了。说的也有道理,我姑且把他作为自我欣赏和自我满足的一种方式吧。
今天,我们讲讲查找边缘算法。可能我说了原理,很多人就不会看下去了,可有几人层仔细的研究过呢。
先贴个效果图吧:

原理:常见的Sobel边缘算子的结果进行反色即可。
为了能吸引你继续看下去,我先给出我的代码的执行速度: 针对3000*4000*3的数码图片,处理时间300ms。
何为Sobel,从百度抄几张图过来了并修改地址后:

对上面两个式子不做过多解释,你只需要知道其中A为输入图像,把G作为A的输出图像就可以了,最后还要做一步: G=255-G,就是查找边缘算法。
查找边缘类算法都有个问题,对图像物理边缘处的像素如何处理,在平日的处理代码中,很多人就是忽略四个边缘的像素,作为专业的图像处理软件,这可是违反最基本的原则的。对边缘进行的单独的代码处理,又会给编码带来冗余和繁琐的问题。解决问题的最简单又高效的方式就是采用哨兵边界。
写多了特效类算法的都应该知道,除了那种对单个像素进行处理的算法不需要对原始图像做个备份(不一定去全局备份),那些需要领域信息的算法由于算法的前一步修改了一个像素,而算法的当前步需要未修改的像素值,因此,一般这种算法都会在开始前对原始图像做个克隆,在计算时,需要的领域信息从克隆的数据中读取。如果这个克隆的过程不是完完全全的克隆,而是扩展适当边界后再克隆,就有可能解决上述的边界处理问题。
比如对下面的一个图,19×14像素大小,我们的备份图为上下左右各扩展一个像素的大小,并用边缘的值填充,变为21*16大小:

这样,在计算原图的3*3领域像素时,从扩展后的克隆图对应点取样,就不会出现不在图像范围内的问题了,编码中即可以少很多判断,可读性也加强了。
在计算速度方面,注意到上面的计算式G中有个开方运算,这是个耗时的过程,由于图像数据的特殊性,都必须是整数,可以采用查找表的方式优化速度,这就需要考虑表的建立。
针对本文的具体问题,我们分两步讨论,第一:针对根号下的所有可能情况建立查找表。看看GX和GY的计算公式,考虑下两者的平方和的最大值是多少,可能要考虑一会吧。第二:就是只建立0^2到255^2范围内的查找表,然后确保根号下的数字不大于255^2。为什么可以这样做,就是因为图像数据的最大值就是255,如果根号下的数字大于255^2,在求出开方值后,还是需要规整为255的。因此,本算法中应该取后者。
贴出代码:
private void CmdFindEdgesArray_Click(object sender, EventArgs e)
{
int X, Y;
int Width, Height, Stride, StrideC, HeightC;
int Speed, SpeedOne, SpeedTwo, SpeedThree;
int BlueOne, BlueTwo, GreenOne, GreenTwo, RedOne, RedTwo;
int PowerRed, PowerGreen, PowerBlue;
Bitmap Bmp = (Bitmap)Pic.Image;
if (Bmp.PixelFormat != PixelFormat.Format24bppRgb) throw new Exception("不支持的图像格式."); byte[] SqrValue = new byte[];
for (Y = ; Y < ; Y++) SqrValue[Y] = (byte)( - (int)Math.Sqrt(Y)); // 计算查找表,注意已经砸查找表里进行了反色 Width = Bmp.Width; Height = Bmp.Height; Stride = (int)((Bmp.Width * + ) & 0XFFFFFFFC);
StrideC = (Width + ) * ; HeightC = Height + ; // 宽度和高度都扩展2个像素 byte[] ImageData = new byte[Stride * Height]; // 用于保存图像数据,(处理前后的都为他)
byte[] ImageDataC = new byte[StrideC * HeightC]; // 用于保存扩展后的图像数据 fixed (byte* Scan0 = &ImageData[])
{
BitmapData BmpData = new BitmapData();
BmpData.Scan0 = (IntPtr)Scan0; // 设置为字节数组的的第一个元素在内存中的地址
BmpData.Stride = Stride;
Bmp.LockBits(new Rectangle(, , Bmp.Width, Bmp.Height), ImageLockMode.ReadWrite | ImageLockMode.UserInputBuffer, PixelFormat.Format24bppRgb, BmpData); Stopwatch Sw = new Stopwatch(); // 只获取计算用时
Sw.Start(); for (Y = ; Y < Height; Y++)
{
System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + ), ); // 填充扩展图的左侧第一列像素(不包括第一个和最后一个点)
System.Buffer.BlockCopy(ImageData, Stride * Y + (Width - ) * , ImageDataC, StrideC * (Y + ) + (Width + ) * , ); // 填充最右侧那一列的数据
System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + ) + , Width * );
}
System.Buffer.BlockCopy(ImageDataC, StrideC, ImageDataC, , StrideC); // 第一行
System.Buffer.BlockCopy(ImageDataC, (HeightC - ) * StrideC, ImageDataC, (HeightC - ) * StrideC, StrideC); // 最后一行 for (Y = ; Y < Height; Y++)
{
Speed = Y * Stride;
SpeedOne = StrideC * Y;
for (X = ; X < Width; X++)
{
SpeedTwo = SpeedOne + StrideC; // 尽量减少计算
SpeedThree = SpeedTwo + StrideC; // 下面的就是严格的按照Sobel算字进行计算,代码中的*2一般会优化为移位或者两个Add指令的,如果你不放心,当然可以直接改成移位
BlueOne = ImageDataC[SpeedOne] + * ImageDataC[SpeedTwo] + ImageDataC[SpeedThree] - ImageDataC[SpeedOne + ] - * ImageDataC[SpeedTwo + ] - ImageDataC[SpeedThree + ];
GreenOne = ImageDataC[SpeedOne + ] + * ImageDataC[SpeedTwo + ] + ImageDataC[SpeedThree + ] - ImageDataC[SpeedOne + ] - * ImageDataC[SpeedTwo + ] - ImageDataC[SpeedThree + ];
RedOne = ImageDataC[SpeedOne + ] + * ImageDataC[SpeedTwo + ] + ImageDataC[SpeedThree + ] - ImageDataC[SpeedOne + ] - * ImageDataC[SpeedTwo + ] - ImageDataC[SpeedThree + ];
BlueTwo = ImageDataC[SpeedOne] + * ImageDataC[SpeedOne + ] + ImageDataC[SpeedOne + ] - ImageDataC[SpeedThree] - * ImageDataC[SpeedThree + ] - ImageDataC[SpeedThree + ];
GreenTwo = ImageDataC[SpeedOne + ] + * ImageDataC[SpeedOne + ] + ImageDataC[SpeedOne + ] - ImageDataC[SpeedThree + ] - * ImageDataC[SpeedThree + ] - ImageDataC[SpeedThree + ];
RedTwo = ImageDataC[SpeedOne + ] + * ImageDataC[SpeedOne + ] + ImageDataC[SpeedOne + ] - ImageDataC[SpeedThree + ] - * ImageDataC[SpeedThree + ] - ImageDataC[SpeedThree + ]; PowerBlue = BlueOne * BlueOne + BlueTwo * BlueTwo;
PowerGreen = GreenOne * GreenOne + GreenTwo * GreenTwo;
PowerRed = RedOne * RedOne + RedTwo * RedTwo; if (PowerBlue > ) PowerBlue = ; // 处理掉溢出值
if (PowerGreen > ) PowerGreen = ;
if (PowerRed > ) PowerRed = ;
ImageData[Speed] = SqrValue[PowerBlue]; // 查表
ImageData[Speed + ] = SqrValue[PowerGreen];
ImageData[Speed + ] = SqrValue[PowerRed]; Speed += ; // 跳往下一个像素
SpeedOne += ;
}
}
Sw.Stop();
this.Text = "计算用时: " + Sw.ElapsedMilliseconds.ToString() + " ms"; Bmp.UnlockBits(BmpData); // 必须先解锁,否则Invalidate失败
}
Pic.Invalidate();
}
为简单的起见,这里先是用的C#的一维数组实现的,并且计时部分未考虑图像数据的获取和更新, 因为真正的图像处理过程中图像数据肯定是已经获得的了。
针对上述代码,编译为Release模式后,执行编译后的EXE,对于3000*4000*3的彩色图像,耗时约480ms,如果你是在IDE的模式先运行,记得一定要在选项--》调试--》常规里不勾选 在模块加载时取消JIT优化(仅限托管)一栏。

上述代码中的填充克隆图数据时并没有新建一副图,然后再填充其中的图像数据,而是直接填充一个数组,图像其实不就是一片连续内存加一点头信息吗,头信息已经有了,所以只要一片内存就够了。
克隆数据的填充采用了系统Buffer.BlockCopy函数,该函数类似于我们以前常用CopyMemory,速度非常快。
为进一步调高执行速度,我们首先来看看算法的关键耗时部位的代码,即for (X = 0; X < Width; X++)内部的代码,我们取一行代码的反编译码来看看:
BlueOne = ImageDataC[SpeedOne] + * ImageDataC[SpeedTwo] + ImageDataC[SpeedThree] - ImageDataC[SpeedOne + ] - * ImageDataC[SpeedTwo + ] - ImageDataC[SpeedThree + ]; cmp ebx,edi
jae 0000073C // 数组是否越界?
0000030a movzx eax,byte ptr [esi+ebx+] // 将ImageDataC[SpeedOne]中的数据传送的eax寄存器
0000030f mov dword ptr [ebp-80h],eax
mov edx,dword ptr [ebp-2Ch]
cmp edx,edi
jae 0000073C // 数组是否越界?
0000031d movzx edx,byte ptr [esi+edx+] // 将ImageDataC[SpeedTwo]中的数据传送到edx寄存器
add edx,edx // 计算2*ImageDataC[SpeedTwo]
add eax,edx // 计算ImageDataC[SpeedOne]+2*ImageDataC[SpeedTwo],并保存在eax寄存器中
cmp ecx,edi
jae 0000073C
0000032e movzx edx,byte ptr [esi+ecx+] // 将ImageDataC[SpeedThree]中的数据传送到edx寄存器
mov dword ptr [ebp+FFFFFF78h],edx
add eax,edx
0000033b lea edx,[ebx+]
0000033e cmp edx,edi
jae 0000073C
movzx edx,byte ptr [esi+edx+]
0000034b mov dword ptr [ebp+FFFFFF7Ch],edx
sub eax,edx
mov edx,dword ptr [ebp-2Ch]
add edx,
cmp edx,edi
0000035b jae 0000073C
movzx edx,byte ptr [esi+edx+]
add edx,edx
sub eax,edx
0000036a lea edx,[ecx+]
0000036d cmp edx,edi
0000036f jae 0000073C
movzx edx,byte ptr [esi+edx+]
0000037a mov dword ptr [ebp+FFFFFF74h],edx
sub eax,edx
mov dword ptr [ebp-30h],eax
上述汇编码我只注释一点点,其中最0000073c 标号,我们跟踪后返现是调用了另外一个函数:
0000073c call 685172A4
我们看到在获取每一个数组元素前,都必须执行一个cmp 和 jae指令,从分析我认为这里是做类似于判断数组的下标是否越界之类的工作的。如果我们能确保我们的算法那不会产生越界,这部分代码有很用呢,不是耽误我做正事吗。
为此,我认为需要在C#中直接利用指针来实现算法,C#中有unsafe模式,也有指针,所以很方便,而且指针的表达即可以用*,也可以用[],比如*(P+4) 和P[4]是一个意思。那么只要做很少的修改就可以将上述代码修改为指针版。
private void CmdFindEdgesPointer_Click(object sender, EventArgs e)
{
int X, Y;
int Width, Height, Stride, StrideC, HeightC;
int Speed, SpeedOne, SpeedTwo, SpeedThree;
int BlueOne, BlueTwo, GreenOne, GreenTwo, RedOne, RedTwo;
int PowerRed, PowerGreen, PowerBlue;
Bitmap Bmp = (Bitmap)Pic.Image;
if (Bmp.PixelFormat != PixelFormat.Format24bppRgb) throw new Exception("不支持的图像格式."); byte[] SqrValue = new byte[];
for (Y = ; Y < ; Y++) SqrValue[Y] = (byte)( - (int)Math.Sqrt(Y)); // 计算查找表,注意已经砸查找表里进行了反色 Width = Bmp.Width; Height = Bmp.Height; Stride = (int)((Bmp.Width * + ) & 0XFFFFFFFC);
StrideC = (Width + ) * ; HeightC = Height + ; // 宽度和高度都扩展2个像素 byte[] ImageData = new byte[Stride * Height]; // 用于保存图像数据,(处理前后的都为他)
byte[] ImageDataC = new byte[StrideC * HeightC]; // 用于保存扩展后的图像数据 fixed (byte* P = &ImageData[], CP = &ImageDataC[], LP = &SqrValue[])
{
byte* DataP = P, DataCP = CP, LutP = LP;
BitmapData BmpData = new BitmapData();
BmpData.Scan0 = (IntPtr)DataP; // 设置为字节数组的的第一个元素在内存中的地址
BmpData.Stride = Stride;
Bmp.LockBits(new Rectangle(, , Bmp.Width, Bmp.Height), ImageLockMode.ReadWrite | ImageLockMode.UserInputBuffer, PixelFormat.Format24bppRgb, BmpData); Stopwatch Sw = new Stopwatch(); // 只获取计算用时
Sw.Start(); for (Y = ; Y < Height; Y++)
{
System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + ), ); // 填充扩展图的左侧第一列像素(不包括第一个和最后一个点)
System.Buffer.BlockCopy(ImageData, Stride * Y + (Width - ) * , ImageDataC, StrideC * (Y + ) + (Width + ) * , ); // 填充最右侧那一列的数据
System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + ) + , Width * );
}
System.Buffer.BlockCopy(ImageDataC, StrideC, ImageDataC, , StrideC); // 第一行
System.Buffer.BlockCopy(ImageDataC, (HeightC - ) * StrideC, ImageDataC, (HeightC - ) * StrideC, StrideC); // 最后一行 for (Y = ; Y < Height; Y++)
{
Speed = Y * Stride;
SpeedOne = StrideC * Y;
for (X = ; X < Width; X++)
{
SpeedTwo = SpeedOne + StrideC; // 尽量减少计算
SpeedThree = SpeedTwo + StrideC; // 下面的就是严格的按照Sobel算字进行计算,代码中的*2一般会优化为移位或者两个Add指令的,如果你不放心,当然可以直接改成移位
BlueOne = DataCP[SpeedOne] + * DataCP[SpeedTwo] + DataCP[SpeedThree] - DataCP[SpeedOne + ] - * DataCP[SpeedTwo + ] - DataCP[SpeedThree + ];
GreenOne = DataCP[SpeedOne + ] + * DataCP[SpeedTwo + ] + DataCP[SpeedThree + ] - DataCP[SpeedOne + ] - * DataCP[SpeedTwo + ] - DataCP[SpeedThree + ];
RedOne = DataCP[SpeedOne + ] + * DataCP[SpeedTwo + ] + DataCP[SpeedThree + ] - DataCP[SpeedOne + ] - * DataCP[SpeedTwo + ] - DataCP[SpeedThree + ];
BlueTwo = DataCP[SpeedOne] + * DataCP[SpeedOne + ] + DataCP[SpeedOne + ] - DataCP[SpeedThree] - * DataCP[SpeedThree + ] - DataCP[SpeedThree + ];
GreenTwo = DataCP[SpeedOne + ] + * DataCP[SpeedOne + ] + DataCP[SpeedOne + ] - DataCP[SpeedThree + ] - * DataCP[SpeedThree + ] - DataCP[SpeedThree + ];
RedTwo = DataCP[SpeedOne + ] + * DataCP[SpeedOne + ] + DataCP[SpeedOne + ] - DataCP[SpeedThree + ] - * DataCP[SpeedThree + ] - DataCP[SpeedThree + ]; PowerBlue = BlueOne * BlueOne + BlueTwo * BlueTwo;
PowerGreen = GreenOne * GreenOne + GreenTwo * GreenTwo;
PowerRed = RedOne * RedOne + RedTwo * RedTwo; if (PowerBlue > ) PowerBlue = ; // 处理掉溢出值
if (PowerGreen > ) PowerGreen = ;
if (PowerRed > ) PowerRed = ; DataP[Speed] = LutP[PowerBlue]; // 查表
DataP[Speed + ] = LutP[PowerGreen];
DataP[Speed + ] = LutP[PowerRed]; Speed += ; // 跳往下一个像素
SpeedOne += ;
}
}
Sw.Stop();
this.Text = "计算用时: " + Sw.ElapsedMilliseconds.ToString() + " ms"; Bmp.UnlockBits(BmpData); // 必须先解锁,否则Invalidate失败
}
Pic.Invalidate();
}
同样的效果,同样的图像,计算用时330ms。
我们在来看看相同代码的汇编码:
BlueOne = DataCP[SpeedOne] + * DataCP[SpeedTwo] + DataCP[SpeedThree] - DataCP[SpeedOne + ] - * DataCP[SpeedTwo + ] - DataCP[SpeedThree + ]; movzx eax,byte ptr [esi+edi]
0000031c mov dword ptr [ebp-74h],eax
0000031f movzx edx,byte ptr [esi+ebx]
add edx,edx
add eax,edx
movzx edx,byte ptr [esi+ecx]
0000032b mov dword ptr [ebp-7Ch],edx
0000032e add eax,edx
movzx edx,byte ptr [esi+edi+]
mov dword ptr [ebp-78h],edx
sub eax,edx
0000033a movzx edx,byte ptr [esi+ebx+]
0000033f add edx,edx
sub eax,edx
movzx edx,byte ptr [esi+ecx+]
mov dword ptr [ebp-80h],edx
0000034b sub eax,edx
0000034d mov dword ptr [ebp-30h],eax
生产的汇编码简洁,意义明确,对比下少了很多指令。当然速度会快很多。
注意这一段代码:
fixed (byte* P = &ImageData[], CP = &ImageDataC[], LP = &SqrValue[])
{
byte* DataP = P, DataCP = CP, LutP = LP;
如果你把更换为:
fixed (byte* DataP = &ImageData[], DataCP = &ImageDataC[], LutP = &SqrValue[])
{
代码的速度反而比纯数组版的还慢,至于为什么,实践为王吧,我也没有去分析,反正我知道有这个结果。你可以参考铁哥的一篇文章:
闲谈.Net类型之public的不public,fixed的不能fixed
当然这个还可以进一步做小动作的的优化,比如movzx eax,byte ptr [esi+edi] 这句中,esi其实就是数组的基地址,向这样写DataCP[SpeedOne] ,每次都会有这个基址+偏移的计算的,如果能实时直接动态控制一个指针变量,使他直接指向索要的位置,则少了一次加法,虽然优化不是很明显,基本可以达到问中之前所提到的300ms的时间了。具体的代码可见附件。
很多人可能对我这些东西不感冒,说这些东西丢给GPU比你现在的.......希望这些朋友也不要过分的打击吧,每个人都有自己的爱好,我只爱好CPU。
完整工程下载地址:http://files.cnblogs.com/Imageshop/FindEdges.rar
同一个图片,本例和PS所得结果有10%左右的差异。
***************************作者: laviewpbt 时间: 2013.7.4 联系QQ: 33184777 转载请保留本行信息*************************
PhotoShop算法原理解析系列 - 风格化---》查找边缘。的更多相关文章
- PhotoShop算法原理解析系列 - 像素化---》碎片。
接着上一篇文章的热度,继续讲讲一些稍微简单的算法吧. 本文来讲讲碎片算法,先贴几个效果图吧: 这是个破坏性的滤镜,拿美女来说事是因为搞图像的人90%是男人,色色的男人. 关于碎 ...
- 2. Attention Is All You Need(Transformer)算法原理解析
1. 语言模型 2. Attention Is All You Need(Transformer)算法原理解析 3. ELMo算法原理解析 4. OpenAI GPT算法原理解析 5. BERT算法原 ...
- 3. ELMo算法原理解析
1. 语言模型 2. Attention Is All You Need(Transformer)算法原理解析 3. ELMo算法原理解析 4. OpenAI GPT算法原理解析 5. BERT算法原 ...
- 4. OpenAI GPT算法原理解析
1. 语言模型 2. Attention Is All You Need(Transformer)算法原理解析 3. ELMo算法原理解析 4. OpenAI GPT算法原理解析 5. BERT算法原 ...
- 5. BERT算法原理解析
1. 语言模型 2. Attention Is All You Need(Transformer)算法原理解析 3. ELMo算法原理解析 4. OpenAI GPT算法原理解析 5. BERT算法原 ...
- FastText算法原理解析
1. 前言 自然语言处理(NLP)是机器学习,人工智能中的一个重要领域.文本表达是 NLP中的基础技术,文本分类则是 NLP 的重要应用.fasttext是facebook开源的一个词向量与文本分类工 ...
- LRU算法原理解析
LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的. 现代操作系统提供了一种对主存的抽象概念虚拟内存,来对主存进行更好地管理.他将主存 ...
- 最全排序算法原理解析、java代码实现以及总结归纳
算法分类 十种常见排序算法可以分为两大类: 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序. 线性时间非比较类排序:不通过 ...
- CGI原理解析系列之中的一个----CGI怎样获取WEBserver数据
//gcc get_post.c -o get_post.ums; #include <stdio.h> #include <stdlib.h> #include <un ...
随机推荐
- WinServer远程部署系统(RDSystem)
系统简介 RemoteDeploySystem是一套BS结构的远程部署管理系统(简称RDSystem),可以通过浏览器上传web站点和windows服务的更新包到RDSystem服务器,然后对多个服务 ...
- Visual Studio 2015在.NET Core RC2项目中的一个错误。
更新了.NET Core RC2 之后,VS的Web Tools更新为“Preview 1”了. 这个版本有一个问题,害我折腾了一个下午. 就是在项目界面的“依赖项 - NPM”上面错误地显示了不必要 ...
- Unity3D 5.x 简单实例 - 发射炮弹
1,下载.安装: http://unity3d.com/cn/get-unity/download/archive 建议直接借助 UnityDownloadAssistant 进行安装,根据需要勾选需 ...
- SQLServer并发问题,先SELECT后UPDATE,避免并发脏读情况解决
在SQL Server中,需要对数据操作进行先SELECT 之后UPDATE,对于这样的操作,如果出现高并发,可能导致脏读情况的发生.不能保证数据的同步. 解决方案是在事物中对表进行加更新锁: 事务一 ...
- 转载:《TypeScript 中文入门教程》 16、Symbols
版权 文章转载自:https://github.com/zhongsp 建议您直接跳转到上面的网址查看最新版本. 介绍 至ECMAScript 2015开始,symbol成为了一种新的原始类型,就像n ...
- Hibernate —— 概述与 HelloWorld
一.Hibernate 概述 1.Hibernate 是一个持久化框架 (1)从狭义的角度来讲,“持久化” 仅仅指把内存中的对象永久的保存到硬盘中的数据库中. (2)从广义的角度来讲,“持久化” 包括 ...
- Longest Substring Without Repeating Characters(C语言实现)
Given a string, find the length of the longest substring without repeating characters. Examples: Giv ...
- 【HTML】 frame和iframe的区别
1.frame不能脱离frameSet单独使用,iframe可以: 2.frame不能放在body中:如下可以正常显示: <!--<body>--> <frameset ...
- Webstorm常用的快捷键
WS的常用操作: 常用快捷键(Keymap/Eclipse): 复制当前行: Ctrl+Alt+↓ 向上/下移动当前行: Alt+↑/↓ 删除当前行: Ctrl+D 注释/取消当前行: Ctrl+/ ...
- Python开发【第三篇】:Python基本之文件操作
Python基本之文本操作 一.初识文本的基本操作 在python中打开文件有两种方式,即:open(...) 和 file(...) ,本质上前者在内部会调用后者来进行文件操作,推荐使用 open ...