-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 178 KB
/
content.json
1
{"meta":{"title":"Youngs's Blog","subtitle":"Good Good Study, Day Day Up !","description":"吾虽浪迹天涯,却未迷失本心。","author":"Youngs","url":"https://stonema.github.io"},"pages":[{"title":"关于作者","date":"2018-03-20T14:05:10.000Z","updated":"2020-01-18T01:45:05.232Z","comments":true,"path":"about/index.html","permalink":"https://stonema.github.io/about/index.html","excerpt":"","text":"欢迎来到我的博客。 在这里记录自己的收获。 在这里憧憬自己的未来。 在这里充实自己的现在。 —- 吾虽浪迹天涯,却未迷失本心。"},{"title":"categories","date":"2018-03-20T13:27:38.000Z","updated":"2020-01-18T01:45:05.233Z","comments":true,"path":"categories/index.html","permalink":"https://stonema.github.io/categories/index.html","excerpt":"","text":""},{"title":"tags","date":"2018-03-20T13:25:18.000Z","updated":"2020-01-18T01:45:05.233Z","comments":true,"path":"tags/index.html","permalink":"https://stonema.github.io/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"Welcome Back","slug":"Welcome-Back","date":"2020-01-18T02:15:41.000Z","updated":"2020-01-18T02:55:16.982Z","comments":true,"path":"2020/01/18/Welcome-Back/","link":"","permalink":"https://stonema.github.io/2020/01/18/Welcome-Back/","excerpt":"","text":"2020, Welcome Back.","categories":[],"tags":[]},{"title":"Java多线程与线程安全问题","slug":"Java多线程与线程安全问题","date":"2018-10-12T06:54:59.000Z","updated":"2020-01-18T01:45:05.164Z","comments":true,"path":"2018/10/12/Java多线程与线程安全问题/","link":"","permalink":"https://stonema.github.io/2018/10/12/Java多线程与线程安全问题/","excerpt":"","text":"Java中的线程问题Servlet是线程安全的吗?Servlet 默认是单例模式,在web 容器中只创建一个实例,所以多个线程同时访问servlet的时候,Servlet是线程不安全的。那么 web 容器能为每个请求创建一个Servlet的实例吗?当然是可以的,只要Servlet实现SingleThreadModel接口,就可以了。12import javax.servlet.SingleThreadModel public class MyServlet extends HttpServlet implements SingleThreadModel {} 变量的线程安全只有全局变量和静态变量才会引起线程安全问题,因为它们存在多线程内存共享;局部变量是每个对象独有的,不存在变量共享,也就没有线程安全问题;","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://stonema.github.io/tags/Java/"},{"name":"线程安全","slug":"线程安全","permalink":"https://stonema.github.io/tags/线程安全/"}]},{"title":"求逆序对数量与归并排序","slug":"求逆序对数量与归并排序","date":"2018-09-30T01:02:22.000Z","updated":"2020-01-18T01:45:05.206Z","comments":true,"path":"2018/09/30/求逆序对数量与归并排序/","link":"","permalink":"https://stonema.github.io/2018/09/30/求逆序对数量与归并排序/","excerpt":"","text":"逆序对统计最近写算法题的时候又遇到了这类问题:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。输入:1,2,3,4,5,6,7,0输出: 7可以从上面得知,逆序对分别为:<1,0> <2,0> <3,0> <4,0> <5,0> <6,0> <7,0>这里问题,一个所有人都能想到的求解方法就是暴力搜索:用两层for循环将数组中的元素全部两两比较一遍,就能得到所有的逆序对数。当然这样的做法也不是不行,但是效果是时间效率非常的低下,时间复杂度是O(n^2);在数据量很大的时候,响应时间会非常的久,这在算法上是不能接受的。今天我们学习另一种思想:Divide And Conquer(分而治之—分治思想),它的核心思想就是:大事化小,通过解决多个子问题来解决大问题。这个问题的解决思路是和归并排序的思想类似的。话不多说先看代码: (可以用归并排序来作为带入) 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748public class 数组逆向序对 { public static void main(String[] args) { 数组逆向序对 nx = new 数组逆向序对(); int[] a = {1, 2, 3, 4, 5, 6, 7, 0}; System.out.println(nx.InversePairs(a)); } public int InversePairs(int[] array) { int n = array.length; int[] temp = new int[n]; long ans = MergeSortAndCount(array, 0, n - 1, temp); return (int) ans; } public long MergeSortAndCount(int[] array, int left, int right, int[] temp) { if (left >= right) return 0; int mid = (left + right) / 2; long count1 = MergeSortAndCount(array, left, mid, temp); long count2 = MergeSortAndCount(array, mid + 1, right, temp); long count3 = MergeAndCount(array, left, mid, right, temp); return (count1 + count2 + count3) % 1000000007; } public long MergeAndCount(int[] array, int left, int mid, int right, int[] temp) { int i = left; int j = mid + 1; int k = left; long cnt = 0; while (i <= mid && j <= right) { if (array[i] <= array[j]) { temp[k++] = array[i++]; } else { temp[k++] = array[j++]; cnt += mid - i + 1; // 关键步骤,进行计数 } } while (i <= mid) temp[k++] = array[i++]; while (j <= right) temp[k++] = array[j++]; for (i = left; i <= right; ++i) { array[i] = temp[i]; } return cnt; }} 直接去读这段代码还是有些吃力的,主要有递归程序在,容易把我们绕晕,所以我们以归并排序作为基本思想,来慢慢解决这个问题: 归并排序归并排序,中国人翻译算法的名字的时候通常使用它们的具体含义来命名,这里归并排序也不理外,很容易理解的是,这个排序算法找中肯定包含着:归类与合并,这两个步骤。归类处理,然后合并,这种思想就是分治——分而治之也就是Divide and conquer。 归并排序的主要思想我们上面的找逆序对的问题,其实就是一个简单的归并排序问题,如果你能够充分的理解归并排序,那么上面的找逆序对的问题就不是什么难题。对于排序,我们知道,它最终的目的是将数据进行有序化,是会对数据进行位置交换的算法。(归并排序是一种稳定的排序算法)。它是通过将一个长度为n的数组分成左右两个部分,通常以n/2作为下面我们通过简单的例子来进行讲解,会做到详尽周到,希望能对自己和他人的理解有所帮助。 实例比如我们有一个数组包含4个元素[4,1,2,3],我们希望通过归并排序,将这个数组进行排序。我们可以通过下面的代码进行实现:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647public class 数组逆向序对 { public static void main(String[] args) { 数组逆向序对 nx = new 数组逆向序对(); int[] a = [4,1,2,3}; System.out.println(nx.InversePairs(a)); } public int InversePairs(int[] array) { int n = array.length; int[] temp = new int[n]; long ans = MergeSortAndCount(array, 0, n - 1, temp); return (int) ans; } public long MergeSortAndCount(int[] array, int left, int right, int[] temp) { if (left >= right) return 0; int mid = (left + right) / 2; long count1 = MergeSortAndCount(array, left, mid, temp); long count2 = MergeSortAndCount(array, mid + 1, right, temp); long count3 = MergeAndCount(array, left, mid, right, temp); return (count1 + count2 + count3) % 1000000007; } public long MergeAndCount(int[] array, int left, int mid, int right, int[] temp) { int i = left; int j = mid + 1; int k = left; long cnt = 0; while (i <= mid && j <= right) { if (array[i] <= array[j]) { temp[k++] = array[i++]; } else { temp[k++] = array[j++]; } } while (i <= mid) temp[k++] = array[i++]; while (j <= right) temp[k++] = array[j++]; for (i = left; i <= right; ++i) { array[i] = temp[i]; } return cnt; }} 这个代码与我们上面缩写的代码仅仅少一行 cnt += mid - i + 1;。而他们的实现过程是一模一样的,我们一点点对代码进行分析。我们从InversePairs开始:它调用了MergeSortAndCount方法,而MergeSortAndCount方法又调用了MergeAndCount方法。这中间涉及到递归调用,用脑子想的办法去理解可能会有些复杂,我花了一个逻辑调用图,来帮助我们理解归并排序。 归并排序的要点在于,一个是递归,一个是将左右两个部分进行对比排序,如果没有这个递归操作,那么仅仅能保证整个数组以mid为中心,右边比左边大,但是不能保证左右两侧都是有序的,带上递归后,左右两侧分别一直进行递归比较操作,知道这个小的比较区域只剩两个元素,也就完成了子区间的比较。","categories":[],"tags":[{"name":"算法基础","slug":"算法基础","permalink":"https://stonema.github.io/tags/算法基础/"},{"name":"分治","slug":"分治","permalink":"https://stonema.github.io/tags/分治/"}]},{"title":"Java中的this关键字","slug":"Java中的this关键字","date":"2018-09-12T14:37:17.000Z","updated":"2020-01-18T01:45:05.164Z","comments":true,"path":"2018/09/12/Java中的this关键字/","link":"","permalink":"https://stonema.github.io/2018/09/12/Java中的this关键字/","excerpt":"","text":"Java中的this关键字 this不能用在static方法中!! this不能用在static方法中!! this不能用在static方法中!!重要的事情说三遍!至于原因,我们知道static方法是先于类对象的创建而的,也就是说,在类的加载的过程中,static方法就已经存在了,而this的用法是指向当前加载的对象。这时候static方法所在的类还没有创建出对象,所以this并没有真实对象的引用,所以也就没有this可言。 (1)this调用本类中的属性,也就是类中的成员变量;(2)this调用本类中的其他方法;(3)this调用本类中的其他构造方法,调用时要放在构造方法的首行。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://stonema.github.io/tags/Java/"}]},{"title":"聚类算法","slug":"聚类算法","date":"2018-09-05T05:43:53.000Z","updated":"2020-01-18T01:45:05.210Z","comments":true,"path":"2018/09/05/聚类算法/","link":"","permalink":"https://stonema.github.io/2018/09/05/聚类算法/","excerpt":"","text":"常用聚类算法聚类:就是把分散的东西,按照相同的类别聚到一起。“物以类聚,人以群分”,它是一种研究,统计的手段,聚类起源于分类,但是不等于分类,聚类与分类的不同在于,聚类所要求划分的类是未知的。聚类分析内容非常丰富,有系统聚类法、有序样品聚类法、动态聚类法、模糊聚类法、图论聚类法、聚类预报法等。本文介绍一些基本的聚类算法,具体的代码实现,后续再跟进,首先了解理论思想。 聚类算法的重点是计算样本项之间的相似度,也叫样本间距的大小。聚类算法和分类算法的区别是:分类算法是有监督学习,基于有标注的历史数据进行算法模型构建。比如神经网络大多构成的是分类器,是通过已知标签的数据来调整网络。聚类算法是无监督学习,数据集中的数据是没有标注的。 K-Means(K均值)聚类算法简介:K-means聚类算法算得上是最著名的聚类方法。Kmeans算法是一个重复移动类中心点的过程,把类的中心点,也称重心(centroids),移动到其包含成员的平均位置,然后重新划分其内部成员。k是算法计算出的参数,表示类的数量;K-means可以自动分配样本到不同的类,但是不能决定究竟要分几个类。k必须是一个比训练集样本数小的正整数。有时,类的数量是由问题内容指定的。总的来讲K-means算法的数学原型来自于线性代数的最优化问题。 基本思想:给定一个有M个对象的数据集,构建一个具有k个簇的模型,其中k<=M 满足以下条件: 每个簇至少包含一个数据对象。 每个对象属于且仅属于一个簇。 将满足上述条件的k个簇称为一个合理的聚类划分。 简单讲就是把给定的总样本数据分成k个类别,首先给定初始划分,然后通过迭代改变样本和簇的隶属关系,使得每次迭代后得到的簇中各个元素到簇中心的距离变小了。 算法核心思想:每次迭代维护一个最小的样本到簇中心的距离:假设簇划分为(C{1},C{2},C{3}…C{k}),那么我们需要维护的最小平方误差E为: E=\\sum_{k}^{i=1}\\sum_{x\\in C_{i}}(x-u_{i})^{2}二分类:多分类:","categories":[{"name":"算法","slug":"算法","permalink":"https://stonema.github.io/categories/算法/"}],"tags":[{"name":"聚类","slug":"聚类","permalink":"https://stonema.github.io/tags/聚类/"}]},{"title":"JAVA中的垃圾回收与内存分配-3","slug":"JAVA中的垃圾回收与内存分配-3","date":"2018-09-03T08:25:41.000Z","updated":"2020-01-18T01:45:05.162Z","comments":true,"path":"2018/09/03/JAVA中的垃圾回收与内存分配-3/","link":"","permalink":"https://stonema.github.io/2018/09/03/JAVA中的垃圾回收与内存分配-3/","excerpt":"","text":"垃圾回收Java的垃圾回收可以有效防止内存泄露,有效使用空闲内存。内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。 对象晋升 年龄阈值VM为每个对象定义了一个对象年龄(Age)计数器, 对象在Eden出生如果经第一次Minor GC后仍然存活, 且能被Survivor容纳的话, 将被移动到Survivor空间中, 并将年龄设为1. 以后对象在Survivor区中每熬过一次Minor GC年龄就+1. 当增加到一定程度(-XX:MaxTenuringThreshold, 默认15), 将会晋升到老年代. 提前晋升: 动态年龄判定然而VM并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代: 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 而无须等到晋升年龄. 垃圾回收算法分代收集算法 VS 分区收集算法 分代收集当前主流VM垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如JVM中的 新生代、老年代、永久代. 这样就可以根据各年代特点分别采用最适当的GC算法: 在新生代: 每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集. 在老年代: 因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存. 分区收集上面介绍的分代收集算法是将对象的生命周期按长短划分为两个部分, 而分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间.在相同条件下, 堆空间越大, 一次GC耗时就越长, 从而产生的停顿也越长. 为了更好地控制GC产生的停顿时间, 将一块大的内存区域分割为多个小块, 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC所产生的停顿. . 何时回收-对象生死判定在堆里面存放着Java世界中几乎所有的对象实例, 垃圾收集器在对堆进行回收前, 第一件事就是判断哪些对象已死(可回收). 可达性分析算法:在主流商用语言(如Java、C#)的主流实现中, 都是通过可达性分析算法来判定对象是否存活的: 通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图: Object5、6、7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象: 在Java, 可作为GC Roots的对象包括: 方法区: 类静态属性引用的对象; 方法区: 常量引用的对象; 虚拟机栈(本地变量表)中引用的对象. 本地方法栈JNI(Native方法)中引用的对象。注: 即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相连接的引用链, 第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过).","categories":[{"name":"Java技术","slug":"Java技术","permalink":"https://stonema.github.io/categories/Java技术/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://stonema.github.io/tags/Java/"}]},{"title":"JAVA中的垃圾回收与内存分配-2","slug":"JAVA中的垃圾回收与内存分配-2","date":"2018-09-01T07:21:24.000Z","updated":"2020-01-18T01:45:05.160Z","comments":true,"path":"2018/09/01/JAVA中的垃圾回收与内存分配-2/","link":"","permalink":"https://stonema.github.io/2018/09/01/JAVA中的垃圾回收与内存分配-2/","excerpt":"","text":"详解内存分配通过上一篇文章,我们大题了解了变量和对象都是如何在栈内存和堆内存中分配的,那么我们继续了解。还是老套路先上个图:栈,就如同它的名字一样,JVM中的栈内存也是一个后进先出LIFO的数据结构,JVM的垃圾回收器对于并非用new开辟的内存区域,就显得无能为力(来自《JAVA编程思想》第四版中文版P87)。垃圾回收器只知道释放那些由new分配的内存。所以为了应对这种情况,便有了finalize()方法。(详细内容,查资料)要注意一点的是finalize()并不等同于C++中的析构函数。而Java中的垃圾回收符合下面的条件: 对象可能不被垃圾回收 垃圾回收并不等于“析构”栈内存与栈操作如上面的图中所示,运行时数据区中有两个栈空间:JVM栈和本地方法栈。栈,常常与线程“联系”在一起,这是因为每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈和出栈。 JVM栈JVM栈也就是虚拟机栈,它是线程私有的,也就是说是线程隔离的,即每个线程都有自己独立的虚拟机栈。它的生命周期与线程相同。JVM栈描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 本地方法栈本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常。不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。详细内容后续更新…… JVM栈之栈上内存分配在JVM中,堆是线程共享的,因此堆上的对象对于各个线程都是共享和可见的,只要持有对象的引用,就可以访问堆中存储的对象数据。虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但对于垃圾收集器来说,无论筛选可回收对象,还是回收和整理内存都需要耗费时间。如果确定一个对象的作用域不会逃逸出方法之外,那可以将这个对象分配在栈上,这样,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,无须通过垃圾收集器回收,可以减小垃圾收集器的负载。JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。 Java堆内存","categories":[{"name":"Java技术","slug":"Java技术","permalink":"https://stonema.github.io/categories/Java技术/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://stonema.github.io/tags/Java/"}]},{"title":"JAVA中的垃圾回收与内存分配-1","slug":"JAVA中的垃圾回收与内存分配-1","date":"2018-08-31T09:12:31.000Z","updated":"2020-01-18T01:45:05.159Z","comments":true,"path":"2018/08/31/JAVA中的垃圾回收与内存分配-1/","link":"","permalink":"https://stonema.github.io/2018/08/31/JAVA中的垃圾回收与内存分配-1/","excerpt":"","text":"Java的JVM中的堆和栈 通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间; 而通过new关键字和构造器创建的对象则放在堆空间。 堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured; 方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据; 程序中的字面量(literal)如直接书写的100、”hello”和常量都是放在常量池中,常量池是方法区的一部分。 栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和常量池空间不足则会引发OutOfMemoryError。 Java内存分配和垃圾回收Java语言中一个显著的特点就是引入了垃圾回收机制,使C++程序员最头疼的内存管理的问题迎刃而解,毕竟C++中的各种构造函数和析构函数能让我们头疼的找不到妈妈。但是既然有了垃圾回收机制,Java的运行效率还是明显的慢于C++,通过这篇文章,我们详细了解Java的内存管理机制与C++的区别,让我们对Java的底层设计更为了解。JVM内存的分配与回收大致可分为如下4个步骤: 何时分配 -> 怎样分配 -> 何时回收 -> 怎样回收. 一般认为new出来的对象都是被分配在堆上的,其实这个结论不完全正确,因为是大部分new出来的对象被分配在堆上,而不是全部,实际上,还有两个地方可以存放new对象,分别是:栈和TLAB(Thread Local Allocation Buffer)。 怎样分配-JVM内存分配策略Java的堆和栈 首先明白一点:我们所讨论的Java中的堆(heap)和栈(Stack),都是JVM内存中的概念,都是指的物理内存memory中的存储空间。不涉及CPU寄存器。 什么是堆,什么是栈?我们先上一张图:堆 和栈都是java用来在内存中存放数据的地方,与C++不同的是,java自动管理堆和栈,程序员不能自行设置堆和栈。java中的堆就是运行时存储数据的区域,类的实例对象可以通过new等指令建立从中分配空间。堆是由jvm自动垃圾回收器负责的,堆的优势是可以动态的分配内存大小,存储空间可以自动回收。但是缺点是,由于实在运行时动态进行空间分配,存取速度较慢。栈 的优势是:存取的速度都比堆要快,仅次于寄存器。栈数据可以共享,但是缺点时,栈空间中的数据大小和生存期必须是确定的,缺乏灵活性。栈主要存放一些基本类型的变量int, short, long, byte, float, double, boolean, char和对象句柄(引用)。 看到这里,也就明白了一些基础概念:在Java中,基本数据类型的声明和赋值,都是在栈空间中实现的。 栈就像一个有序序列,而且有个很重要的特殊性,就是存在栈中的数据可以共享:来看下面一个例子:123int a = 1;int b = 1;int c = 2; 编译器处理的过程:首先对于第一行,会在栈中创建一个变量,其引用为a,然后查找栈内存中是否有1这个值:(1)如果没有,就给这个变量赋值为1,然后a指向这个变量;(2)如果有,就直接让a直接指向1;所以这段程序,实际上栈中只为两个变量开辟了内存空间,一个存1,一个存2,而a和b都是指向1的引用,c是指向2的引用。 堆就像它的名字一样,就是一堆!!放在那里,很多很乱~上面的形容可能不是很贴切,但是堆相比于栈而言确实无序很多,大很多,那么堆中存放的都是那些内容呢?我们还是用一段代码来说明:1Person per = new Person(); 我们看上面这段代码,在Java中我们常这样写,但是实际上,这段代码包含了两步,分别是:12Person per = null; // 声明per = new Person(); // 实例化 这样这两部分操作就分别用到了栈空间和堆空间,它们在内存中的划分是这样的:堆内存用来存放所有new 创建的对象和 数组的数据,而且它的内存是可以动态调整的,对象的声明周期也不不需要明确。当堆对象对应的栈引用被销毁后,堆对象自然也就变成了无引用状态,也就会被GC回收。这部分内容我们后续会接着介绍。","categories":[{"name":"Java技术","slug":"Java技术","permalink":"https://stonema.github.io/categories/Java技术/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://stonema.github.io/tags/Java/"}]},{"title":"CCIS","slug":"CCIS","date":"2018-08-23T08:12:21.000Z","updated":"2020-01-18T01:45:05.152Z","comments":true,"path":"2018/08/23/CCIS/","link":"","permalink":"https://stonema.github.io/2018/08/23/CCIS/","excerpt":"","text":"休假归来~又要开始忙碌了……纪念一下论文录用,比想象中的easy……但是感觉像是在圈钱啊……这些机构真是……没法说……感觉真正做事情的人很少。。。不管了,自己好好干就行了~","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://stonema.github.io/tags/随笔/"}]},{"title":"WebGL","slug":"WebGL","date":"2018-07-18T05:46:17.000Z","updated":"2020-01-18T01:45:05.181Z","comments":true,"path":"2018/07/18/WebGL/","link":"","permalink":"https://stonema.github.io/2018/07/18/WebGL/","excerpt":"","text":"WebGL基础与应用新的技术啊,没人引导,自己啃的话,一开始的时候真的会觉得无从下手,尤其新的领域的内容,有时会感觉力不从 心,但是相信功夫不负有心人,只有肯努力,一定能在这个领域有所建树的,加油! 对于webGL,它实际上是OpenGL的web实现,是HTML5标准之下,新出的一套web呈现技术,能够在浏览器端实现以前只有在桌面上才能实现的复杂图形图像。 通过这篇文章,来介绍学习过程,解决实际问题。这次的问题是:如果将三维模型标准化,归一化,正常的显示在显示器上 模型矩阵,视图矩阵和投影矩阵PS:这部分内容来源于model_view_projection通常用于表示3D模型对象的三个核心矩阵就是:模型矩阵,视图矩阵,以及投影矩阵。下面我们详细介绍每个矩阵的用途,以及三者的联系。 空间中点和多边形的基本变换(如平移,缩放和旋转)由各种变换矩阵来处理。这些矩阵可以组合在一起,使它们可用于渲染复杂的3D场景。这些组合矩阵最终将原始模型数据移动到称为 剪辑空间(Clipspace) 的特殊坐标空间中。这是一个2单位宽的立方体。中心坐标为(0,0,0),而角点范围为(-1,-1,-1)至(1,1,1)。此剪辑空间被压缩到2d空间并光栅化为图像。 模型矩阵模型矩阵定义了如何获取原始模型数据,以及如何在三维世界坐标系中移动模型( which defines how you take your original model data and move it around in 3d world space.)下一步,为了获取世界坐标系中的坐标,以及将模型移动到剪辑空间中(剪辑空间其实就是我们的可视区域),我们需要投影矩阵,而常用于投影的矩阵是透视矩阵(perspective matrix)它模仿的就是照相机的原理。最后如果需要移动相机,就又需要一个视图矩阵,用来移动相机。 剪辑空间(裁剪空间 Clipspace)剪辑空间也就是我们的可视区域,任何数据位于剪辑空间外的话,则会被剪掉,并且不会渲染。在一个WebGL程序中,模型数据通常会以它自己的坐标系上传到GPU,然后顶点着色器会将这些点转换到不同的坐标系系统下进行渲染。上图是所有点必须适合的剪辑空间的可视化。它是2个单位宽,由角(-1,-1,-1)到角(1,1,1)的立方体组成。立方体的中间是点(0,0,0)。这个空间就是剪辑空间或者叫裁剪空间。裁剪空间的目标是能够方便地对渲染图元进行裁剪:完全位于这块空间内部的图元将会被保留,完全位于这块空间外部的图元将会被剔除,而与这块空间边界相交的图元就会被裁剪。。那么,这块空间是如何决定的呢?答案是由视锥体(view frustum)来决定。 两种投影模式视锥体指的是空间中的一块区域,这块区域决定了摄像机可以看到的空间。视锥体由六个平面包围而成,这些平面也被称为 裁剪平面(clip planes)。视锥体有两种类型,这涉及两种投影类型:一种是 正交投影(orthographic projection),一种是 透视投影(perspective projection)。透视投影: 正交投影: 世界坐标系(World Coordinate System)世界坐标系,我们可以看做是一个永恒不变的参考系,所以的其他模型,和各种坐标系都以这个坐标系作为基准。世界坐标系的位置不随模型的变化而改变。 模型坐标系 (Model Coordinate System)模型坐标系是建模时所用到的坐标系,模型的原点为(0,0,0),但是它可以是世界坐标系中的任意一点。通常在Three.js中,我们设置Object.position.set(0,0,0)就是将模型的原点设置为世界坐标系的原点。 模型中心设置 object.children[i].geometry.center(); //将网格模型的中心移动到世界坐标系的中心 这样做,对于单一Mesh构成的三维模型能够轻松将模型移动到世界坐标系中央,但是对于多Mesh构成的模型来讲,每个children的Mesh都会被移动到World坐标系原点。 如何将加载后处于偏移位置的模型移动到坐标系原点呢?可以看到上图中的模型,虽然模型坐标系和世界坐标系重合,但是模型的原点却位于三维物体的一个角落,看起来不美观。我们起初的办法是通过设置object.children[i].geometry.center()将三维物体的中心移动到模型坐标系原点,但是刚好上图中的模型是单个Mesh构成的三维物体,所以没有发现问题,当使用多Mesh三维模型的时候,上面的方法就会把所有的Mesh移动的模型坐标系中心,使得模型的Mesh混乱,无法显示原来的样子。 为了修复这个问题,想到的解决办法是:计算三维模型AABB包围盒两个对角点的位置坐标相应方向和的均值,得到的均值后,将模型的position设置到这个点,从而达到将三维物体中心放置到坐标系原点的目的。 加载三维模型 12345678910111213objLoader = new THREE.OBJLoader();objLoader.setPath('./obj/');objLoader.load('a.obj', function (object) {oneObj = object;object.traverse(function (child) { if (child.type === \"Mesh\") { child.geometry.computeBoundingBox(); child.geometry.verticesNeedUpdate = true; child.material.side = THREE.DoubleSide; //child.geometry.center(); //设置模型中心点为几何体的中心 }});}); 构造AABB包围盒 1box = new THREE.BoxHelper(object); 计算包围盒任意两个对角点的均值,并移动模型 12345var points = box.geometry.attributes.position.array;obj_x = (points[0]+points[18])/2;obj_y = (points[1]+points[19])/2;obj_z = (points[2]+points[20])/2;oneObj.position.set(-obj_x,-obj_y,-obj_z); //这是移动模型时要反向移动 这样,就解决了模型便宜的问题,但是模型的大小适配问题还在,我们的思路是:获取到模型表面的距离最远的两个点,然后保证这个距离是小于camera的far-near的,同时这个长度大于far-near x 倍,就相应缩小 n*x倍,n是个放大系数,这里我取值是20。实现过程如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354 // 获取三维模型表面点的最大距离 var box3 = new THREE.Box3(); box3.setFromObject(object); var maxLength = box3.getSize(new THREE.Vector3()).length(); var consult = (camera.far - camera.near) / (20*maxLength); //写入场景内 var currentScale = consult; object.scale.set(currentScale,currentScale,currentScale);``` ## 实现过程完整实现过程:```javascript // objLoader objLoader = new THREE.OBJLoader(); objLoader.setPath('./obj/'); objLoader.load('a.obj', function (object) { oneObj = object; object.traverse(function (child) { if (child.type === \"Mesh\") { child.geometry.computeBoundingBox(); child.geometry.verticesNeedUpdate = true; child.material.side = THREE.DoubleSide; //child.geometry.center(); //设置模型中心点为几何体的中心 } }); object.name = \"zxj\"; //设置模型的名称 //对模型的大小进行调整 // object.scale.x =0.001; // object.scale.y =0.001; // object.scale.z =0.001; //object.lookAt(new THREE.Vector3(0,0,0)); // 加入模型的aabb包围盒 // 获取三维模型表面点的最大距离 var box3 = new THREE.Box3(); box3.setFromObject(object); var maxLength = box3.getSize(new THREE.Vector3()).length(); var consult = (camera.far - camera.near) / (20*maxLength); //写入场景内 var currentScale = consult; object.scale.set(currentScale,currentScale,currentScale); box = new THREE.BoxHelper(object); // box.material.transparents = true; // box.material.depthTest = false; // box.visible = true; var points = box.geometry.attributes.position.array; obj_x = (points[0]+points[18])/2; obj_y = (points[1]+points[19])/2; obj_z = (points[2]+points[20])/2; oneObj.position.set(-obj_x,-obj_y,-obj_z); //scene.add(box); scene.add(object); }); 裁剪空间(clipspace) 顶点接下来要从观察空间转换到裁剪空间(clip space,也被称为齐次裁剪空间)中,这个用于转换的矩阵叫做裁剪矩阵(clip matrix),也被称为投影矩阵(projection matrix)。如上面的图所示,裁剪空间是由6个面构成的棱台,它就是webGL中的视锥体,对于这个视锥而言,近平面和远平面分别是近裁剪平面(near clip plane)和远裁剪平面(far clip plane)。裁剪空间的作用就是对存在于视锥体内部的顶点进行渲染,如果不在视锥体内,就裁减掉,不进行渲染。 由此,我们引入一个新的概念:投影矩阵 试想一下,对于这个视锥体而言,如果我们想判断一个点是否在视锥体内部是比较麻烦的,因为视锥体的边界计算就是个复杂的活,那么投影矩阵就是这么一种工具,能够方便的对三维模型的点进行映射,通过结果判断顶点是否位于模型内部。 投影矩阵有两个目的: 首先是为投影做准备。这是个迷惑点,虽然投影矩阵的名称包含了投影二字,但是它并没有进行真正的投影工作,而是在为投影做准备。真正的投影发生在后面的 齐次除法(homogeneous division) 过程中。而经过投影矩阵的变换后,顶点的 w 分量将会具有特殊的意义。 其次是对x、y、z分量行进缩放。我们上面讲过直接使用视锥体的6个裁剪平面来进行裁剪会比较麻烦。而经过投影矩阵的缩放后,我们可以直接使用w分量作为一个范围值,如果x、y、z分量都位于这个范围内,就说明该顶点位于裁剪空间内。 在裁剪空间之前,虽然我们使用了齐次坐标来表示点和矢量,但它们的第四个分量都是固定的:点的w分量是1,方向矢量的w分量是0。经过投影矩阵的变换后,我们就会赋予齐次坐标的第4个坐标更加丰富的含义。下面,我们来看一下透视投影使用的投影矩阵具体是什么。 透视投影视锥体的意义在于定义了场景中的一块三维空间。所有位于这块空间内的物体将会被渲染,否则就会被剔除或裁剪。我们已经知道,这块区域由6个裁剪平面定义,那么这6个裁剪平面又是怎么决定的呢?它们由Camera组件中的参数和Game视图的横纵比共同决定。我们可以通过Camera组件的Field of View(简称FOV)属性来改变视锥体竖直方向的张开角度,而Clipping Planes中的Near和Far参数可以控制视锥体的近裁剪平面和远裁剪平面距离摄像机的远近。这样,我们可以求出视锥体近裁剪平面和远裁剪平面的高度,也就是: $nearClipPaneHeight = 2Neartan(FOV/2)$$farClipPlaneHeight = 2Fartan(FOV/2)$ 现在我们还缺乏横向的信息。这可以通过摄像机的横纵比得到。假设,当前摄像机的横纵比为Aspect,我们定义: $Aspect = nearClipPlaneWidth/nearClipPlaneHeight$$Aspect = farClipPlaneWidth/farClipPlaneHeight$ 现在,我们可以根据已知的Near、Far、FOV和Aspect的值来确定透视投影的投影矩阵。如下:投影矩阵需要注意的是,这里的投影矩阵是建立在WebGL,Unity等对坐标系的假定上面的,也就是说,我们针对的是观察空间为 右手坐标系,使用列矩阵在矩阵右侧进行相乘,且变换后z分量范围将在[-w, w]之间的情况。而在类似DirectX这样的图形接口中,它们希望变换后z分量范围将在[0, w]之间,因此就需要对上面的透视矩阵进行一些更改。而一个顶点和上述投影矩阵相乘后,可以由观察空间变换到裁剪空间中,结果如下: 从结果可以看出,这个投影矩阵本质就是对x、y和z分量进行了不同程度的缩放(当然,z分量还做了一个平移),缩放的目的是为了方便裁剪。我们可以注意到,此时顶点的w分量不再是1,而是原先z分量的取反结果。现在,我们就可以按如下不等式来判断一个变换后的顶点是否位于视锥体内。如果一个顶点在视锥体内,那么它变换后的坐标必须满足: -w","categories":[{"name":"图形学","slug":"图形学","permalink":"https://stonema.github.io/categories/图形学/"}],"tags":[{"name":"前端","slug":"前端","permalink":"https://stonema.github.io/tags/前端/"},{"name":"Three.js","slug":"Three-js","permalink":"https://stonema.github.io/tags/Three-js/"},{"name":"webgl","slug":"webgl","permalink":"https://stonema.github.io/tags/webgl/"}]},{"title":"考试院.net项目技术总结","slug":"考试院-net项目技术总结","date":"2018-07-10T01:29:08.000Z","updated":"2020-01-18T01:45:05.210Z","comments":true,"path":"2018/07/10/考试院-net项目技术总结/","link":"","permalink":"https://stonema.github.io/2018/07/10/考试院-net项目技术总结/","excerpt":"","text":"开发考试院笔迹鉴别系统的过程中遇到了很多的问题,也学到了很多,将其记录下来。以供今后回顾。Warning:项目构建和项目打开的时候一定要使用解决方法,不要使用网站!一定要使用解决方法,不要使用网站!一定要使用解决方法,不要使用网站!重要的事情说三遍!!!网站项目是给静态web项目用的。使用解决方法打开项目,才能每次生成项目的时候把dll更新到最新的状态。 数据库连接方式这个最好实时跟着Oracle的官方文档走,我使用的是Oracle.ManagerdDataAccess,把它下载后加入到项目的引用。Web.config文件的配置如下:123<!-- 本地 --><!--<add name=\"Oracle_DB\" connectionString=\"DATA SOURCE=127.0.0.1/orcl;PASSWORD=bjshadmin;USER ID=ZK\" providerName=\"Oracle.ManagedDataAccess.Client\"/>--><!-- 远程 --> 配置完成后,我们在后台程序中就可以用下面的方式调用数据库连接和方法:1234567891011121314151617181920//创建连接字符串String connctionString = ConfigurationManager.ConnectionStrings[\"Oracle_DB\"].ToString();OracleConnection conn = new OracleConnection(connctionString); //创建连接OracleCommand cmd = conn.CreateCommand();// 创建数据库控制器cmd.CommandText = \"SELECT * FROM table\";//设置commandTextconn.Open();//开启连接/****************查询语句的执行方法 begin******************/OracleDataReader reader = cmd.ExecuteReader(); //至此查询结果接收到了reader对象中//读取reader对象中的值。while (reader.Read()){ StuInfo stu = new StuInfo(); stu.Ks_zkz = reader[0].ToString(); stu.Ks_xm = reader[1].ToString(); stuList.Add(stu);} /*************** 执行Update语句的方法 ******************/ int rows = cmd.ExecuteNonQuery(); //返回受影响条数,删除与增加语句类似conn.close();//最后不要忘了close连接 aspx页面中处理JS的Ajax请求aspx页面中处理JS的Ajax请求时可以使用ashx, 右键->添加->一般处理程序,一般处理程序可以充当服务器端处理Ajax请求的文件: JS:1234567891011121314151617181920212223//通过审核的处理function passStu_next() { var zkzh = $('.kszkz').val(); alert(\"标记学号:\" + zkzh + \"已通过审核\"); $.ajax({ type: \"post\", url: \"passAndNextHandler.ashx\", data: { \"kszkz\": zkzh }, success: function (result) { //跳转到下一个考生的详细内容页面 if (result == \"\") { alert(\"审核完毕\"); } else { window.location.href = \"DetailCardsInfo.aspx?zkzh=\" + result; } }, error: function (result) { alert(\"error\"); } })} ashx程序:1234567891011121314151617181920212223242526272829303132333435363738394041public class passAndNextHandler : IHttpHandler, IRequiresSessionState{ //创建连接字符串 String connctionString = ConfigurationManager.ConnectionStrings[\"Oracle_DB\"].ToString(); public void ProcessRequest(HttpContext context) { string czr = HttpContext.Current.Session[\"UserName\"].ToString(); String nextZkz = null; context.Response.ContentType = \"text/plain\"; String zkzh = context.Request.Form[\"kszkz\"]; String sql = \"UPDATE ZK.V_BYSQ_BJSH_SKB_KS SET BJSH_JG_SKB = '1',BJSH_SKB_CZR='\" + czr + \"' WHERE KS_ZKZ= '\" + zkzh + \"'\"; OracleConnection conn = new OracleConnection(connctionString); OracleCommand cmd = conn.CreateCommand(); conn.Open(); //向表中插入数据的同时,选择下一个考生 OracleTransaction transaction = conn.BeginTransaction(IsolationLevel.ReadCommitted); cmd.Transaction = transaction; //开始事务 try { cmd.CommandText = sql1; cmd.ExecuteNonQuery(); //提交事务 transaction.Commit(); //这里如果是多条语句,同时提交成功才返回 } catch (Exception ex) { transaction.Rollback(); //未成功就rollback Console.Write(ex.ToString()); } context.Response.Write(nextZkz); conn.Close(); } public bool IsReusable { get { return false; } }} 访问网络路径中的文件,以及下载文件到本地。 在本地磁盘创建存储路径: 12345String directoryPath = @\"D:\\Downloadimgs\\\"+list_zkz[i]+\"//\"; //定义一个路径变量if (!Directory.Exists(directoryPath))//如果路径不存在{ Directory.CreateDirectory(directoryPath);//创建一个路径的文件夹} 网络路径的映射以及文件拷贝 12345678910111213141516171819202122232425//设置源目标路径string sourceFile = @\"http://127.0.0.1/img//\" + reader[0] + \"//\" + reader[1].ToString().Trim() + \"//\" + reader[3].ToString().Trim() + \".jpg\";//设置目的地路径string destinationFile = @\"D:\\Downloadimgs\\\" + list_zkz[i] + \"//\" + reader[2] + \"_\" + reader[1].ToString().Trim() + \"_\" + reader[0] + \".jpg\";//FileInfo file = new FileInfo(sourceFile);//下面的这种文件拷贝方式,只能用于本地磁盘的文件拷贝,不能用于网络dowmload//if (file.Exists)//{// // true is overwrite// file.CopyTo(destinationFile, true);//}WebClient wc = new WebClient(); //创建网络通信实例byte[] by = new byte[1024]; //接收数据的数组WriteTextLog(\"beforeCheck:\",sourceFile.ToString()+\"|\"+destinationFile, DateTime.Now);WriteTextLog(\"check:\", UrlCheck(sourceFile).ToString(), DateTime.Now);if (UrlCheck(sourceFile)) // 这里的Urlcheck是必须的{ WriteTextLog(\"savefile:\", sourceFile.ToString() +\"|\"+ destinationFile, DateTime.Now); FileStream fs = new FileStream(destinationFile, FileMode.Create, FileAccess.Write); //创建文件 BinaryWriter bw = new BinaryWriter(fs); by = wc.DownloadData(sourceFile); bw.Write(by, 0, by.Length); bw.Close(); fs.Close(); //关闭数据流} URL的Check需要判断上面的URL是否是网络地址 123456789101112131415161718192021222324252627/*** 如果路径中含有http或者https,就进行webrequest的处理**/public bool UrlCheck(string strUrl) { if (!strUrl.Contains(\"http://\") && !strUrl.Contains(\"https://\")) { strUrl = \"http://\" + strUrl; } try { HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(strUrl); myRequest.Method = \"HEAD\"; myRequest.Timeout = 20000; //超时时间20秒 HttpWebResponse res = (HttpWebResponse)myRequest.GetResponse(); Boolean te; if (res.StatusCode == HttpStatusCode.OK) te = true; else te = false; res.Close(); return te; } catch { return false; } } 上面的日志文件会在项目目录下生成日志文件 123456789101112131415161718192021222324public static void WriteTextLog(string action, string strMessage, DateTime time){ string path = AppDomain.CurrentDomain.BaseDirectory + @\"System\\Log\\\"; if (!Directory.Exists(path)) Directory.CreateDirectory(path); string fileFullPath = path + time.ToString(\"yyyy-MM-dd\") + \".System.txt\"; StringBuilder str = new StringBuilder(); str.Append(\"Time: \" + time.ToString() + \"\\r\\n\"); str.Append(\"Action: \" + action + \"\\r\\n\"); str.Append(\"Message: \" + strMessage + \"\\r\\n\"); str.Append(\"-----------------------------------------------------------\\r\\n\\r\\n\"); StreamWriter sw; if (!File.Exists(fileFullPath)) { sw = File.CreateText(fileFullPath); } else { sw = File.AppendText(fileFullPath); } sw.WriteLine(str.ToString()); sw.Close();}","categories":[{"name":"开发笔记","slug":"开发笔记","permalink":"https://stonema.github.io/categories/开发笔记/"}],"tags":[{"name":".net技术","slug":"net技术","permalink":"https://stonema.github.io/tags/net技术/"},{"name":"web开发","slug":"web开发","permalink":"https://stonema.github.io/tags/web开发/"}]},{"title":"日记","slug":"日记","date":"2018-07-09T10:00:09.000Z","updated":"2020-01-18T01:45:05.206Z","comments":true,"path":"2018/07/09/日记/","link":"","permalink":"https://stonema.github.io/2018/07/09/日记/","excerpt":"","text":"搞不明白这些研究生导师都是在干什么…… 为中国的教育堪忧!","categories":[{"name":"生活","slug":"生活","permalink":"https://stonema.github.io/categories/生活/"}],"tags":[{"name":"日记","slug":"日记","permalink":"https://stonema.github.io/tags/日记/"}]},{"title":"OpenCV图像处理总结","slug":"OpenCV图像处理总结","date":"2018-07-05T02:35:19.000Z","updated":"2020-01-18T01:45:05.171Z","comments":true,"path":"2018/07/05/OpenCV图像处理总结/","link":"","permalink":"https://stonema.github.io/2018/07/05/OpenCV图像处理总结/","excerpt":"","text":"OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。OpenCV用C++语言编写,主要用于图像处理。 基本方法包括但不限于: 图像灰度化 图像二值化 图像降噪,增强 图像特征提取 全局特征与局部特征 一些图像方面的基本知识:图像增强(image enhancement)图像增强可以分为两大类: 频率域和空间域(频域和空域)。 前者把图像看成一种二维信号,对其进行基于二维傅里叶变换的信号增强。采用低通滤波(即只让低频信号通过)法,可去掉图中的噪声;采用高通滤波法,则可增强边缘等高频信号,使模糊的图片变得清晰。 后者空间域法中具有代表性的算法有局部求平均值法和中值滤波(取局部邻域中的中间像素值)法等,它们可用于去除或减弱噪声。 灰度化什么是灰度化?从字面意思来看就是将彩色图片变成灰色的。1:那么如何实现的呢?2:以及为什么要这样做呢?首先我们知道常见的图像都是彩色图像,色彩的展示靠的是计算机计算出每个像素点的色值,比如一个像素位上的颜色是黄色,那么对于RGB三通道的图像来讲,就是R通道,G通道,B通道各有一个色值,然后将三者通过计算之后叠加,得到这个黄色的色值。当然我们上述说的都是RGB空间中的颜色处理,对于计算机上显示的彩色图像还有很多的颜色空间,比如HSV,和HLS等等。彩色图像中的每个像素的颜色有R、G、B三个分量决定,每个分量占一个字节,也就是8个bit,所以每个分量有$2^{8}=255$个颜色等级可以表示。所以一个24深度的RGB图像,每个像素点就有1600多万中色彩可以表示。如果我们想将其处理成灰色呢? 第一个问题,如何实现?在RGB模型中,如果R=G=B时,则彩色表示一种灰度颜色,其中R=G=B的值叫灰度值,因此,灰度图像每个像素只需一个字节存放灰度值(又称强度值、亮度值),灰度范围为0-255。也就是说这时候图像的RGB三个通道的值都是相同的。对于图像的灰度化,有如下几种处理方法: 分量法将彩色图像中的三分量的亮度作为三个灰度图像的灰度值,可根据应用需要选取一种灰度图像。 f_{1}(i,j)=R(i,j) \\\\ f_{2}(i,j)=G(i,j) \\\\ f_{3}(i,j)=B(i,j) \\\\其中$f_{k}(i,j)(k=1,2,3)$为转换后的灰度图像在图像坐标为(i,j)处的灰度值。 最大值法将彩色图像中的三分量亮度的最大值作为灰度图的灰度值。 f(i,j)=max(R(i,j),G(i,j),B(i,j)) 平均值法将彩色图像中的三分量亮度求平均得到一个灰度值。 f(i,j)=(R(i,j)+G(i,j)+B(i,j)) /3 加权平均法根据重要性及其它指标,将三个分量以不同的权值进行加权平均。由于人眼对绿色的敏感最高,对蓝色敏感最低,因此,按下式对RGB三分量进行加权平均能得到较合理的灰度图像。 f(i,j)=0.30R(i,j)+0.59G(i,j)+0.11B(i,j)) Opencv中图像灰度化处理:在Opencv中可以通过以上几种方法的数值计算来得到灰度图像也可以通过opencv提供的颜色空间转换函数来得到。Opencv封装灰度法:12//彩色图转为灰度图方法调用cv::cvtColor(rgbMat, greyMat, CV_BGR2GRAY); 第二个问题,为什么要这样灰度化?灰度图像不会消除图像的边缘信息,纹理信息,梯度信息等,所以使用灰度化后的图片可以完美保留这些有用的信息,同时还能节省图片的存储空间,(因为RGB压缩为一个通道来表示)所以这样的优的显而易见。还有就是梯度信息对于识别物体来说很重要,所以我们可以把灰度图像看作图像的强度(Intensity),来求一些梯度特征,比较常用的有 HOG,LBP,SIFT等等。 像素点之间的关系 像素梯度图像梯度可以把图像看成二维离散函数,图像梯度其实就是这个二维离散函数的求导:图像梯度: G(x,y) = dx(i,j)+dy(i,j) \\\\ dx(i,j) = I(i+1,j) - I(i,j) \\\\ dy(i,j) = I(i,j+1) - I(i,j) \\\\其中,I是图像像素的值(如:RGB值),(i,j)为像素的坐标。图像梯度一般也可以用中值差分: dx(i,j) = \\frac{I(i+1,j)-I(i-1,j)}{2}; \\\\ dy(i,j) = \\frac{I(i,j+1)-I(i,j-1)}{2};图像边缘一般都是通过对图像进行梯度运算来实现的。","categories":[{"name":"图形学","slug":"图形学","permalink":"https://stonema.github.io/categories/图形学/"}],"tags":[{"name":"特征提取","slug":"特征提取","permalink":"https://stonema.github.io/tags/特征提取/"},{"name":"OpenCV","slug":"OpenCV","permalink":"https://stonema.github.io/tags/OpenCV/"},{"name":"灰度化","slug":"灰度化","permalink":"https://stonema.github.io/tags/灰度化/"},{"name":"全局特征","slug":"全局特征","permalink":"https://stonema.github.io/tags/全局特征/"},{"name":"局部特征","slug":"局部特征","permalink":"https://stonema.github.io/tags/局部特征/"}]},{"title":"BP神经网络","slug":"BP神经网络","date":"2018-06-06T02:07:22.000Z","updated":"2020-01-18T01:45:05.152Z","comments":true,"path":"2018/06/06/BP神经网络/","link":"","permalink":"https://stonema.github.io/2018/06/06/BP神经网络/","excerpt":"","text":"感谢师姐,感谢博主:Charlotte77,感谢魏老师,感谢龙哥等等同学与前辈在学习的道路上为我指点迷津。 反向传播算法(Back Propagation)分二步进行,即正向传播和反向传播。这两个过程简述如下: 正向传播输入的样本从输入层经过隐单元一层一层进行处理,传向输出层;在逐层处理的过程中。在输出层把当前输出和期望输出进行比较,如果现行输出不等于期望输出,则进入反向传播过程。 反向传播反向传播时,把误差信号按原来正向传播的通路反向传回,逐层修改连接权值,以使得代价函数趋向最小。 详细过程介绍:首先要明白BP神经网络是神经网络的权值更新过程运用了BP算法而得名的。神经网络结构有输入层,隐藏层(可能包含多层),输出层组成;在神经网络中每一个节点的都与上一层的所有节点相连,称为全连接。神经网络的上一层输出的数据是下一层的输入数据。在图中的神经网络中,原始的输入数据,通过第一层隐含层的计算得出的输出数据,会传到第二层隐含层。而第二层的输出,又会作为输出层的输入数据。需要注意的是,图中的没个节点都可以分为两个部分:比如 $f{1}(e)$ 它的前半部分可以表示的是,$x{1}$ 和 $x_{2}$共同作用后的结果,然后它的后半部分这个结果经过激活函数的映射后得到的值。 参考http://www.cnblogs.com/charlotte77/p/5629865.html","categories":[{"name":"神经网络","slug":"神经网络","permalink":"https://stonema.github.io/categories/神经网络/"}],"tags":[{"name":"神经网络","slug":"神经网络","permalink":"https://stonema.github.io/tags/神经网络/"},{"name":"BP算法","slug":"BP算法","permalink":"https://stonema.github.io/tags/BP算法/"}]},{"title":"认识卷积神经网络CNN","slug":"认识卷积神经网络","date":"2018-06-04T06:53:29.000Z","updated":"2020-01-18T01:45:05.211Z","comments":true,"path":"2018/06/04/认识卷积神经网络/","link":"","permalink":"https://stonema.github.io/2018/06/04/认识卷积神经网络/","excerpt":"","text":"卷积神经网络卷积神经网络,是一种前馈神经网络,人工神经元可以响应周围单元,可以进行大型图像处理。卷积神经网络包括卷积层和池化层。 首先来看上一篇文章中的图,这个图就是用来描述CNN 从入坑开始 —— CNN或许这并不是你接触到的第一个深度学习领域、或者是机器学习领域的专业名词,但是搞明白它,足以让我们学习到很多知识以供后续的学习和进步。CNN(Convolutional Neural Networks)也就是我们常说的卷积神经网络,单单从这个名词来看,这里就有几个概念需要我们来弄明白——什么是卷积,什么是神经网络。 卷积最早接触这个词的时候是读大学的时候学习《数字信号处理》这门课程,里面在FFT快速傅里叶变化的时候有讲到卷积运算,$\\bigotimes$ 表示卷积运算,那么: f(x) \\bigotimes g(x) = \\int_{-\\infty}^{+\\infty} f(\\tau)g(x-\\tau)d\\tau\\tag{$1$}离散化的卷积运算: y(n) = \\sum_{i=-\\infty}^{+\\infty}x(i)h(n-i)=x(n)*h(n)\\tag{$2$}但是!!!这里神经网络中的卷积运算,并非傅里叶变换中的卷积运算,这里的卷积运算是以图像和卷积核(也叫滤波器)对应位置相乘再相加得到的结果:如下图所示:图中的黄色区域的标注数字为卷积核,如下所示: \\left[ \\begin{matrix} 1 & 0 & 1 \\\\ 0 & 1 & 0 \\\\ 1 & 0 & 1 \\end{matrix} \\right]\\tag{$3$}我们可以看到,右边得到的结果是左边大矩阵和小矩阵卷积得到的,大矩阵就是我们的原图像,里面的值就是图像像素的值,而小矩阵就是卷积核。两者对应位置相乘然后依次相加,得到新矩阵的一个位置的值,然后向右滑动卷积核(滑动的间隔可调)得到新的位置的值。这就是神经网络中的卷积。这里要注意的是,我们从上图中可以看到,卷积核在移动的过程中是一个一个像素滑动的,这个滑动的间隔叫做步长(strides),strides的值可以自己设置,表示滑动的间隔的大小。通过这样的操作,我们可以直观的看到,原图像尺寸被卷积操作缩小了。这就是卷积操作的作用,可以通过卷积核把一个小区间内的特征提取出来,并以数值化的形式表现出来。如果你要深挖为什么要用这样的计算形式来处理卷积核与图形,建议看图像滤波方面的内容。 那么到这里,我们初步具备了学习卷积神经网络的基础知识。Next,我们开始了解卷积神经网络中的各层,以及它们的工作内容。 卷积神经网络中的分层卷积神经网络,是一种前馈神经网络,卷积神经网络包括卷积层和池化层。 输入层(Input Layer)输入层:该层要做的处理主要是对原始图像数据进行预处理,其中包括: 去均值:把输入数据各个维度都中心化为0,如下图所示,其目的就是把样本的中心拉回到坐标系原点上。 归一化:幅度归一化到同样的范围,如下所示,即减少各维度数据取值范围的差异而带来的干扰,比如,我们有两个维度的特征A和B,A范围是0到10,而B范围是0到10000,如果直接使用这两个特征是有问题的,好的做法就是归一化,即A和B的数据都变为0到1的范围。 PCA/白化:用PCA降维;白化是对数据各个特征轴上的幅度归一化 卷积层(Convention Layer)卷积神经网络(CNN)第一次提出是在1997年,杨乐春(LeNet)大神的一篇关于数字OCR识别的论文,在2012年的ImageNet竞赛中CNN网络成功击败其它非DNN模型算法,从此获得学术界的关注与工业界的兴趣。从本质上来说,图像卷积都是离散卷积,离散卷积本质上是线性变换、(这也是为什么要引入激活函数)上面已经介绍了离散卷积运算的形式,卷积层也是卷积神经网络中最重要的一个层:卷积在上文,以及之前的文章中我们介绍过它的运算方式,实际卷积的过程或者说卷积运算的过程,就是在提取原矩阵中的特征,(或者理解为对原矩阵做滤波)所以每次卷积操作完成后,得到的都是一个featuremap下面的图片详细解释了卷积的过程:上面这个图中有几个重要的名词,我们需要了解: 深度/depth(就是每次卷积能得到多少个featuremap) 步长/stride (窗口一次滑动的长度) 填充值/zero-padding(边缘填充0)VALID(边缘不填充)上面就是单层卷积操作的全部过程了,对于多层卷积我们以一个例子来说明:比如上图中输入为5x5x2,卷积核为3个3x3的filter,填充方式为VALID,那么这幅图从下往上看,我们第一步分别用三个卷积核对原图像做卷积得到第二层的3个3x3x2的结果,然后对于每一个3x3x2的结果,两层featuremap的对应位置相加,就得到一张3x3的featuremap,同样其他卷积核得到的结果相同,这样就一共得到了三张featuremap,所以说最后输出的featuremap数量与卷积核的数量相同。 池化层(Sampling Layer,Pooling Layer)不知道为什么使用‘池化’这个词,实际这个过程更符合“采样”,我的猜测是“采样”更多的体现的是从已有样本中选择出代表,而池化是使用某些运算后的结果作为代表,并不一定是在原样本中采样。在CNN网络中卷积池之后会跟上一个池化层,池化层的作用是提取局部均值与最大值,根据计算出来的值不一样就分为均值池化层与最大值池化层,一般常见的多为最大值池化层。池化的时候同样需要提供filter的大小、步长。 全连接(Full-Connection Layer)上图的最后两层小圆圈就是全连接层,全连接层的作用就是将前面的二维数据输出成一维数据!啊~终于快结束了~学到这里脑子已经感觉有些木了。别急马上结束~看到上面这个图,我的第一反应是,它是如何把20x12x12变成100x1x1的?这里我们套用网上的一个例子:这个过程可以理解为在中间做了一个卷积,但是这里的卷积核与前面的输出层图像维度相同,都是5层,每层的滤波器都是相同的,但是我们有4096个卷积核。从上图我们可以看出,我们用一个3x3x5的filter 去卷积激活函数的输出,得到的结果就是一个fully connected layer 的一个神经元的输出,这个输出就是一个值因为我们有4096个神经元我们实际就是用一个3x3x5x4096的卷积层去卷积激活函数的输出。“这一步卷积一个非常重要的作用就是把分布式特征representation映射到样本标记空间”—知乎:蒋竺波例子:目标是检查图片中有没有猫,而不关心猫的位置。 到这里我们明白了一点:卷积层就是在提取特征,全连接层就是在分类,全连接层中的没个数值就是判断前面的卷积层有没有提取到目标的某个特征的依据。比如全连接层输出为[1,0,1,1];则表示,除了第二个特征,其余的特征都检测到了。红色的神经元表示特征被找到。(激活了)最终的输出形式:那么全连接层对模型影响参数就是三个: 全接解层的总层数(长度)单个全连接层的神经元数(宽度)激活函数 输出层(Output Layer)…… 有了上面对卷积神经网络的了解,我们要继续了解一下其他内容,下面的前馈网络和BP网络都是对网络中权值更新的算法。 前馈神经网络现在讲的都是前馈网络,后来人们发现权值更新使用了BP思想以后,才有了BP神经网络这个名词。 BP神经网络首先不要认为BP网络与卷积网络是并列关系,两者专注的领域不同,BP网络的目的是使用反向传递的思想来更新权值,而卷积网络的目的是降低网络中神经元的连接数量从而减少权值的计算。 其他卷积神经网络 LeNet,这是最早用于数字识别的CNN AlexNet, 2012 ILSVRC比赛远超第2名的CNN,比 LeNet更深,用多层小卷积层叠加替换单大卷积层。 ZF Net, 2013 ILSVRC比赛冠军 GoogLeNet, 2014 ILSVRC比赛冠军 VGGNet, 2014 ILSVRC比赛中的模型,图像识别略差于GoogLeNet,但是在很多图像转化学习问题(比如object detection)上效果奇好 深度学习卷积神经网络的常用框架 Caffe 源于Berkeley的主流CV工具包,支持C++,python,matlab Model Zoo中有大量预训练好的模型供使用 Torch Facebook用的卷积神经网络工具包 通过时域卷积的本地接口,使用非常直观 定义新网络层简单 TensorFlow Google的深度学习框架 TensorBoard可视化很方便 数据和模型并行化好,速度快 参考卷积神经网络全连接层的含义","categories":[{"name":"神经网络","slug":"神经网络","permalink":"https://stonema.github.io/categories/神经网络/"}],"tags":[{"name":"机器学习","slug":"机器学习","permalink":"https://stonema.github.io/tags/机器学习/"},{"name":"感知器","slug":"感知器","permalink":"https://stonema.github.io/tags/感知器/"},{"name":"卷积神经网络","slug":"卷积神经网络","permalink":"https://stonema.github.io/tags/卷积神经网络/"}]},{"title":"认识人工神经网络","slug":"从ANN到CNN","date":"2018-05-29T05:09:53.000Z","updated":"2020-01-18T01:45:05.188Z","comments":true,"path":"2018/05/29/从ANN到CNN/","link":"","permalink":"https://stonema.github.io/2018/05/29/从ANN到CNN/","excerpt":"","text":"写在前面: 或许机器学习这部分内容不应该就这么展开,应该循序渐进,待自己有了深层次了解以后,从基本的回归、分类讲起,但是最后还是选择了边学习边总结的形式,至少能详细记录自己曾经踩过的坑,记录自己的学习路线,也算是个好的形式吧! 周末跟北京的兄弟们聚了一波,每次见他们的感觉都特别的不一样,每次见完面后都感觉自己受了很大鼓舞,有无限动力。W哥好像一直都是这么的有激情有干劲儿,YL好像一直这么的能钻研从来不觉得累,而且他们的努力程度远在我认识的其他人之上!一起加油~ 引言我相信很多人跟我一样,接触深度学习的最初原因是它很火听起来很厉害,而对于具体是如何实现的,自己根本闹不清……看一些论文、博客的时候都是看到一些类似下图的这样的图片,然后告诉自己:嗯~我大概了解深度学习的概念了。今天我们正儿八经的把它的皮扒开,认真分析它的经脉连通,深入了解什么是神经网络!什么是卷积神经网络,什么是BP神经网络,又是如何延伸到深度学习的! 本文我们从神经元讲解到感知器,再到神经网络,带你认识最基本的神经元,然后了解感知器,神经网络是如何工作的!为了保证我的思路不混乱,保证讲解的路线的正确性,我先以自己踩过的坑为带入点,然后依次深入。有关CNN,BP网络,等等我们会在后续文章陆续更新,希望通过几篇文章,理清自己的思路,所机器学习领域的概念有个统筹的认识。 神经网络好~我们初步解决了一个小问题,那么已经开始上道了,继续分析表象——神经网络,这个词~听起来真的高大上有木有!!第一次听到的时候也是上大学的时候,那时候先进一点的同学都在写神经网络算法,学什么蚁群算法,支持向量机,balabala~~一大堆在当时根本听不懂的名词~那,人总是要进步的,当时不愿意学,研究生再不学就有点耍流氓了~这里我们先介绍什么是神经网络。 神经元如果你和我一样,是想从0开始,把所有概念全弄懂搭建自己的知识脉络,而不仅仅是关心应用层的东西,我希望你可以看看这部分,看看我这个愚钝的人是如何坑自己的,如果你早已对这些基本概念了解的熟烂于心,建议跳过。神经元,这个词乍听起来怎么都不能和计算机专业联系在一起,它怎么也得是个生物学相关的东西啊!没错,神经元本就是生物学的概念,第一个人造神经元是1943年由Warren McCulloch和Walter Pitts首次提出的,最早用来作为大脑中“神经网络”的计算模型。如果你不清楚神经元的工作原理,请看下面这个图,神经元有很多个输入端,当输入的信号经过神经元的细胞核处理超过某个阈值后,就把生成的信号向下传递。那么知道了真正的神经元的样子,人工神经元实际上就是模拟的生物学中的神经元,如下图所示,左侧的就是输入,中间圆圈就是处理单元,后面的就是输出:看到这个图,总的来讲我们要有一个概念,这个东西基本表示的是:有多个输入单元,然后将这些输入单元经过某种运算以后得到一个输出。上图这个神经元的输入值是$x{1},x{2},x{3}$以及截距b这里是$+1$,中间是运算单元,输出是$h{W,b}(x)=f(W^{T}x)=f(\\sum^{3}{i=1}W{i}x_{i}+b)$ ,其中函数 $f:\\Re \\longmapsto \\Re$ 被称激活函数。 到这里,其实对于一个非数学或者计算机专业的同学来说已经有点懵了……(其实即使是数学和计算机专业的我,一开始也是懵的……)那么这里这个函数有什么作用呢?我们用一个例子来说明:比如我们有个数据a它的形式是这样的 {$x^{0},1$},现在我们想通过一个函数能够使得$f(x^{0})=1$那么这个函数$f(x)$就是我们要研究的对象,也就是我们的模型!只不过这里是最简单的一维数据模型,那么让我们把原始输入复杂一点,假设输入是多维数据,(可以用向量来表示)那么就成了$(\\boldsymbol x^{(0)},1)$ 也就是说对于这个输入进来的向量,我需要给每个维度的数据配比不同的权重来使得$f(\\boldsymbol x^{(0)}) = 1,$;到这里依然很好理解,但是这里还是在讲单一数据,那么对于数据集呢?假设我们有这样的数据集$((\\boldsymbol x^{i}),\\boldsymbol y^{(i)})$,那我们就无法使用一个神经元来解决问题了,一个不行的话,就多个!那么多个神经元也就组成了我们的神经网络,(多层感知器)。 感知器(Perceptron)多个神经元构成的组合结构可以成为感知机(感知器)。感知器分为单层感知器和多层感知器。感知器之所以叫感知器原因就是它具备某种感知功能,当我们的网络中具有众多感知各种事物的感知器的时候,也就构成了复杂的神经网络。具有了多重感知能力。这也是感知器与神经网络的关系。 单层感知器多层感知器 感知器的学习策略我们需要介绍一下的是,感知机的学习策略,也是我们神经网络中的重点。首先要明白我们为什么要引入这样一个概念。通过了解上面的神经元和感知器得知,他们的目的是通过一个模型(多层感知器),能够使得样本集$((\\boldsymbol x^{i}),\\boldsymbol y^{(i)})$ 实现我们的输入$\\boldsymbol x^{i}$ 都能和应有的输出$\\boldsymbol y^{(i)}$对应上。所以我们的主要目的就是要得到这样的一个模型,对于上面的神经元来说,就是要得到针对各项输入的权重。能够完美匹配我们所有的输入。好的~到这里,我们基本明确了我们的目标。让我们继续去了解如何解决这个问题通常我们肯定会遇到这样的情况:我们给一组权重$(w^{i})$和函数$f(x)$它可能只能满足某些输入信号从$x^{(i)}$到$y^{(i)}$的映射,并不能适配所以的样本集。那么如何根据已经正确适配或是适配错误的输出来调整我们的$w^{(i)}$呢?这就引入了我们要讲的重点:学习策略感知器的学习规则是这样的:学习信号等于神经元期望输出的值与实际的输出值的差。 Result = desired_{j} - output_{j} \\tag{$1$}$desired{j}$表示期望望的输出,$output{j}$表示实际的输出。$Result$就是误差,然后根据学习信号,就可以对权值进行更新,也就是我们所说的 学习 了。看到这里不要慌,不要一见到公式就害怕,这里还是很好理解的。只不过把我们前面语言叙述的内容符号化了。继续,看上面感知器中的图,$W$代表特征向量或者是特征矩阵,$\\boldsymbol x$ 表示输入信号(样本集中的样本)。那么我们把上面的公式重新书写一下: output_{j} = f(W^{T}_{j} \\cdot X) = sgn(W^{T}_{j} \\cdot X) = \\begin{cases} 1,& W^{T}_{j}X\\geqslant0\\\\ -1,& W^{T}_{j}X","categories":[{"name":"人工智能","slug":"人工智能","permalink":"https://stonema.github.io/categories/人工智能/"},{"name":"机器学习","slug":"人工智能/机器学习","permalink":"https://stonema.github.io/categories/人工智能/机器学习/"}],"tags":[{"name":"神经网络","slug":"神经网络","permalink":"https://stonema.github.io/tags/神经网络/"},{"name":"ANN","slug":"ANN","permalink":"https://stonema.github.io/tags/ANN/"},{"name":"神经元","slug":"神经元","permalink":"https://stonema.github.io/tags/神经元/"},{"name":"感知器","slug":"感知器","permalink":"https://stonema.github.io/tags/感知器/"},{"name":"Perceptron","slug":"Perceptron","permalink":"https://stonema.github.io/tags/Perceptron/"}]},{"title":"深入浅出OpenGL","slug":"深入浅出OpenGL","date":"2018-05-21T01:00:36.000Z","updated":"2020-01-18T01:45:05.208Z","comments":true,"path":"2018/05/21/深入浅出OpenGL/","link":"","permalink":"https://stonema.github.io/2018/05/21/深入浅出OpenGL/","excerpt":"","text":"OpenGL概述什么是OpenGL?OpenGL是一种应用程序的编程接口(Application Programming Interface, API),它是一种可以对图形硬件设备特性进行访问的软件库。《OpenGL编程指南》 OpenGL的工作流:OpenGL主要工作就是将二维及三维物体绘制到帧缓存器中 主要步骤: 构造几何要素(点,线,多边形),创建对象的数学描述。在三维空间中放置对象。 计算对象的颜色,颜色可以给出,或由光照条件及纹理间接给出。 光栅化,把对象的数字描述和颜色信息转换为屏幕上的像素。","categories":[{"name":"OpenGL编程","slug":"OpenGL编程","permalink":"https://stonema.github.io/categories/OpenGL编程/"}],"tags":[{"name":"OpenGL","slug":"OpenGL","permalink":"https://stonema.github.io/tags/OpenGL/"},{"name":"图形图像","slug":"图形图像","permalink":"https://stonema.github.io/tags/图形图像/"}]},{"title":"进军OpenCV和OpenGL","slug":"进军OpenCV和OpenGL","date":"2018-05-16T02:54:49.000Z","updated":"2020-01-18T01:45:05.225Z","comments":true,"path":"2018/05/16/进军OpenCV和OpenGL/","link":"","permalink":"https://stonema.github.io/2018/05/16/进军OpenCV和OpenGL/","excerpt":"","text":"近期开始做OpenCV和OpenGL方向的研究与开发工作了,感谢my的书卡,感谢GitHub和机械工业出版社,带我入门,带我编程,能够从一个门外汉进入这个领域。 OpenCV简介OpenCV是一个跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上运行。在没有做这方面工作之前不太明白OpenCV与OpenGL的区别。现在让我来讲的话,大概可以这么理解:OpenCV是一个算法库,它实现了很多图形处理和计算机视觉的通用算法,而OpenGL是一个图形库,它实现了一些基本的计算机图形图像,可以方便的开发二维或者三维计算机应用程序。 1. Windows下配置OpenCV与VS2015集成环境为了配置这个环境,中间遇到了无数的坑,一个一个解决,将这个过程中遇到的问题总结一下,写在这里。在做图像处理的时候,频繁使用到OpenCV中的算法,于是想系统的学习一下这方面的知识,买了一本《OpenCV By Example》照着里面的例子学习…… 1.1 环境:Windows10 x64, VS2015, OpenCV3.3.0, Cmake3.11.1首先下载OpenCV: https://opencv.org/releases.htmlDocumentation是OpenCV的使用手册,source是源码,Win pack是编译后的opencv自解压包,我们这里可以选择下载源码或者下载编译后的自解压包。二者最基本的区别就是:前者需要我们本地编译,后者可以直接使用。我们将两个文件都下载下来: 1.2 方法一:自解压包的安装与配置下载完成后的exe文件直接运行,就是自解压过程选择好目标文件夹(这里注意路径中不要有中文),解压完成后得到如下文件:环境变量: 在windows的环境变量中的Path中添加opencv中的bin路径:例如我的是:C:\\opencv\\build\\x64\\vc14\\binvs配置: 在配置好环境变量后在VS2015中创建一个C++的win32控制台程序,然后开始配置这个项目的属性,首先在视图->其他窗口 中打开当前项目的属性管理器窗口:因为我们前面配置的都是x64的环境,所以这里选择x64的Debug属性文件夹下面需要修改相关配置: VC++目录:将下面的内容依次加入到VC++的包含目录(include path)C:\\opencv\\build\\includeC:\\opencv\\build\\include\\opencvC:\\opencv\\build\\include\\opencv2将下面内容依次加入到VC++库目录(lib path)C:\\opencv\\build\\x64\\vc14\\lib 连接器目录:链接器->输入->附加依赖项将下面的lib加入到其中:opencv_world330d.lib这里注意的是,有些地方可能会用到opencv_ts*.lib但是自解压包内是不包含这个lib的,需要我们使用自己编译的程序。至此,OpenCV的自解压版已经配置完毕,可以直接在工程中调用OpenCV中的函数:123456789101112131415161718192021222324252627282930313233343536#include <iostream>#include <string>#include <sstream>using namespace std;#include \"opencv2/core.hpp\"#include \"opencv2/highgui.hpp\"using namespace cv;int mainOne(){ Mat color = imread(\"D:\\\\WorkSpace\\\\VSworkspace\\\\OpenCV\\\\OpencvBooks\\\\img\\\\lena.jpg\"); //图片必须添加到工程目下 //也就是和test.cpp文件放在一个文件夹下!!! imshow(\"测试程序\", color); Mat gray = imread(\"D:\\\\WorkSpace\\\\VSworkspace\\\\OpenCV\\\\OpencvBooks\\\\img\\\\lena.jpg\",0); //写图像 imwrite(\"D:\\\\WorkSpace\\\\VSworkspace\\\\OpenCV\\\\OpencvBooks\\\\img\\\\lena.jpg\", gray); // 通过opencv函数获取相同像素 int myrow = color.cols - 1; int myCol = color.rows - 1; Vec3b pixel = color.at<Vec3b>(myrow,myCol); cout << \"Pixel value (B,G,R): (\" << (int)pixel[0] << \",\" << (int)pixel[1] << \",\" << (int)pixel[2] << \")\" << endl; cout << color.size << endl; // 初始化矩阵 Mat m = Mat::eye(5, 5, CV_32F); Mat n = Mat::ones(5,5,CV_32F); Mat result = m*n; cout << m << endl; FileStorage fs(\"test.yml\", FileStorage::WRITE); fs << \"result\" << result; fs.release(); imshow(\"Lena BGR\", color); imshow(\"Lena Gray\", gray); waitKey(0); return 0;} 1.3 方法二:本地编译再配置找到前面下载的源码版的opencv,同时下载并安装cmake工具到本机。然后查看这个文件夹,可以看到有cmakelists文件,这个就是cmake需要用的构建工程的文件然后选择source code和要build到的文件夹:点击configure选择本机的 VS2015 Win64,然后Finish,这个过程是生成VS2015的opencv环境,并检验本机的vs环境对于opencv的必要依赖是否完整,这个过程需要一段时间,完成后,选择你需要build的文件,然后再点击Configure开始构建:然后点击Generate开始生成链接库,完成后就会发现build文件夹内已经存在了opencv的工程sln。然后打开这个工程,找到CMakeTargets文件夹内的ALL_BUILD,进行编译:右键->生成,这个过程持续一段时间。编译完成后,再找到CMakeTargets文件夹内的INSTALL工程,再次执行生成,这样整个OpenCV的编译工作就完成了,生成了对应的动态链接库和可执行文件。可以看到在build文件夹内的的install文件夹为编译生成的文件:我们对比一下编译完成的OpenCV 和官方的自解压版本中的内容:可以看到编译版本比自解压版多一些链接库和lib文件。但是同时缺少 opencv_world***.dll 以及 opencv_world***.lib后面的操作就和自解压版相同,如果需要使用编译版本的OpenCV,就需要把环境变量配置成刚刚编译好的内容,同时设置VS中的属性页即可。 PS: 我在前面的执行cmake的过程中,多勾选了BUILD_openc_world 和 WITH_QT 导致在VS中编译OpenCV源码的过程中出现了很多错误:至今不知道如何解决,感觉是本机的QT配置有问题。","categories":[{"name":"计算机图形学","slug":"计算机图形学","permalink":"https://stonema.github.io/categories/计算机图形学/"}],"tags":[{"name":"OpenCV","slug":"OpenCV","permalink":"https://stonema.github.io/tags/OpenCV/"},{"name":"OpenGL","slug":"OpenGL","permalink":"https://stonema.github.io/tags/OpenGL/"}]},{"title":"特征检测技术总结","slug":"特征检测技术总结","date":"2018-04-25T07:36:34.000Z","updated":"2020-01-18T01:45:05.209Z","comments":true,"path":"2018/04/25/特征检测技术总结/","link":"","permalink":"https://stonema.github.io/2018/04/25/特征检测技术总结/","excerpt":"","text":"写在前面对于模式识别,到了研二终于发自内心的体会到它的应用领域已经广阔到一种不可描述的状态,大到人工智能,数据挖掘,,机器学习,小到特征提取,图像处理,都会有模式识别的影子与你相随,这种感觉就像数学领域中的欧拉,拉格朗日,与柯西,爷仨~贯穿你的整个学习过程,如影随形。在读研以前,从没去思考过计算机是如何识别图片的、如何认知文字的、如何回答语音的……直到后来想做三维模型检索相关领域的内容,本以为不会是太难的东西,到后来才发现,这里面的内容足够我消化半辈子了……我这个人大概率还是脑子太笨了,最开始的时候,通过各种手段去了解图像处理,深度学习这方面的知识,很长时间以来,都没有深入理解这些内容的精髓,没有清晰的知识脉络。什么是滤波器,什么是卷积核,什么是池化,什么是采样,什么是激活函数,什么是分类器,sigmoid是干什么的,全连接是干什么的,怎么有的CNN,什么是前馈网络,什么是反向传递。那些乱七八糟的算子有什么用,SIFT,SVM跟这些东西又是什么关系……等等等等一系列的名称一下子涌过来,搞得我迷迷糊糊。通过不断地学习,不断地温习以往的知识,我开始有了一点点清晰一点的知识脉络。也感谢自己养成了写博客的习惯,今后会慢慢地将自己的理解与认知写下来,理清自己的思路与见解,也供他人参考。如果有读者看到一些错误或者有待商榷的地方,希望您能够不吝指出,与我联系,共同进步。 从介绍模式识别开始刚读研一的时候,张老师让我读《模式识别》(《Pattern Recognition》)这本书。而实际在我真正接触图像识别的工程之前,这本书我仅仅看了第一章,仅仅看了一些概念性的东西,而后面的算法讲解,贝叶斯决策,分类器,支持向量机,多层感知机等等,都因为内容中的公式太多,直接就怕了……感觉这本书会非常的难啃。而后来,在我拜读OpenCV中的关于SVM的算法讲解的时候,有一些地方弄不明白,魏老师让我去看模式识别的分类器这部分,我才茅塞顿开。慢慢也开始进入了良性循环的节奏,看一些公式也不闹心了,也知道一篇论文的重点该看哪里了。非常的nice。 PS:假如我不干编程工作了,大概率可以当个讲师吧!哈哈哈,因为遇到的大部分老师讲一些理论的时候讲的是真的烂啊!!不怪学生们不爱听~自己真正弄明白一些理论之后,真的觉得自己的吹毛求疵,刨根问底都是值得的!研究生阶段的学习,不能是单纯的调用函数接口,还是要深入的了解这个函数。社会的精英阶层,领导阶层什么时候才能真正意识到我们国家的教育问题呢?富人家的孩子暂且可以摆脱经济压力,去国外镀金,花更多的时间投资在个人能力的提升上,但是那些群穷人家的孩子怎么办呢?这大概就是差距慢慢拉大的原因吧~高等教育的改革势在必行,如果能够因材施教,学生找得到自己的兴趣方向,使得学习的驱动力来自于自身,而不是强行往某个领域推。。。我想这样会从上往下传导出一种新的活力吧!估计也不会有这么多人的人生目标是逃离高考大省了吧! 模式识别简介传统的模式识别包括且不仅限于一下内容: 语音识别 自然语言识别 字符与文字识别 图像识别 动作识别 而现在比较流行的甚至可以说烂大街的深度学习,也属于模式识别建立分类器的范畴,而这个范畴又包括了 监督模式识别 (supervised pattern recognition) 非监督模式识别 (unsupervised pattern recognition) 而对于监督式模式识别,我们了解的大多数深度学习模型都属于这个范畴。这两种问题的处理方式是有一些区别的。 对于处理监督模式识别问题的一般步骤是: 分析问题,分析给定数据哪些因素可以与分类有关。 原始特征获取。 特征提取与选择。 分类器设计,用已知样本进行分类训练。 分类决策。 对于处理非监督模式识别问题的一般步骤是: 分析问题; 原始特征获取; 特征提取与选择; 聚类分析; 结果解释;对于模式识别中的识别而言,引用维基百科中模式识别的内容来简单解释:识别过程与人类的学习过程相似。以光学字符识别之“汉字识别”为例:首先将汉字图像进行处理,抽取主要表达特征并将特征与汉字的代码存在计算机中。就像老师教我们“这个字叫什么、如何写”记在大脑中。这一过程叫做“训练”。识别过程就是将输入的汉字图像经处理后与计算机中的所有字进行比较,找出最相近的字就是识别结果。这一过程叫做“匹配”。所以我们大致可以知道模式识别至少包含两个最重要的过程:特征检测与特征匹配(也就是识别)。 特征检测(特征提取)特征检测也是特征提取,它是计算机图像处理整个流水线工作中上游的核心工作了,比如我们拿到一副图片,首先需要进行预处理,这个预处理包括对图形的剪裁,旋转,平移,缩放,二值化等等简单的操作,下一步就是要进行特征检测了,这也是将一个图形从广义图片的范畴调整到数值化,数字化的开端,从这开始我们知道了人们是如何巧妙的将“图片”这种人类感性认知的东西映射到计算机的内存当中的,又是如何让计算机认识它的! 特征检测可以分为边缘特征检测,角点特征检测,斑点特征检测,脊检测 边缘检测 [edg detection]对于边缘检测,如果你上网搜索一下,首先查到的就是各种边缘检测算子,而这些算子无非就是一些$2\\ast2$或者$3\\ast3$的矩阵罢了。它们又是如何检测图像的边缘的呢?对于一个灰度图像而言,一张图片不过是灰度值从0~255构成的m*n的矩阵罢了。图像矩阵中的每个位置的像素值都是0~255之间的一个数,0就表示没有灰度,就是黑色;255就表示满级的灰度,就是白色。而图片中的边缘是灰度值变化较为明显的地方,这些地方的值与前面的各种边缘滤波器——也就是对应的矩阵做卷积,就会得到高响应,从而判断出哪里是图像的边缘。具体的各类边缘检测算子的区别,我们后续的文章里会继续讨论。但是,到这里,我感觉其实已经可以入门了。实际上检测边缘不是一个简单的问题,如果将边缘认为是一定数量点亮度发生变化的地方,那么边缘检测大体上就是计算这个亮度变化的导数。下图中这个例子,我们的数据是一行不同点亮度的数据。例如,在下面的1维数据中我们可以直观地说在第4与第5个点之间有一个边界: 除非场景中的物体非常简单并且照明条件得到了很好的控制,否则确定一个用来判断两个相邻点之间有多大的亮度变化才算是有边界的阈值,并不是一件容易的事。实际上,这也是为什么边缘检测不是一个简单问题的原因之一。 边缘检测的方法有许多用于边缘检测的方法,他们大致可分为两类:基于搜索和基于零交叉。 基于搜索的边缘检测方法首先计算边缘强度,通常用一阶导数表示,用计算估计边缘的局部方向,通常采用梯度的方向,并利用此方向找到局部梯度模的最大值。 基于零交叉的方法找到由图像得到的二阶导数的零交叉点来定位边缘。滤波做为边缘检测的预处理通常是必要的,通常采用高斯滤波。像我们常见的边缘检测算子包括但不止于: Roberts(这是最简单了算子) Canny (目前最常用的) Sobel Isotropic Sobel Prewitt Laplacian等等一系列算子,都是用来做边缘检测的。角检测(兴趣点检测)[interest point detection]与边缘同样重要的一种特征就是角,也是角点,兴趣点;角点的检测经常用于三维建模以及物体识别中。两条边的交点形成一个角(点)。而图像的要点(也称为受关注点)是指图像中具有代表性以及健壮性的点。也就是说,要点可以是角(点),也可以不是,例如局部亮点或暗点,线段终点,或者曲线上的曲率最大值点。在实际应用中,很多所谓的(角)点检测算法其实是检测要点,而不仅仅是角(点)。所以,如果我们只想检测角的话,还需要对检测出的要点进一步分析。例如也可以先经过边检测,之后在做一些后处理来检测角。角,兴趣点检测方法 $Moravec$ 角检测算法这是最早使用来做角检测的做法,他首先定义所谓的“角”就是那些自我相似程度低的点。这个算法检查所有图像中的像素,并考虑以该像素为中心点的一片范围,该范围与他周围覆盖最大的另一个范围的相似度做为参考,而相似度通常是将两个范围的对应点计算误差的平方和(SSD: Sum of Squared differences) ,越小代表相似度越高。 $Harris\\,\\&\\,Stephens/ Plessey / Shi-Tomasi$ 角检测算法$Harris\\,\\&\\,Stephens$改善了$Moravec$的方法,他们直接考虑每个像素沿着特定方向处的像素的SSD,而不是使用与周围像素范围的SSD。详细内容可以查看之前的文章:Harris Corner Detection $Förstner$ 角检测在某些情况,会希望更精确地去计算角的位置,为了得到近似值,Förstner 算法可以解出闭集上的角附近范围中的所有切线与最接近这些切线的点,该算法依赖于在一个理想的角附近。这个算法感觉有些复杂目前还没有深入了解… 多尺度 $Harris$ 算子…这个就看不懂在讲什么了斑点检测 Laplacian of Gaussian (LoG) 高斯差 Determinant of Hessian (DoH) 最大稳定极值区域 PCBR脊检测 霍夫变换 广义霍夫变换特征描述 SIFT SURF GLOH HOG","categories":[],"tags":[{"name":"计算机视觉","slug":"计算机视觉","permalink":"https://stonema.github.io/tags/计算机视觉/"},{"name":"图像处理","slug":"图像处理","permalink":"https://stonema.github.io/tags/图像处理/"},{"name":"特征","slug":"特征","permalink":"https://stonema.github.io/tags/特征/"}]},{"title":"高频信号与低频信号,图像锐化与模糊","slug":"高频信号与低频信号-图像锐化与模糊","date":"2018-04-24T05:56:12.000Z","updated":"2020-01-18T01:45:05.232Z","comments":true,"path":"2018/04/24/高频信号与低频信号-图像锐化与模糊/","link":"","permalink":"https://stonema.github.io/2018/04/24/高频信号与低频信号-图像锐化与模糊/","excerpt":"","text":"图像频率图像处理中经常遇到”处理高频信号”与”处理低频信号”,对于图像而言,频率的高低,就是图像灰度变化的快慢,也就对应了:高频是噪声和细节,低频是轮廓 低频低频就是颜色缓慢地变化,也就是灰度缓慢地变化,就代表着那是连续渐变的一块区域,这部分就是低频。对于一幅图像来说,除去高频的就是低频了,也就是边缘以外的内容为低频,而边缘的内容就是图像的大部分信息,即图像的大致概貌和轮廓,是图像的近似信息。 高频反过来,高频就是频率变化快。图像中什么时候灰度变化快?就是相邻区域之间灰度相差很大,这就是变化得快的区域。图像中,一个影像与背景的边缘部位,通常会有明显的差别,也就是说变化那条边线那里,灰度变化很快,也即是变化频率高的部位。因此,图像边缘的灰度值变化快,就对应着频率高,即高频显示图像边缘。图像的细节处也是属于灰度值急剧变化的区域,正是因为灰度值的急剧变化,才会出现细节。 噪点另外噪声(即噪点)也是这样,噪点就是与正常的点颜色不一样的区域,也就是说该像素点灰度值有明显差异,也就是灰度值变化过快,所以是高频部分,因此有噪声也属于高频部分。","categories":[{"name":"图像处理","slug":"图像处理","permalink":"https://stonema.github.io/categories/图像处理/"}],"tags":[{"name":"图像信号","slug":"图像信号","permalink":"https://stonema.github.io/tags/图像信号/"},{"name":"信号处理","slug":"信号处理","permalink":"https://stonema.github.io/tags/信号处理/"}]},{"title":"MachineLearning之SVM","slug":"MachineLearning之SVM","date":"2018-04-17T09:23:54.000Z","updated":"2020-01-18T01:45:05.169Z","comments":true,"path":"2018/04/17/MachineLearning之SVM/","link":"","permalink":"https://stonema.github.io/2018/04/17/MachineLearning之SVM/","excerpt":"","text":"了解SVM (Support Vector Machine—支持向量机)线性可分数据SVM(Support Vector Machine)是支持向量机,是常见的一种判别方法。在机器学习领域,是一个有监督的学习模型(通过改进,当数据未被标记时,不能进行监督式学习,需要用非监督式学习,它会尝试找出数据到簇的自然聚类,并将新数据映射到这些已形成的簇。将支持向量机改进的聚类算法被称为支持向量聚类),通常用来进行模式识别、分类以及回归分析。 SVM的目的通过上一章的KNN,我们已经了解到,它可以对数据进行分类处理了:思想就是通过计算每个元素与待分类元素的距离来进行判断。这样做的缺点是需要大量的计算和存储(因为需要计算每个点到待测点的距离);而SVM就是为了解决这样的问题而诞生的.如下图:考虑这种情况:我们有一条线,它的函数形式为:$f(x) = ax{1}+bx{2}+c$(x为测试数据), 通过这条线,可以将数据划分成两个部分。(如上图中的线),当我们新得到一个测试数据$X$的时候,只需要将$X$代入到函数$f(x)$中进行计算。如果$f(x) > 0$ 这个测试样本就属于蓝色的类别,否则它属于红色的类别。我们把这个函数对应的直线叫做:决策边界(Decision Boundary),这种判断的形式非常简单,而且节省内存。像这类我们可以通过使用一条直线(或者高维的超平面)将其分为两部分的数据,我们称其为:线性可分数据。所以,在上面的图片中我们可以找到很多这样的直线,那么,采用哪一条直线是合理的呢?答案是:距离两侧的点越远越好,这样是最好的分类边界(目的是对抗噪声);因此,SVM所做的就是找到一条直线(或超平面),最大化它与训练样本的距离。类似图像中通过中心的粗线。为了找到这个决策边界,我们需要一些训练数据,事实上,我们仅需要两个类别相邻处的一部分数据就可以了。在我们的上图中的例子中,两个类别边界的的临近数据我们选择的分别是两个红色的方块和一个蓝色的圆圈。我们把他们叫做:支持向量(Support Vectors),通过他们的线(虚线)叫做:支持平面(Support Planes)。通过他们足以发现我们需要找到的决策边界。举个例子:蓝色数据是通过$w^{T}x+b{0}>1$来表示的,同时红色数据是由$w^{T}x+b{0}<-1$来表示的。($w$是权重向量$(w=[w{1},w={2},w{3},…,w{n}])$)同时$x$是特征向量$(x=[x{1},x{2},x{3},…,x{n}])$。$b{0}$ 是 bias偏置项。决策边界的表示为:$w^{T}x+b{0}=0$ 。从支持向量到决策边界的最小距离由下式给出$distance{support \\, vectors}=\\frac{1}{||w||}$,margin是这个边界距离两倍,我们需要的是最大化的margin。如图中的例子,我们需要找到了一个最小化的function:$L(w,b{0})$它的公式构成如下: \\min_{w,b_{0}}L(w,b_{0} = \\frac{1}{2}||w||^{2})subject\\ to\\ t_{i}(w^{T}x+b_{0})\\geqslant1\\forall{i}其中$t{i}$是每一个类的标签label,$t{i}\\in {-1,1}$ 对于上述公式的解读上面讲到的SVM的核心就是:使得超平面与和它距离最近的点的距离最大化。这个距离,我们就可以理解成几何距离;很好理解,例如对于上图中的二维平面,就是点到直线的距离(这里直线就是超平面);对于三维空间,就是点到平面的距离;以及高维空间的点到超平面的距离;计算公式都是相同的。在SVM中,把这个几何距离叫做:几何间隔。 例如,在上图的二维空间中,点A到分隔超平面(直线)的距离即为线段AB的长度。几何间隔的计算公式如下:(其中$y^{i}$表示数据点的类别标签,$w,b$分别是超平面$w^{T}x+b=0$的参数) \\gamma ^{(i)}=y^{(i)}((\\frac{w}{||w||})^{T}x^{(i)}+\\frac{b}{||w||})需要注意的是这里分子上是没有绝对值符号的。而我们通常求的距离公式中是有绝对值符号的。(这里后续会详细说明) 距离公式的推导过程:我们知道对于二维平面上的点到直线的距离公式为: d=\\frac{|Ax+By+C|}{\\sqrt{A^{2}+B^{2}}}PS:这个公式的推导过程如下:运用三角形面积相等法则:我们已知$P(x{0},y{0})$,直线方程为:$Ax+By+C=0$。三角形$\\triangle PMN$的面积等于$\\frac{PM \\bullet PN} {2} = \\frac{MN \\bullet PO} {2}$所以,我们把$P$点坐标$(x{0},y{0})$中的$x{0}$带入到直线方程中,可以得到直线上横坐标为$x{0}$的点的纵坐标,然后用这个坐标减去$P$点的纵坐标,再取模就可以得到$PM$的长度。同理可以得到PN的长度。 \\because |PM| = |\\frac{Ax_{0}+By_{0}+C}{B}||PN| = |\\frac{Ax_{0}+By_{0}+C}{A}|\\therefore |PO| = \\frac{|PM|*|PN|}{\\sqrt{|PM|^{2}+|PN|^{2}}}对于三维空间中的距离公式: d=\\frac{|Ax+By+Cz+D|}{\\sqrt{A^{2}+B^{2}+C_{2}}}高维空间: d=\\frac{|A_{1}x_{1}+A_{2}x_{2}+...A_{n}x_{n}|}{\\sqrt{A_{1}^{2}+A_{2}^{2}+...+A_{n}^{2}}}所以,以向量的形式来书写这个公式,高维空间的几何距离为: d = \\frac{|w^{T}x+b|}{||w||} 所以通过上面的推导,我们可以得知高维空间的几何间隔就是这样的计算方式这里的$y^{(i)}$是$x^{(i)}$对应的标签类,属于[-1,+1]与函数间隔做乘后可以判断出是否分类正确: \\gamma ^{(i)}=y^{(i)}((\\frac{w}{||w||})^{T}x^{(i)}+\\frac{b}{||w||})可以看到这个公式与上面的不同是后者是没有绝对值符号的,而SVM中的函数间隔就是指的上式中的分子:$y^{(i)}(w^{T}x+b)$ 也就是未归一化之前的距离。并且函数间隔是带正负号的,也就是表示样本集是属于正样本还是负样本,通过函数间隔是可以得出的。这个公式可以认作是任意样本点到超平面的距离。更要注意的一点是:$y^{(i)}$是与后面的同号的,这样才能保证分类是正确的。才能使得乘积是大于0的。下图中,在超平面上侧的数据集带入到超平面公式中是 $w^{T}x^{(i)}+b < 0$,在超平面下侧的数据集带入到超平面公式中 $w^{T}x^{(i)}+b > 0$,这样的话,$y^{(i)}$ 的值就是要保证与前面的公式同号,上半部分的$y^{(i)}$值应该是负的,下半部分的$y^{(i)}$值应该是正的。(这里$y^{(i)}$怎么确定的,到现在也不明白。) 2018.5.9更新:[$y^{(i)}$标签的由来,以及实例介绍]我们考虑以下形式的n点测试集:$(\\vec{x{1}},y{1}),…,(\\vec{x{n}},y{n})$其中$y{i}$是1或者-1,表明点$\\vec{x{i}}$中每个都是一个$p$ 维实向量。我们想要得到的是将$y{i}=1$的点集与$y{i}=-1$的点集分开的“最大间隔超平面”,使得超平面与最近的点$\\vec{x_{i}}$之间的距离最大化。任何超平面都可以写作满足以下方程的点集$\\vec{x}$。 \\vec{w} \\cdot \\vec{x} -b=0公式很好理解:实际就是三维空间中平面表示法的一般式$Ax+By+Cz+D=0$;因为$A,B,C$可以构成一个平面的法向量,所以这里的$\\vec{w}$也是法向量,$\\frac{b}{||\\vec{w}||}$决定从原点沿着法向量方向到差平面的偏移量。 实际:这个$w^{T}x^{(i)}+b$就是一个分类器,而分类器不过就是二维平面中的直线,三维空间中的平面,高维空间中的超平面罢了。 关于$y(i)$$y(i)$是样本数据的标签,$y(i)\\in {-1,1}$ \\begin{cases} (w \\cdot x_{i})+b \\geqslant 1,& y_{i}=+1\\\\ (w \\cdot x_{i})+b \\leqslant 1,& y_{i}=-1 \\end{cases}如上面的图像所示:离超平面最近的点到最优超平面的距离就是$margin$,我们要优化的,就是使得这个$margin$最大化。又因为最大间隔超平面(也就是最优超平面)位于两个数据集分割平面的中间,而分割两个数据集的平面是无数多的,(可以称为超平面簇)而超平面簇的两个最边缘的平面表达式为: \\begin{cases} \\vec{w}\\cdot\\vec{x}-b=1 \\\\ \\vec{W}\\cdot\\vec{x}-b=-1 \\end{cases}不难得到两个超平面的距离就是$\\frac{2}{||\\vec{w}||}$,因此我们需要最大化间隔,就是最小化$||\\vec{w}||$。同时为了使得样本数据点都在超平面的间隔以外,我们就有了上面的公式: \\begin{cases} (w \\cdot x_{i})+b \\geqslant 1,& y_{i}=+1\\\\ (w \\cdot x_{i})+b \\leqslant 1,& y_{i}=-1 \\end{cases}最大间隔超平面完全是由最靠近它的那些 ${\\displaystyle {\\vec {x}}{i}}$ 确定的。这些 ${\\displaystyle {\\vec {x}}{i}}$ 叫做支持向量。 对于线性不可分数据(待续)","categories":[{"name":"机器学习","slug":"机器学习","permalink":"https://stonema.github.io/categories/机器学习/"}],"tags":[{"name":"机器学习","slug":"机器学习","permalink":"https://stonema.github.io/tags/机器学习/"},{"name":"有监督学习","slug":"有监督学习","permalink":"https://stonema.github.io/tags/有监督学习/"},{"name":"SVM","slug":"SVM","permalink":"https://stonema.github.io/tags/SVM/"},{"name":"Supervised Learning","slug":"Supervised-Learning","permalink":"https://stonema.github.io/tags/Supervised-Learning/"}]},{"title":"MachineLearning之KNN","slug":"MachineLearning之KNN","date":"2018-04-17T02:42:46.000Z","updated":"2020-01-18T01:45:05.168Z","comments":true,"path":"2018/04/17/MachineLearning之KNN/","link":"","permalink":"https://stonema.github.io/2018/04/17/MachineLearning之KNN/","excerpt":"","text":"KNN (k-Nearest Neighbour) K临近算法kNN是可用于监督学习的最简单的分类算法之一。它的思想是:在特征空间中搜索最匹配的测试数据。所谓K临近,就是k个最近的邻居的意思,说的是每个样本都可以用它最接近的k个邻居来代表。kNN算法的核心思想是:如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。该方法在确定分类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。 kNN方法在类别决策时,只与极少量的相邻样本有关。由于kNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,kNN方法较其他方法更为适合。 像上图中的例子,红色的三角和蓝色的方块是已经存在的图形,从我们人类的分类角度来看,把方块看作一类,把三角看作一类,是合理的办法,但是计算机如何去实现这样的分类呢?比如我们现在想知道这个新加入的绿色的圆圈属于什么类别?KNN的思想如下:我们寻找距离圆圈最近的K个特征,比如K=3,我们找到了两个三角,一个方块,我们认定它有2/3的概率是三角,所以归为”三角”类,又如K=7,距离圆圈最近的7个元素中,有5个方块,2个三角,所以有5/7的概率是方块,所以它被归类到方块。 KNN算法的实现步骤: 计算距离 选择距离最小的k个点 排序 改进:在使用KNN的过程中难免会遇到如下情况:当计算需要分类的点与其他点的距离时,当给定一个K值,发现距离它最近的两个类中的元素数量是一样的,这样就有了后续的改进算法。 Python代码实现:1234567891011121314151617181920212223242526272829import cv2import numpy as npimport matplotlib.pyplot as plt# Feature set containing (x,y) values of 25 known/training data# 10个特征数据,每个数据都是由一个坐标构成,每个数都是0~100之间的随机数# 特征数据矩阵为10行,2列 :[10,2]trainData = np.random.randint(0,100,(10,2)).astype(np.float32)# Labels each one either Red or Blue with numbers 0 and 1# 给每个数据标定labelresponses = np.random.randint(0,2,(10,1)).astype(np.float32)# Take Red families and plot them# 让label = 0的数据为red三角red = trainData[responses.ravel()==0]plt.scatter(red[:,0],red[:,1],80,'r','^')# Take Blue families and plot them# 让label = 1的数据为blue的方块blue = trainData[responses.ravel()==1]plt.scatter(blue[:,0],blue[:,1],80,'b','s')# 新加入的绿色圆圈newcomer = np.random.randint(0,100,(1,2)).astype(np.float32)plt.scatter(newcomer[:,0],newcomer[:,1],80,'g','o')knn = cv2.ml.KNearest_create()knn.train(trainData, cv2.ml.ROW_SAMPLE, responses)ret, results, neighbours ,dist = knn.findNearest(newcomer, 3)# 打印结果,如果属于red ,result中属于0 label;如果是blue,就是result=1print( \"result: {}\\n\".format(results) )print( \"neighbours: {}\\n\".format(neighbours) )print( \"distance: {}\\n\".format(dist) )plt.show() 输出结果:12345result: [[ 1.]]neighbours: [[ 1. 1. 0.]]distance: [[ 18. 145. 1109.]] 图像:从图像中可以看出,圆圈距离方块的距离较近,同时K=3,所以可以看到输出结果为label=1,也就是分到了blue方块类,所以result: [[ 1.]],最近的三个元素分别为方块,方块,三角,所以neighbours: [[ 1. 1. 0.]],三个元素到目标圆圈的距离为:distance: [[ 18. 145. 1109.]]","categories":[{"name":"机器学习","slug":"机器学习","permalink":"https://stonema.github.io/categories/机器学习/"}],"tags":[{"name":"机器学习","slug":"机器学习","permalink":"https://stonema.github.io/tags/机器学习/"},{"name":"OpenCV","slug":"OpenCV","permalink":"https://stonema.github.io/tags/OpenCV/"},{"name":"KNN","slug":"KNN","permalink":"https://stonema.github.io/tags/KNN/"},{"name":"有监督学习","slug":"有监督学习","permalink":"https://stonema.github.io/tags/有监督学习/"}]},{"title":"LoG算子与DoG算子","slug":"LoG算子与DoG算子","date":"2018-04-16T06:07:01.000Z","updated":"2020-01-18T01:45:05.165Z","comments":true,"path":"2018/04/16/LoG算子与DoG算子/","link":"","permalink":"https://stonema.github.io/2018/04/16/LoG算子与DoG算子/","excerpt":"","text":"LoG简介图像中灰度变化较大的非连续像素可以看做是边缘,边缘是最为重要的图像特征之一,在目标检测、追踪、识别中都必不可少的使用到了边缘,人类视觉系统也对边缘信息非常敏感。边缘检测的一般步骤如下: 滤波(去噪) 增强(一般是通过计算梯度幅值) 检测(在图像中有许多点的梯度幅值会比较大,而这些点并不都是边缘,所以应该用某种方法来确定边缘点,比如最简单的边缘检测判据:梯度幅值阈值) 定位(有些应用场合要求确定边缘位置,可以在子像素水平上来估计,指出边缘的位置和方向) 边缘检测方法比较常用的有基于各种算子的方法,有基于一阶导数的各种算子(Roberts、Sobel、Canny、Prewitt等),还有基于二阶导数的Laplacian算子(LoG)等。 LoG边缘检测算子是David Courtnay Marr和Ellen Hildreth(1980)共同提出的。因此,也称为边缘检测算法或Marr & Hildreth算子。该算法首先对图像做高斯滤波,然后再求其拉普拉斯(Laplacian)二阶导数。即图像与 Laplacian of the Gaussian function 进行滤波运算。最后,通过检测滤波结果的零交叉(Zero crossings)可以获得图像或物体的边缘。因而,也被业界简称为Laplacian-of-Gaussian (LoG)算子。 Marr和Hildreth证明了以下两个观点: 灰度变化与图像尺寸没有关系,因此检测需要不同尺度的算子 灰度的突然变化会在一阶导数中引起波峰和波谷,或者二阶导数中一起零交叉 其中一阶导数一般找梯度极值。二阶导数找过零点(需要忽略无意义的过零点(即均匀零区))。 LoG边缘检测算法步骤: 平滑:高斯滤波器 增强:Laplacian算子计算二阶导 检测:二阶导零交叉点并对应于一阶导数的较大峰值 定位:线性内插 根据卷积的求导法则,先卷积后求导和先求导后卷积是相等的,所以可以把第1、2步合并为一步,先对高斯滤波器做拉普拉斯变换,得到LoG算子,然后再用这个算子与图像做卷积。 探测器灰度的突然变化会在一阶导数中引起波峰或者波谷,或者在二阶导数中等效的引起零交叉。 一阶微分探测器从数学上讲,像素的灰度值变化速率,可以用一阶微分来检测,对于二维离散函数,其微分可以表示为:\\frac{\\partial f(x,y)}{\\partial x} = f(x+1,y) - f(x,y)\\qquad\\frac{\\partial f(x,y)}{\\partial y} = f(x,y + 1) - f(x,y)\\qquad 二阶微分检测器二阶微分同样可以如一阶微分一样,检测到函数的变化。二阶微分其实就是拉普拉斯算子,一般使用下面的方法表示:(两个方向) \\nabla^{2}f = \\frac{\\partial^{2}f}{\\partial{x^{2}}} + \\frac{\\partial^{2}f}{\\partial{y^{2}}}为了适应图像处理,我们将拉普拉斯算子写成离散形式,其中 \\frac{\\partial^{2}f}{\\partial{x^{2}}} = f(x+1,y)+f(x-1,y)-2f(x,y)\\frac{\\partial^{2}f}{\\partial{y^{2}}} = f(x,y+1)+f(x,y-1)-2f(x,y)最终结果为:(两个方向作和)\\begin{equation}\\begin{aligned}\\frac{\\partial^{2}f}{\\partial{x^{2}}} + \\frac{\\partial^{2}f}{\\partial{x^{2}}}&= f(x+1,y)+f(x-1,y)-2f(x,y) + f(x+1,y)+f(x-1,y)-2f(x,y)\\&= f(x+1,y)+f(x-1,y) + f(x+1,y)+f(x-1,y) - 4f(x,y)\\nonumber\\end{aligned}\\end{equation} 所以拉普拉斯算子在图像处理中的模板为: \\left[ \\begin{matrix} 0 & 1 & 0 \\\\ 1 & -4 & 1 \\\\ 0 & 1 & 0 \\end{matrix} \\right]拉普拉斯算子对噪声和离散点极为敏感,所以在利用其进行边缘检测的时候,需要首先对图像进行平滑,除去噪声的影响。因为高斯运算和拉普拉斯运算可以叠加,我们可以将其的最终形式写为:最终简化为:MatLab中的样子12345laplace_gaussian_filter = fspecial('log',[40 40],4.5);subplot(121)surf(laplace_gaussian_filter);subplot(122)surf(-laplace_gaussian_filter); DoG简介在计算机视觉中,高斯差(英语:Difference of Gaussians,简称“DOG”)是一种将一个原始灰度图像的模糊图像从另一幅灰度图像进行增强的算法,通过DOG以降低模糊图像的模糊度。这个模糊图像是通过将原始灰度图像经过带有不同标准差的高斯核进行卷积得到的。用高斯核进行高斯模糊只能压制高频信息。从一幅图像中减去另一幅可以保持在两幅图像中所保持的频带中含有的空间信息。这样的话,DOG就相当于一个能够去除除了那些在原始图像中被保留下来的频率之外的所有其他频率信息的带通滤波器。 DoG的数学含义高斯函数的定义为: G_{\\sigma_{1}}(x,y)=\\frac{1}{\\sqrt{2\\pi\\sigma^{2}_{1}}}\\exp(-\\frac{x^{2}+y^{2}}{2\\sigma^{2}_{1}})我们知道,对于高斯金字塔的构建,就是对于不同尺度下的相同图片进行不同的 $\\sigma$ 参数下的滤波操作,看下图中的左侧,就是对原图像进行了不同 $\\sigma$ 高斯滤波的图像,对一副图像进行高斯滤波操作,在数学上就是高斯函数与原函数做卷积运算,从而得到高斯模糊图像,例如分别为$\\sigma{1},\\sigma{2}$ : g_{1}(x,y)=G_{\\sigma_{1}}(x,y)*f(x,y) g_{2}(x,y)=G_{\\sigma_{2}}(x,y)*f(x,y)而右侧的DoG图像(高斯差分图像)就是左侧两个不同的高斯模糊图像做差得到的,高斯差分的数学表达:(上图中的两式相减) g_{1}(x,y)-g_{2}(x,y)=G_{\\sigma_{1}}(x,y)*f(x,y)-G_{\\sigma_{2}}(x,y)*f(x,y)=DoG*f(x,y)所以,上述公式可以将 $DoG$ 表示为: DoG = G_{\\sigma_{1}}-G_{\\sigma_{2}}=\\frac{1}{\\sqrt{2\\pi}}(\\frac{1}{\\sigma_{1}}e^{-\\frac{(x^{2}+y^{2})}{2\\sigma^{2}_{1}}}-\\frac{1}{\\sigma_{2}}e^{-\\frac{(x^{2}+y^{2})}{2\\sigma^{2}_{2}}})这样,右侧就得到了高斯差分(DoG)图像 得到DoG图像后,再在DoG图像上检测特征,提取特征,这样做的好处就是,既能降低计算成本,又能不损失特征,不同的高斯平滑后得到的DoG图像都是明显的特征点。 DoG的应用作为一个增强算法,DOG可以被用来增加边缘和其他细节的可见性,大部分的边缘锐化算子使用增强高频信号的方法,但是因为随机噪声也是高频信号,很多锐化算子也增强了噪声。DOG算法去除的高频信号中通常包含了随机噪声,所以这种方法是最适合处理那些有高频噪声的图像。这个算法的一个主要缺点就是在调整图像对比度的过程中信息量会减少。 当它被用于图像增强时,DOG算法中两个高斯核的半径之比通常为4:1或5:1。当设为1.6时,即为高斯拉普拉斯算子的近似。高斯拉普拉斯算子在多尺度多分辨率像片。用于近似高斯拉普拉斯算子两个高斯核的确切大小决定了两个高斯模糊后的影像间的尺度。 DOG也被用于尺度不变特征变换中的斑点检测。事实上,DOG算法作为两个多元正态分布的差通常总额为零,把它和一个恒定信号进行卷积没有意义。当K约等于1.6时它很好的近似了高斯拉普拉斯变换,当K约等于5时又很好的近似了视网膜上神经节细胞的视野。它可以很好的作为一个实时斑点检测算子和尺度选择算子应用于递归程序。 在DOG算法中,它被认为是在模拟视网膜上的神经从影像中提取信息从而提供给大脑。 参考: 边缘检测滤波器 高斯差","categories":[{"name":"特征提取","slug":"特征提取","permalink":"https://stonema.github.io/categories/特征提取/"}],"tags":[{"name":"特征提取","slug":"特征提取","permalink":"https://stonema.github.io/tags/特征提取/"},{"name":"斑点检测算法","slug":"斑点检测算法","permalink":"https://stonema.github.io/tags/斑点检测算法/"},{"name":"算子","slug":"算子","permalink":"https://stonema.github.io/tags/算子/"},{"name":"滤波","slug":"滤波","permalink":"https://stonema.github.io/tags/滤波/"}]},{"title":"SIFT尺度不变特征变换","slug":"SIFT尺度不变特征变换","date":"2018-04-12T06:22:00.000Z","updated":"2020-01-18T01:45:05.172Z","comments":true,"path":"2018/04/12/SIFT尺度不变特征变换/","link":"","permalink":"https://stonema.github.io/2018/04/12/SIFT尺度不变特征变换/","excerpt":"","text":"通过上一篇文章,我们了解到了一些角点探测器,如 Harris Corner Detection 等。它们是旋转不变的,这意味着,即使图像旋转了,我们也可以找到相同的角点。这是显而易见的,因为角落在旋转的图像中也是角点。但是缩放呢?如果图像缩放,角点可能不是角点。例如,请查看下面的简单图片。放大同一个窗口时,小窗口内的小图像中的一个角点变成平坦的。所以Harris Corner的角点不是尺度不变的。 因此,2004年,不列颠哥伦比亚大学的D.Lowe在他的论文中提出了一种新的算法 - 尺度不变特征变换(SIFT),该算法从尺度不变关键点获取独特的图像特征,提取关键点并计算其描述符。 SIFT算法主要涉及四个步骤。我们将逐一分析他们。 Scale-space Extrema Detection 尺度空间极值检测从上图中可以看出,我们不能使用相同的窗口来检测不同尺度下图片上的关键点。上图中,用来检测小图像中的角点还能使用,但是如何检测大尺度图片下的角点,我们就需要更大的窗口函数,因此,我们需要尺度空间滤波。其中,Laplacian of Gaussian(LoG) 就是为具有不同 $\\sigma$ 值的图像而创建的。LoG 作为一个斑点探测器(a blob detector),可以检测由于 $\\sigma$ 变化而产生的各种尺寸的斑点。简而言之,$\\sigma$ 的作用就是一个缩放参数。也就是说当 $\\sigma$ 较小的时候,表示高斯核拥有较小的高斯核,适合探测较小尺寸图片上的角点;当 $\\sigma$ 较大的时候,表示该高斯核适合探测较大图片尺寸上的角点。 因此,我们探测一个图片的角点就需要三个参数 $(x,y,\\sigma)$ 这意味着在 $\\sigma$ 尺度下 $(x,y)$ 是候选关键点。 但是LoG算法的算法开销略大,所以SIFT选用了Difference of Gaussians (DoG——高斯差分)。DoG图像的获得是由两个不同高斯模糊的图像(——例如一个为 $\\sigma$, 另一个为 $k\\sigma$,也就是不同尺寸的同一张图像)作差差而得到的,这个DoG图像叫做:高斯差分图像。这个过程在高斯金字塔下完成: 从上图可以看出,左侧是不同尺度,不同分辨率的原图像构成的图像金字塔,(一般把原图像放大一倍作为第一组,然后每一组中又有多个图像构成的层,同一组中的不同层的区别是高斯模糊的程度,然后组跟组之间的关系为:第二组中的第一层的图像是选取的第一组中的倒数第三层图像,依次类推;同时第二组跟第一组比图像大小上依次尺度降低为上一层的二分之一,长宽各变为原来的一半)。 一旦找到这个DoG图像,就会在尺度和空间中搜索该DoG图像的局部极值。(具体的实现过程下面会介绍)从下面图像中可以看到,对于DoG图像,对于某个像素处,我们要判断它是否是我们要找的局部极值点。方式是提取图中的 × 点处周围的26个点(它所在的DoG图中的周围8个点,它上下两层相邻的各9个点),进行比较,如果是极值(局部极值),就当做一个候选点(潜在关键点)。 所以整个流程如下图: Keypoint Localization 关键点定位候选点(潜在关键点)被找到后,需要确定更高精度的极值的。文章中使用了尺度空间的Taylor展开来获取这个更加准确的极值,如果这个极值的大小小于给定的阈值(Paper中的阈值为:0.03),这个极值就会被拒绝,也就是说候选点被舍弃。这个阈值在OpenCV中被称作 contractThreshold。 DoG 对图像的边缘也有很高的响应,所以也要想办法把图像边缘过滤掉,为此,论文中使用了类似于Harris角点探测的概念——使用2x2的Hessian矩阵(H)来计算曲率,我们从Harris角点探测的文章中了解到,一个特征值比另一个特征值大很多的时候,这时表示的是边缘,回顾如下图: 这里文中也使用了简单的探测函数。 这样以后,消除了一部分候选点和边缘,剩下的是原图中的强兴趣点(原文叫做 strong interest point,这。。怎么翻译?)。 Orientation Assignment (梯度)方向赋值现在为每个关键点赋值一个方向,以实现图像的旋转不变性,在当前尺度下确定一个关键点的邻域,并在该区域计算梯度方向。具体方法为:创建一个具有36个方向的方向直方图,每个方向相隔10度,这样36个方向可以覆盖360度。如下图中的右图。图中左侧是关键点圆形邻域内的所有分割,这些区域都有自己的方向梯度,然后将每个区域的方向梯度在相同方向加和,分列在直方图的各个方向上,纵轴就是该点各方向梯度和的值。最高的那个就是该关键点的主梯度方向。当然有可能一个点处有多个方向梯度,就是该点向多个方向的灰度梯度都比较大,体现在直方图中就是会有多个方向的值都很高,所以一个点有主梯度方向,也可以有辅梯度方向。 Keypoint Descriptor 关键点描述符找到了关键点的梯度方向,下一步就是要描述这些关键点了,与求该点主方向不同的是,如下图中的左侧,关键点划分成16x16的区域,它被划分为16个4x4的子块。下一步工作就是,针对每一个4x4的子块,创建一个具有8个方向(每隔45°一个方向)的直方图,即这个区域的8个方向的梯度强度。这样16个子块就会有 8x16=128 个梯度强度信息数据,这也就是SIFT的128维特征矢量。除此之外,还采取了几项措施来实现对光照变化,旋转等的鲁棒性。 Keypoint Matching 关键点匹配两幅图像之间的关键点的匹配工作是通过判断他们的最近邻域来实现的。但在某些情况下,第二最近邻域可能比最近邻域匹配的更好。(这个地方该怎么翻译?总感觉不对 Keypoints between two images are matched by identifying their nearest neighbours. But in some cases, the second closest-match may be very near to the first.)这可能是由于噪音或其他原因产生的。在这种情况下,需要计算最近距离与第二近距离的比值,如果这个比值大于0.8,则这个第二最近距离就被舍弃。这个理解如下:计算量上,如果我们有10个待匹配点,那么计算量就是$10^{2}$; 使用SIFT被申请了专利,所以原文中的算法放在了the opencv contrib repo。12345678import numpy as npimport cv2 as cvimg = cv.imread('home.jpg')gray= cv.cvtColor(img,cv.COLOR_BGR2GRAY)sift = cv.xfeatures2d.SIFT_create()kp = sift.detect(gray,None)img=cv.drawKeypoints(gray,kp,img)cv.imwrite('sift_keypoints.jpg',img) sift.detect()函数用来在图像中查找关键点,OpenCV还提供cv.drawKeyPoints()函数,用于在关键点位置上绘制小圆圈。12img=cv.drawKeypoints(gray,kp,img,flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)cv.imwrite('sift_keypoints.jpg',img) 图例:计算特征描述符,OpenCV提供了两种方法: 既然你已经找到了关键点,你可以调用sift.compute()来计算我们找到的关键点的描述符。例如:kp,des = sift.compute(gray,kp) 如果还没有找到关键点,可以使用如下方法将寻找关键点和计算描述符合并为一步:sift.detectAndCompute() 在OpenCV中:12sift = cv.xfeatures2d.SIFT_create()kp, des = sift.detectAndCompute(gray,None) 这里kp是一个关键点列表,des是一个形为$Number_of_Keypoints×128$的numpy数组。 至此我们获得了关键点,描述符等等,SIFT的基本概念也了解了。如何匹配不同图像中的关键点呢?后续文章中将会学习。 其他知识储备角点 尺度规范化的LoG算子 Lowe使用DoG图像 高斯差分图像 拟合三维二次函数 图像灰度 归一化处理 非线性光照 相机饱和度 coding+pooling线路 BOW(Bag of Word),VLAD,SCP以及我们使用的LLC(Locality-constrained Linear Coding) sparse 匹配与选取 RANSAC 相似分计算 SURF、Color-SIFT、Affine-SIFT、Dense-SIFT、PCA-SIFT、Kaze","categories":[{"name":"图像处理","slug":"图像处理","permalink":"https://stonema.github.io/categories/图像处理/"}],"tags":[{"name":"SIFT","slug":"SIFT","permalink":"https://stonema.github.io/tags/SIFT/"},{"name":"特征描述","slug":"特征描述","permalink":"https://stonema.github.io/tags/特征描述/"}]},{"title":"Harris Corner Detection","slug":"HarrisCornerDetection","date":"2018-04-10T08:50:40.000Z","updated":"2020-01-18T01:45:05.157Z","comments":true,"path":"2018/04/10/HarrisCornerDetection/","link":"","permalink":"https://stonema.github.io/2018/04/10/HarrisCornerDetection/","excerpt":"","text":"上一篇文章介绍了什么是特征,本篇介绍一下如何提取特征(角点特征)特征点检测广泛应用到目标匹配、目标跟踪、三维重建等应用中,在进行目标建模时会对图像进行目标特征的提取,常用的有颜色、角点、特征点、轮廓、纹理等特征。上一篇文章当中,我们学习到了对于一个图像,图像的角落是比较适合作为特征的区域,是各个方向上强度变化较大的区域。早在一九八八年(1988年老子还特么没出生呢!人家就开始研究这些东西了。。。这个领域到底跟美国有多大差距啊!!),Chris Harris&Mike Stephens早期在其论文《A Combined Corner and Edge Detector》中就尝试去发现了这些角落,并把它称为Harris Corner Detector。Harris角点检测是通过数学计算在图像上发现角点特征的一种算法,而且其具有旋转不变性的特质。它的原理就是找到滑动窗口内的点,然后这个点向各个方向上的灰度差最明显的地方(实际就是一阶导数最大的地方)。OpenCV中的Shi-Tomasi角点检测就是基于Harris角点检测改进算法。 他把这个简单的想法变成了一种数学形式。这个表达式如下: E(u,v) = \\sum_{x,y}\\underbrace{w(x,y)}_{window function}[\\underbrace{I(x+u,y+v)}_{shifted intensity} - \\underbrace{I(x,y)}_{intensity}]^2其中 $W(x, y)$ 表示移动窗口,$I(x, y)$ 表示像素灰度值强度,范围为 0~255。根据泰勒级数 计算一阶到N阶的偏导数,最终得到一个Harris矩阵公式:Window function是给对应的 $x,y$ 像素加权的矩形窗口或高斯窗口。对于角点检测,我们必须使得$E(u,v)$取得最大值。这样就意味着,对于上面的公式,我们必须使得等号后的第二项最大化(中括号内的项),运用泰勒展开式并使用一些数学过程,我们能够最终得到下列方程: E(u,v) \\approx [u,v] M \\left[ \\begin{matrix} u\\\\ v\\\\ \\end{matrix} \\right]同时: M = \\sum_{x,y}(x,y) \\left[ \\begin{matrix} I_{x}I_{x}&I_{x}I_{y}\\\\ I_{x}I_{y}&I_{y}I_{y}\\\\ \\end{matrix} \\right]这里$I{x}$ 和 $I{y}$ 是图像在 $x$ 和 $y$ 方向上的导数,(能够简单的通过cv.Sobel()方法得到)。 然后是主要部分,这帮人又创建了一个得分数,它将决定一个窗口是否可以包含角点。 R = det(M) - k(trace(M))^2and $det(M) = \\lambda{1}\\lambda{2}$ (是行列式) $trace(M) = \\lambda{1} + \\lambda{2}$ (是矩阵的迹) $\\lambda{1}$ and $\\lambda{2}$ (是M的特征值) 通过上面的公式可以确定的是特征值决定了我们找到的这个区域中是包含的角点,边缘,还是平面。 当 $|R|$ 是比较小的值的时候,也就是 $\\lambda{1}$ and $\\lambda{2}$ 很小,则表示这个区域是平坦的。 当 $R < 0$ 时,只有当 $\\lambda{1} >> \\lambda{2}$ 或者 $\\lambda{2} >> \\lambda{1}$ 时才会发生,此时表示这块区域是边缘。 当 $R$ 是非常大的值的时候,只有当 $\\lambda{2}, \\lambda{2}$ 都比较大而且 $\\lambda{1}\\sim\\lambda{2}$ 的时候这个区域表示角点。 上面的表述可以用一张图片来表示 上图中的横向和纵向分别可以看作是$\\lambda{1}$ 和 $\\lambda{2}$ 的坐标,不要把当作是个正方形。哈里斯角点(角落)检测的结果是一个具有得分数的灰度图像。合适的阈值会帮助我们找到图像中的角点。我们将会以一个简单的例子说明。 Harris Corner Detector 在 OpenCV 中的应用OpenCV中有cv.cornerHarris(),它的参数分别是: img - Input image, 应该是32位的灰度图. blockSize - 考虑角点检测的邻域的大小. ksize - 使用Sobel导数的光圈参数 k - 方程中Harris detector的自由参数. example:1234567891011121314import numpy as npimport cv2 as cvfilename = 'chessboard.png'img = cv.imread(filename)gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)gray = np.float32(gray)dst = cv.cornerHarris(gray,2,3,0.04)#result is dilated for marking the corners, not importantdst = cv.dilate(dst,None)# Threshold for an optimal value, it may vary depending on the image.img[dst>0.01*dst.max()]=[0,0,255]cv.imshow('dst',img)if cv.waitKey(0) & 0xff == 27: cv.destroyAllWindows() 结果如下: Corner with SubPixel Accuracy (具有子像素准确度的角点)有时候,你可能需要以最高的精度找到角点,OpenCV提供了可以进一步细化像素检测角点的函数cv.cornerSubPix(),首先还是要先找到 Harris Corner 然后通过这些角点的质心来优化它们,下图中,Harris Corner是红色像素,精确角点为绿色像素。我们指定迭代次数或达到一定的准确度后停止,(二者以先发生者为准)。我们还需要定义要搜索拐角的邻域的大小。12345678910111213141516171819202122import numpy as npimport cv2 as cvfilename = 'chessboard2.jpg'img = cv.imread(filename)gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)# find Harris cornersgray = np.float32(gray)dst = cv.cornerHarris(gray,2,3,0.04)dst = cv.dilate(dst,None)ret, dst = cv.threshold(dst,0.01*dst.max(),255,0)dst = np.uint8(dst)# find centroidsret, labels, stats, centroids = cv.connectedComponentsWithStats(dst)# define the criteria to stop and refine the cornerscriteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 100, 0.001)corners = cv.cornerSubPix(gray,np.float32(centroids),(5,5),(-1,-1),criteria)# Now draw themres = np.hstack((centroids,corners))res = np.int0(res)img[res[:,1],res[:,0]]=[0,0,255]img[res[:,3],res[:,2]] = [0,255,0]cv.imwrite('subpixel5.png',img) 结果如下: (一些关键点可能需要放大来看) 至此Harris Corner Detection就介绍完了。 总结Harris角点检测是特征点检测的基础,提出了应用邻近像素点灰度差值概念,从而进行判断是否为角点、边缘、平滑区域。Harris角点检测原理是利用移动的窗口在图像中计算灰度变化值,其中关键流程包括转化为灰度图像、计算差分图像、高斯平滑、计算局部极值、确认角点。在特征点检测中经常提出尺度不变、旋转不变、抗噪声影响等,这些是判断特征点是否稳定的指标。 总的来说通过看公式 E(u,v) = \\sum_{x,y}\\underbrace{w(x,y)}_{window function}[\\underbrace{I(x+u,y+v)}_{shifted intensity} - \\underbrace{I(x,y)}_{intensity}]^2第一项是窗口函数,第二项是灰度梯度,也就是灰度变化的导数,$x$,$y$ 决定了各个方向,而后窗口函数是矩形窗口函数,或者高斯窗口函数。这是因为:矩形窗口函数可以过滤掉图像边缘,因为边缘出去在边缘方向的梯度是很小的,其他方向梯度也很大;高斯窗口函数可以过滤掉凸点或者一些孤立点,因为凸点和孤立点的各个方向的梯度也很大。 这两个结合使用就可以选出角点(图像上拐角的地方)。 问题与后续发展 后续又出现了Harris Corner的改进版:Shi-Tomasi Corner Detection Harris Corner是旋转不变的,但是不是尺度不变的,因为探测窗口的大小不变的情况下,放大图像后,原来的角点有可能变成平滑的,如下图: 为了找到旋转,尺度都不变的探测器,由此诞生了SIFT。","categories":[{"name":"图像处理","slug":"图像处理","permalink":"https://stonema.github.io/categories/图像处理/"}],"tags":[{"name":"特征提取","slug":"特征提取","permalink":"https://stonema.github.io/tags/特征提取/"},{"name":"图形","slug":"图形","permalink":"https://stonema.github.io/tags/图形/"},{"name":"图像","slug":"图像","permalink":"https://stonema.github.io/tags/图像/"},{"name":"角检测","slug":"角检测","permalink":"https://stonema.github.io/tags/角检测/"}]},{"title":"特征和特征提取","slug":"特征和特征提取","date":"2018-04-10T08:18:02.000Z","updated":"2020-01-18T01:45:05.208Z","comments":true,"path":"2018/04/10/特征和特征提取/","link":"","permalink":"https://stonema.github.io/2018/04/10/特征和特征提取/","excerpt":"","text":"What are featuresFeatures Extraction特征提取的主要目的是:降维! 特征提取的主要目的是:降维! 特征提取的主要目的是:降维! 重要的事情说三遍!!! 本篇文章学习一下什么是特征,在刚开始读研的时候,看论文,跑网上的实例,始终体会特征这两个字的真正含义,没办法在大脑中构建一个关于特征的清晰的具象化,数字化,量化的概念,直到后来慢慢学习的多了,了解的多了,实践的多了,才有了一些自己的认知。本文就记录一下自己的认知过程,希望对今后自己能有些启发,对看到了人们能有些帮助,就够了。 在学习OpenCV的过程中,第一章就是介绍 What are Features, why corners are important. 比如说我们看到任何动物,看到任何常见物体,都能迅速的做出判断,它是什么。这个结果是如何产生的呢?中间又经历了哪些过程呢?我想这大概率就是特征工程的起源,想真实反映大脑的思维,并通过计算机技术在编程中实现,不过就目前来讲很难说我们分辨某个事物是通过目前计算机领域所定义的特征来识别的,也很难描述人脑是如何找到这些特征的,不过在计算机图形学领域,确实已经做出了一些富有成效的工作。 例如下面这张图: The image is very simple. At the top of image, six small image patches are given. Question for you is to find the exact location of these patches in the original image. How many correct results can you find? A and B are flat surfaces and they are spread over a lot of area. It is difficult to find the exact location of these patches. C and D are much more simple. They are edges of the building. You can find an approximate location, but exact location is still difficult. This is because the pattern is same everywhere along the edge. At the edge, however, it is different. An edge is therefore better feature compared to flat area, but not good enough (It is good in jigsaw puzzle for comparing continuity of edges). Finally, E and F are some corners of the building. And they can be easily found. Because at the corners, wherever you move this patch, it will look different. So they can be considered as good features. So now we move into simpler (and widely used image) for better understanding. 译文: 这个图像非常简单。在图像的顶部,给出了六个小图像。您的问题是要在原始图像中找到这些小图像的确切位置。你能找到多少正确的结果? A和B是平坦的表面,它们分布在很多区域。很难找到这些小图像的确切位置。 C和D更简单。它们是建筑物的边缘。你可以找到一个大概的位置,但确切的位置仍然很困难。这是因为沿边缘的模式是相同的。然而,在边缘,它是不同的。因此,与平坦区域相比,边缘的特征更好,但不够好(对于比较边缘的连续性,它在拼图游戏中效果很好)。 最后,E和F是建筑物的一些角落。他们可以很容易的在原图像中找到。因为对于‘角’而言,无论你如何移动这个‘角’的小图像,它都不会在原图像中轻易找到相同的。所以他们可以当做最合适的特征也(就是feature)。 现在我们了解一个更简单的例子(并广泛使用的图像)以便更好地理解。图片如下: Just like above, the blue patch is flat area and difficult to find and track. Wherever you move the blue patch it looks the same. The black patch has an edge. If you move it in vertical direction (i.e. along the gradient) it changes. Moved along the edge (parallel to edge), it looks the same. And for red patch, it is a corner. Wherever you move the patch, it looks different, means it is unique. So basically, corners are considered to be good features in an image. (Not just corners, in some cases blobs are considered good features). So now we answered our question, "what are these features?". But next question arises. How do we find them? Or how do we find the corners?. We answered that in an intuitive way, i.e., look for the regions in images which have maximum variation when moved (by a small amount) in all regions around it. This would be projected into computer language in coming chapters. So finding these image features is called Feature Detection. 就像上面那样,蓝色斑块是平坦的区域,很难找到它在原图中的具体位置。因为在原图中蓝色的区域,不论你如何移动色块,它看起来都一样。黑色框内有一个边缘。如果沿垂直方向移动它,会发现它与垂直边缘不同。如果沿着水平边缘移动,看起来是一样的。对于红色框,它是一个角落。无论你在哪里移动这部分,它看起来都不一样,意味着它是独一无二的。所以基本上,角落被认为是图像中的优秀特征。(不仅仅是角落,在某些情况下,斑点也被认为是很好的特征)。 所以现在我们回答了我们的问题,“特征是什么?”。 但同时下一个问题出现了——我们如何找到它们?或者我们如何找到角落?我们以一种直观的方式回答了这个问题,例如,在周围的所有地区移动(少量)时,寻找图像中具有最大变化的区域。这将在未来的章节中被投射到计算机语言中。因此找到这些图像特征称为特征检测。 现在再引入一个名词:特征检测(Feature Detection) We found the features in the images. Once you have found it, you should be able to find the same in the other images. How is this done? We take a region around the feature, we explain it in our own words, like "upper part is blue sky, lower part is region from a building, on that building there is glass etc" and you search for the same area in the other images. Basically, you are describing the feature. Similarly, a computer also should describe the region around the feature so that it can find it in other images. So called description is called Feature Description. Once you have the features and its description, you can find same features in all images and align them, stitch them together or do whatever you want. 一旦我们在图像中找到了这些特征,你就能够在其他图像中找到相同的图像。这是如何完成的?我们在特征周围确定一个区域,用我们自己的话来描述这个区域,比如“上半部分是蓝天,下半部分是建筑物的区域,那个建筑物是玻璃等等”,然后你在另一个图片中搜索相同的区域。基本上,你正在描述的就是这个图像的特征。同样,计算机也应该描述该特征周围的区域,以便它可以在其他图像中找到相同的特征。所谓的描述称为特征描述。一旦你有了这些特性和描述,你就可以在所有图像中找到相同的特征并对齐它们,将它们缝合在一起或做任何你想做的事情。 所以在这个模块中,我们正在寻找OpenCV中的不同算法来查找特征,描述它们,匹配它们等。","categories":[{"name":"图形图像","slug":"图形图像","permalink":"https://stonema.github.io/categories/图形图像/"}],"tags":[{"name":"特征提取","slug":"特征提取","permalink":"https://stonema.github.io/tags/特征提取/"}]},{"title":"图像处理中的滤波filtering和卷积convolution","slug":"图像处理中的滤波filtering和卷积convolution","date":"2018-04-08T06:57:50.000Z","updated":"2020-01-18T01:45:05.204Z","comments":true,"path":"2018/04/08/图像处理中的滤波filtering和卷积convolution/","link":"","permalink":"https://stonema.github.io/2018/04/08/图像处理中的滤波filtering和卷积convolution/","excerpt":"","text":"很多人认为卷积就是滤波,两者并无区别,其实不然。两者在原理上相似,但是在实现的细节上存在一些区别。滤波操作和卷积操作,实际都是用一个矩阵和原矩阵进行对应位置乘积做和。 滤波操作: Filtering滤波操作不改变原图像矩阵的大小 具体以下图为例: 左边是滤波器,右边是原图像,滤波操作就是将滤波器的中心的‘5’放在原图像左上角‘0’的位置,然后依次向右滑动,原图像空缺的位置以‘0’填充,然后对应位置乘积,加和,便得到原图像对应位置的数值:原图像左上角‘0’对应的就是滤波后的图像‘64’。 如果我们想设计一个什么也不做的滤波器,这个滤波器应该是什么样子的? 答案如下: \\left[ \\begin{matrix} 0 & 0 & 0 \\\\ 0 & 1 & 0 \\\\ 0 & 0 & 0 \\end{matrix} \\right]这样的滤波器不会对原图像矩阵产生任何影响 卷积操作: Convolution卷积操作会改变图像大小 具体如下: 卷积操作也是卷积核与图像对应位置的乘积和。但是卷积操作在做乘积之前,需要先将卷积核翻转180度,之后再做乘积。卷积操作的具体过程是卷积核与原图像依次对齐,卷积核左上角的‘1’与原图像左上角的‘0’对齐。这样对应位置相乘求和之后,图像的大小就变小了,得到了最后的2*2的图像。 由于卷积操作会导致图像变小(损失图像边缘),所以为了保证卷积后图像大小与原图一致,经常的一种做法是人为的在卷积操作之前对图像边缘进行填充(填充的数值因实际情况不同而不同)。","categories":[{"name":"图形图像","slug":"图形图像","permalink":"https://stonema.github.io/categories/图形图像/"}],"tags":[{"name":"滤波","slug":"滤波","permalink":"https://stonema.github.io/tags/滤波/"},{"name":"卷积","slug":"卷积","permalink":"https://stonema.github.io/tags/卷积/"}]},{"title":"大话交叉熵","slug":"大话交叉熵","date":"2018-03-27T08:55:03.000Z","updated":"2020-01-18T01:45:05.206Z","comments":true,"path":"2018/03/27/大话交叉熵/","link":"","permalink":"https://stonema.github.io/2018/03/27/大话交叉熵/","excerpt":"","text":"前言:为什么要写这一篇文章呢?大概是因为自己的脑子太笨了吧……以前就问过龙哥这个东西怎么理解,他给了我一套公式: $H(p,q) = -\\sum_xp(x) \\log q(x)$ 瞅了半天,发现以自己的智商并不能从公式中总结出什么门道儿,所以有感而发:”熵”这个字是特么谁发明的……内心的焦灼已经按耐不住了,既然要学,就学个明明白白,刨根问底!文章将陆续更新直至更新不动…… 什么是熵?(Entropy)熵的英文单词为:Entropy。熵是热力学中表征物质状态的参量之一,用符号S表示,其物理意义是体系混乱程度的度量。我们在中学时期学习热力学第二定律, 热力学第二定律(second law of thermodynamics):不可能把热从低温物体传到高温物体而不产生其他影响,或不可能从单一热源取热使之完全转换为有用的功而不产生其他影响。其实我们学到的是删减版的内容,真实的热力学第二定律还有一条: 不可逆热力过程中熵的微增量总是大于零。又称“熵增定律”,表明了在自然过程中,一个孤立系统的总混乱度(即“熵”)不会减小。熵越大,表示系统越无序,熵越小,表示系统越有序。如何理解这句话呢?不妨在仔细揣摩一下前面的第二定律,我们知道热能是不可能完全用来做功的,因为有热散失,这部分散失就表示我们的系统是不可逆的,这就是熵增的过程。也就是说:如果一个系统没有外接干扰,肯定是向整体能量变低的方向运动的,也就是熵值变大。对任何已知孤立的物理系统的演化,热熵只能增加,不能减少。 什么是信息熵?(Information Entropy)信息熵是香农引入到信息论当中的计量单位。用来衡量一个随机变量出现的希望值。香农提出了用信息熵来定量衡量信息的大小,就像热力学中一样,概率越大的事件,信息熵越小;概率越小的事件,信息熵越大。在信息世界,熵越高(大),则能传输越多的信息,熵越低(小),则意味着传输的信息越少。然而这里与热熵相反的是,信息熵只能减少,不能增加。举一个简单的例子来体会在生活中什么是信息熵:比如:你在炒股,正在10支股票中犹豫不决,而这时候我作为某公司内部人员告诉你:第3支股票明天涨停。这时候我说的这句话就有很高的信息熵;再比如:我告诉你太阳明天从东方升起。这句话的信息熵就很低,因为它几乎是概率为1的确定性事件。所以到这里,我们明白了:所谓信息熵高,就是说,我得到某个信息后,事件的不确定性大幅度降低了。 反观“太阳从东边升起” 这句话,从信息论的角度讲,并没有消除任何不确定性,所以它的信息熵几乎为0。信息熵不仅定量衡量了信息的大小,同时也为信息编码提供了理论上的最优值:编码平均长度的理论下界就是信息熵。即:信息熵为数据压缩的极限。 如何计算信息熵?熵的本质是香农信息量$(\\log\\frac{1} {p} )$的期望。 我们以一个例子来说明。 比如说:百米赛跑,有四位选手参加,分别是:$ {A, B, C, D }$,他们获胜的概率分别为:$ { \\frac{1} {2}, \\frac{1} {4}, \\frac{1} {8}, \\frac{1} {8} }$。接下来,我们将“谁能获胜”视为一个随机变量 $X\\in{A,B,C,D}$ 。假定我们需要用尽可能少的二元问题来确定随机变量 X 的取值。 例如: 问题1:A获胜了吗? 问题2:B获胜了吗? 问题3:C获胜了吗? 最后我们可以通过最多3个二元问题,来确定 $X$ 的取值,即谁赢了比赛。 如果 $X=A$ ,那么需要问1次(问题1:是不是$A$?),概率为$ \\frac{1} {2} $; 如果$ X=B$ ,那么需要问2次(问题1:是不是$A$?问题2:是不是$B$?),概率为$ \\frac{1} {4} $; 如果 $X=C$,那么需要问3次(问题1,问题2,问题3),概率为$ \\frac{1} {8} $; 如果 $X=D$ ,那么同样需要问3次(问题1,问题2,问题3),概率为$ \\frac{1} {8} $; 那么很容易计算,在这种问法下,为确定 $ X $ 取值的二元问题数量为: E(N) = \\frac {1} {2}\\cdot 1+\\frac{1} {4}\\cdot2 + \\frac{1} {8} \\cdot 3 + \\frac{1} {8} \\cdot 3 =\\frac {7} {4}那么我们回到信息熵的定义,会发现通过之前的信息熵公式,神奇地得到了: H(X) = \\frac {1} {2}\\log(2) + \\frac{1} {4} \\log(4) + \\frac{1} {8} \\log(8)+ \\frac{1} {8} \\log(8) = \\frac{7} {4} bits 在计算机领域中的应用另一个稍微复杂的例子是假设一个随机变量$X$,取三种可能值$x_1,x_2,x_3$,概率分别为$\\frac{1} {2}$,$\\frac{1} {4}$,$\\frac{1} {4}$,那么编码平均比特长度为:$\\frac{1} {2}\\times1+\\frac{1} {4}\\times2+\\frac{1} {4}\\times2 = \\frac{3} {2} $ 因此信息熵为$\\frac{3} {2}$ 在二进制计算机中,一个比特为0或1,其实就代表了一个二元问题的回答。也就是说,在计算机中,我们给“谁能夺冠” 这个事件进行编码,所需要的平均码长为1.75个比特。 平均码长的定义为: L(C)=\\sum\\limits_{x\\in\\mathcal{X}}p(x)l(x)很显然,为了尽可能减少码长,我们要给发生概率 $p(x)$ 较大的事件,分配较短的码长 $l(x)$ 。这个问题深入讨论,可以得出霍夫曼编码的概念。 因此信息熵实际是对随机变量的比特量和顺次发生概率相乘再总和的数学期望。 什么是交叉熵?交叉熵 : 其用来衡量在给定的真实分布下,使用非真实分布所指定的策略消除系统的不确定性所需要付出的努力的大小。 现有关于样本集的2个概率分布$p$和$q$,其中 $p$ 为真实分布,$q$非真实分布。按照真实分布$p$来衡量识别一个样本的所需要的编码长度的期望(即平均编码长度)为:$H(p)=\\sum{i}^{} p(i)\\log \\frac{1}{p(i)}$。如果使用错误分布$q$来表示来自真实分布$p$的平均编码长度,则应该是:$H(p,q)= \\sum{i} p(i) \\log \\frac{1} {q(i)}$ 。因为用$q$来编码的样本来自分布$p$,所以期望$H(p,q)$中概率是$p(i)$。$H(p,q)$我们称之为“交叉熵”。 标准的交叉熵公式为: \\sum_{k=1}^N p_k \\log_2 \\frac{1}{q_k}其中$ p_k$ 表示真实分布, $q_k$ 表示非真实分布。 举例:真实分布 p_k = (\\frac {1} {2},\\frac {1} {4},\\frac {1} {8},\\frac {1} {8}),非真实分布 q_k = (\\frac {1} {4},\\frac {1} {4},\\frac {1} {4},\\frac {1} {4}),交叉熵为\\frac{1} {2}\\times\\log_2 4 + \\frac{1} {4}\\times \\log_2 4 + \\frac{1} {8} \\times \\log_2 4 + \\frac{1} {8} \\times \\log_2 4 = 2可以看到这个交叉熵是比真实分布 \\sum_{k=1}^N p_k \\log_2 \\frac{1} {p_k}计算得出的信息熵要大一些的。因此,交叉熵越低,这个策略就越好,最低的交叉熵也就是使用了真实分布所计算出来的信息熵,此时 交叉熵 = 信息熵。这也是为什么在机器学习中的分类算法中,我们总是最小化交叉熵,因为交叉熵越低,就证明由算法所产生的策略最接近最优策略,也间接证明我们算法所算出的非真实分布越接近真实分布。 什么是相对熵($KL$散度)?我们如何去衡量不同策略之间的差异呢?这就需要用到相对熵,其用来衡量两个取值为正的函数或概率分布之间的差异即: KL(f(x) || g(x)) = \\sum_{ x \\in X} f(x) * \\log_2 \\frac{f(x)} {g(x)}现在,假设我们想知道 策略$A$ 和 最优策略 之间的差异,我们就可以用相对熵来衡量这两者之间的差异。即:相对熵($KL$散度)= 策略$A$ 的交叉熵 - 信息熵(根据系统真实分布计算而得的信息熵,为最优策略),公式如下: \\begin{equation}\\begin{aligned}KL (p | q) &= H(p,q) - H(p) \\&= \\sum{k=1} ^N p_k \\log_2 \\frac{1} {q_k} - \\sum{k=1} ^N pk \\log_2 \\frac{1} {p_k} \\&= \\sum{k=1}^N p_k \\log_2 \\frac{p_k} {q_k}\\nonumber\\end{aligned}\\end{equation} 机器学习中的交叉熵?H(p,q) = -\\sum_{x}p(x)\\log q(x)从名字上看,$Cross Entropy$(交叉熵)主要是描述两个事件之间的相互关系,事件$A$对自己求交叉熵就等于熵。即:$H(A,A) = S(A)$ 熵的意义是对A事件中的随机变量进行编码所需的最小字节数。 $KL$散度的意义是“额外所需的编码长度”如果我们用B的编码来表示A。 交叉熵指的是当你用B作为密码本来表示A时所需要的“平均的编码长度。机器如何“学习”?机器学习的过程就是希望在训练数据上模型学到的分布 $P(model) $和真实数据的分布 $P(real)$ 越接近越好。那么怎么最小化两个分布之间的不同呢?方法就是:使其KL散度最小!但我们没有真实数据的分布,那么只能退而求其次,希望模型学到的分布和训练数据的分布 $P(training) $尽量相同,也就是把训练数据当做模型和真实数据之间的代理人。假设训练数据是从总体中独立同步分布采样$(Independent $ $and$ $ identically $ $distributed$ $ sampled$ $ )$ 而来,那么我们可以利用最小化训练数据的经验误差来降低模型的泛化误差。简单讲: 最终目的是希望学到的模型的分布和真实分布一致: $P(model) \\simeq P(real )$ 但真实分布是不可知的,我们只好假设 训练数据 是从真实数据中独立同分布采样而来: $P(training) \\simeq P(real )$ 退而求其次,我们希望学到的模型分布至少和训练数据的分布一致 $P(model) \\simeq P(training)$由此非常理想化的看法是如果模型(左)能够学到训练数据(中)的分布,那么应该近似的学到了真实数据(右)的分布:$ P(model) \\simeq P(training) \\simeq P(real)$ 参考文献: 香农信息论. 什么是信息熵. 为什么交叉熵可以用于计算代价.","categories":[{"name":"机器学习","slug":"机器学习","permalink":"https://stonema.github.io/categories/机器学习/"}],"tags":[{"name":"机器学习","slug":"机器学习","permalink":"https://stonema.github.io/tags/机器学习/"},{"name":"基础知识","slug":"基础知识","permalink":"https://stonema.github.io/tags/基础知识/"}]},{"title":"JS循环绑定click事件","slug":"JS事件循环绑定","date":"2018-03-22T06:29:19.000Z","updated":"2020-01-18T01:45:05.163Z","comments":true,"path":"2018/03/22/JS事件循环绑定/","link":"","permalink":"https://stonema.github.io/2018/03/22/JS事件循环绑定/","excerpt":"","text":"遇到的问题:在开发的上一个Annotation的项目的过程中,遇到了需要绑定鼠标事件的问题,如图中所示,模型上有多个热点,可以通过点击热点实现热点说明的展开与关闭。 热点是数据库中动态查询出来的List<annotation>,这样的话,我们在前端就需要循环绑定onclick事件。我在迭代前端的对象的过程中,一度按照自己的想法去写(未使用闭包之前的写法):1234567891011121314151617181920212223242526272829var anno_data;function loadAnnos() { $.ajax({ type: 'post', url: './queryAnnos.do', success: function (data) { if (data != null){ anno_data = data; createExsitAnnos(); } } })}var existAnnos = [];function createExsitAnnos() { for (var j = 0; j < anno_data.length; j++){ var e_div; // 创建anno content的div e_div = document.createElement('div'); document.body.appendChild(e_div); existAnnos.push(e_div); e_div.appendChild(sp); // 这样绑定click方法不行 $(e_div).on('click', function hideExistAnno() { $(e_div).find(\"*\").toggle('fast'); }) }} 导致的结果是对于所有的按钮都没有办法绑定独立的onclick事件,鼠标点击最终响应的是最后加载的那个热点。其实这就是常见的JS面试题里的那些东西,只不过换了个形式……自己就掉沟里了。雷同面试题:下面代码中点击按钮后的内容:123456789101112<button id=\"0\">0</button><button id=\"1\">1</button><button id=\"2\">2</button><script> $(function(){ for (var i=0; i<=2; i++) { $(\"#\" + i).on(\"click\", function() { alert(i); }); };})</script> 这段代码如果不仔细看的话会误以为三个按钮点击结果分别为0,1,2。但是运行结果却是3,3,3。我们来分析一下代码执行过程:前三遍循环分别给按钮0,1,2绑定了alert(i)的事件,第四遍循环开始时i=3,不符合i<=2的条件,因此终止循环。这里要注意的是,前三遍循环绑定的是alert(i)事件,而不是alert(0),alert(1),alert(2),因为在绑定的过程中on的事件处理函数里的代码并没有运行,因此在触发click事件之前并不知道i等于几,代码此时只认为i是一个全局变量(实际上i的作用域为最外层的function)。上面分析了,当循环结束时i等于3,因此3个按钮点击均为alert(3)。 回到上面的代码:我起初认为createExsitAnnos()方法中$(e_div)每次都能获取到当前句柄然后都当前句柄进行事件绑定,但实际并不是这样,在执行:123$(e_div).on('click', function hideExistAnno() { $(e_div).find(\"*\").toggle('fast');}) 这段代码的时候,跟上面的1,2,3的例子一样,是对每一个当前句柄绑定了click事件hideExistAnno(),但是,事件中的具体实现却没有在绑定时执行,也就是说hideExistAnno()这个方法是在用户点击鼠标的时候才会被调用,而前面的整个for循环绑定的过程中,都未执行,只是给句柄绑定了一个方法,具体方法实现是在点击的时候才进行,当鼠标点触发的时候,$(e_div).find("*").toggle('fast');先在实现方法内部寻找变量,发现并没有e_div这个句柄,因此到方法外部寻找句柄,此时的句柄是最后一个for循环的句柄,也就是最后的annotation因此所有的鼠标点击都会绑定到最后一个句柄。这就是没有维护好函数的作用域造成的结果。因此我们引入了闭包的思想。 解决办法:将上面的事件绑定方法改为使用立即调用函数表达式 去编写:123 (function(value){ //代码块 })(i)//这就是立即调用函数表达式 12345(function(e){ $(e).on('click', function hideExistAnno() { $(e).find(\"*\").toggle('fast'); });})(e_div) 这样写就能实现每个annotation的事件绑定,后面括号中的e_div就会以参数e的形式传递到函数体内。 修改后的代码:12345678910111213141516171819202122232425262728293031var anno_data;function loadAnnos() { $.ajax({ type: 'post', url: './queryAnnos.do', success: function (data) { if (data != null){ anno_data = data; createExsitAnnos(); } } })}var existAnnos = [];function createExsitAnnos() { for (var j = 0; j < anno_data.length; j++){ var e_div; // 创建anno content的div e_div = document.createElement('div'); document.body.appendChild(e_div); existAnnos.push(e_div); e_div.appendChild(sp); // 使用立即执行函数绑定 (function(e){ $(e).on('click', function hideExistAnno() { $(e).find(\"*\").toggle('fast'); }); })(e_div) }} 介绍三种解决循环绑定事件的方法:1. 第一种、编写一个function,在这个function中返回一个函数 :其中get(0)指的是将jQuery对象转为DOM对象。1234567891011 $(function(){ for(var i=1;i<=4;i++){ $(\"#btn\"+i).get(0).onclick=btnClick(i); } }); var btnClick=function(value){ return function(){ alert(value); } } 2. 第二种、使用立即调用函数表达式 :123456789 $(function(){ for(var i=1;i<=4;i++){ $(\"#btn\"+i).get(0).onclick=(function(value){ return function(){ alert(value); } })(i); } }); 3. 第三种、使用jQuery的each函数 :1234567 $(function(){ $.each([1,2,3,4],function(index,value){ $(\"#btn\"+value).get(0).onclick=function(){ alert(value); } }); });","categories":[{"name":"JS","slug":"JS","permalink":"https://stonema.github.io/categories/JS/"}],"tags":[{"name":"JS","slug":"JS","permalink":"https://stonema.github.io/tags/JS/"},{"name":"前端","slug":"前端","permalink":"https://stonema.github.io/tags/前端/"}]},{"title":"ThreeJS中的Raycaster应用","slug":"ThreeJS中的Raycaster应用","date":"2018-03-20T02:06:16.000Z","updated":"2020-01-18T01:45:05.180Z","comments":true,"path":"2018/03/20/ThreeJS中的Raycaster应用/","link":"","permalink":"https://stonema.github.io/2018/03/20/ThreeJS中的Raycaster应用/","excerpt":"","text":"总结Three.js中的Raycaster的使用经验先上图: 图中的热点查询自数据库,同时鼠标点击可以添加热点 功能简介:这个项目是按照‘党’的需求做的,党给提出的需求是: 1. 能够加载三维模型。 2. 能切换三维模型。 3. 能在模型上点击添加热点annotation。 4. 热点上可以修改标注信息。 5. 鼠标可以操作模型,包括放缩和旋转平移。 6. 能够将标注信息存到数据库,每次加载模型时不需要重新标注。 按照上面的需求,我做的过程中,遇到的主要的难点就在模型的annotation添加和保存后再加载这块了,因为之前在Sketchfab.com网站上看到过类似的功能,(PS:老外的技术还是领先的)用起来非常的丝滑,柔顺,便利。这简单的几个功能,不知道是多少工程师的心血,但是可以肯定的是,他们能实现的,我们肯定也能实现。此处省去N多日夜……最终,如愿以偿,虽然代码写得有些冗余,但是还是基本实现了想要的功能。 Raycaster是什么?Raycaster是Three.js中的射线,通过它,可以计算出射线与物体的交点,从而进行模型拾取,或者选中,等其他功能。我要实现的是鼠标双击模型上的某一点,然后在这个点处添加一个annotation,然后在鼠标推拽物体,旋转物体的时候让annotation随着模型一起运动。 思路: 鼠标双击是要选中模型上的点的,鼠标是屏幕上的点,是二维坐标,但是模型上的点是三维坐标,如何将这个坐标进行转换呢? 鼠标双击事件给annotaion添加title和content。 annotation跟随模型运动,应该是实时渲染的结果。解决方法与实现过程:1. 加载模型:1234567891011121314151617181920212223objLoader = new THREE.OBJLoader();objLoader.setPath('./obj/');objLoader.load('zxj.obj', function (object) { object.traverse(function (child) { if (child.type === \"Mesh\") { child.geometry.computeBoundingBox(); child.geometry.verticesNeedUpdate = true; child.material.side = THREE.DoubleSide; } }); object.name = \"zxj\"; //设置模型的名称 object.position.x = -100;//载入模型的时候的位置 object.position.y = 0; object.position.z = -500; // 对模型的大小进行调整 object.scale.x = 0.001; object.scale.y = 0.001; object.scale.z = 0.001; //写入场景内 scene.add(object); objects.push(object);//仿照ThreeJS写法}); 2.如何才能选中鼠标点击的点?在Three.js中还是能找到一些例子的:Three.js需要注意的地方:鼠标双击进行事件绑定,要监听鼠标双击事件,这里切记不要使用全局鼠标事件监听,而是应该一个一个进行绑定,否者会引起html标签中的鼠标事件失效。12$('#test').click(onDocumentMouseDown);//document.addEventListener('mousedown', onDocumentMouseDown, false); // 这里是个坑啊,一定要注意 鼠标单击事件如果绑定给全局document,其他需要鼠标单击的控件,全部失效 鼠标双击事件:鼠标获取的点是屏幕坐标,因屏幕大小而定,这里要改成设备坐标系,也就是把屏幕坐标转换成从-1到1的标准坐标系,然后使用Camera位置和Mouse点击的点来定位射线方向,通过判断射线是否经过模型来实现在模型上添加点。我们每次鼠标双击事件就是添加一个热点,所以要将热点存储到一个数组中rays[];123456789101112131415161718192021222324252627282930var rays = []; //记录多次双击之后的射线与模型的外表面交点var annos = new Array(); //热点定义成数组var intersects; // 射线与模型的交点,这里交点会是多个,因为射线是穿过模型的,与模型的所有mesh都会有交点,但我们选取第一个,也就是intersects[0]。function ondblClick(event) { var clientX; var clientY; event.preventDefault(); //获取屏幕坐标,鼠标点击的位置的坐标 clientX = event.clientX; clientY = event.clientY; // 获取'转换后的设备坐标(即屏幕标准化后的坐标从 -1 到 +1): mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1; //获取鼠标点击的位置的坐标 mouse.y = - (event.clientY / renderer.domElement.clientHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera);//从相机发射一条射线,经过鼠标点击位置 mouse为鼠标的二维设备坐标,camera射线起点处的相机 // intersects 是后者的返回对象数组, intersects = raycaster.intersectObjects(objects[0].children, false); //intersects是交点数组,包含射线与mesh对象相交的所有交点/* 如果射线与模型之间有交点,才执行如下操作 */if (intersects.length > 0) { if (annos.length > 20){ alert(\"到达热点上限\"); return; } else { $('#myModal').modal(); //调用模态框 //加载模态框 rays.push(intersects[0]); //将射线与模型的第一个交点push到rays数组中. }}} 这样,就可以把选出来的点放入到rays[]中了。如何给这些点添加样式呢?我们需要有多少个rays交点创建多少个annotation:创建结点,append到页面,并push到annos数组中。1234567891011121314var div; // 创建anno content的divvar sp;var strong;var p;var num; // annotation的数量sp = document.createElement('p');strong = document.createElement('strong');p = document.createElement('p');strong.type = 'text';div = document.createElement('div');div.className = 'annos';div.style.background = 'rgba(0, 0, 0, 0.8)';document.body.appendChild(div);annos.push(div); 3.我们使用Ajax向后台传值,把坐标存起来123456789101112131415161718192021222324252627282930$.ajax({ type: 'POST', url: './addAnnos.do', data:{ // 'id' : num, //回头这个地方的id可以都去掉,让数据库的id自增长 'id' : $('#recipient-id').val(), 'title' : $('#recipient-name').val(), 'content': $('#message-text').val(), 'x_point': x_point, 'y_point': y_point, 'z_point': z_point }, // 考虑一下,如何把当前div对应的交点的下x,y,z值一起传过去 success: function(data){ $(div).attr('data-attr', $('#recipient-id').val());//操作伪dom中的内容,但是不能使用类选择器 $(div).on('click', function hideAnno() { $(div).find(\"*\").toggle('fast'); if (div.style.background != '') { div.style.background = ''; } else { div.style.background = 'rgba(0, 0, 0, 0.8)'; } }); // var v = window.getComputedStyle(div,'::before').getPropertyValue('content'); // annos[i] 初始化结束后进行赋值 $('#recipient-id').val(\"\"); $('#recipient-name').val(\"\"); $('#message-text').val(\"\"); }}); css页面:注意伪dom结点的处理123456789101112131415161718192021222324252627282930.annos { position: absolute; top: 0; left: 0; z-index: 1; margin-left: 15px; margin-top: 15px; padding: 1em; width: 200px; color: #fff; border-radius: .5em; font-size: 12px; line-height: 1.2; -webkit-transition: opacity .5s; transition: opacity .5s;}.annos::before { content: attr(data-attr); position: absolute; top: -30px; left: -30px; width: 30px; height: 30px; border: 2px solid #fff; border-radius: 50%; font-size: 16px; line-height: 30px; text-align: center; background: rgba(0, 0, 0, 0.8);} 4.更新屏幕中annotation的位置:annotation的位置是实时渲染的,所以这个方法是对所有的annotation进行位置更新,最终应该放在render渲染器中。123456789101112131415161718192021222324 function updateAnnosPosition() { for (var i = 0; i < annos.length; i++){ var canvas = renderer.domElement; //var vector = new THREE.Vector3(clientX,clientY,-1); // var vector = new THREE.Vector3(intersects[i].point.x, intersects[i].point.y, intersects[i].point.z); var vector = new THREE.Vector3(rays[i].point.x, rays[i].point.y, rays[i].point.z); vector.project(camera); //这个位置的写法有问题 vector.x = Math.round((0.5 + vector.x / 2) * (canvas.width / window.devicePixelRatio)); // 控制annotation跟随物体一起旋转 vector.y = Math.round((0.5 - vector.y / 2) * (canvas.height / window.devicePixelRatio)); annos[i].style.left = vector.x + \"px\"; annos[i].style.top = vector.y + \"px\"; annos[i].style.opacity = spriteBehindObject ? 0.25 : 1; } } //渲染器 function render() { renderer.render(scene, camera); updateAnnotationOpacity(); // 修改注解的透明度 //updateScreenPosition(); // 修改注解的屏幕位置 if (annos != null){ updateAnnosPosition(); }} 这就是整个的实现过程了,而后就是将数据存入数据库,并同样将数据库中的数据取出来重新放入一个数组 ,再将对应的x,y,z坐标复原到一个annotation中。利用render进行渲染.","categories":[{"name":"前端","slug":"前端","permalink":"https://stonema.github.io/categories/前端/"}],"tags":[{"name":"Three.js","slug":"Three-js","permalink":"https://stonema.github.io/tags/Three-js/"}]},{"title":"前端压缩","slug":"前端压缩","date":"2018-03-10T12:40:51.000Z","updated":"2020-01-18T01:45:05.204Z","comments":true,"path":"2018/03/10/前端压缩/","link":"","permalink":"https://stonema.github.io/2018/03/10/前端压缩/","excerpt":"","text":"基于JSZip的前端文件压缩1. 简介:这段时间,项目需要做一个这样的功能:客户端在上传文件的时候(具体文件类型),需要对文件进行压缩再上传以节省带宽和服务器端资源,完成这个功能,我们选择了GitHub上的JSZip,它是一个客户端插件,可以提供客户端压缩功能,作者给出了API,但是实际使用过程中还是有很多问题,下面是实际过程中遇到的各类问题,直至最终完成整个文件压缩再上传至后台。 2.项目相关组件与环境:前端node.js + webpack 作前后台分离,后端java 3. 具体实现主要js代码:1234567891011121314151617var JSZip = require('jszip');const components = require('components');$('#confirmBtn').on('click', async function() { // 绑定上传的确认按钮,获取到obj等模型文件,并压缩 let zip = new JSZip();//声明并创建JSZip对象 var fileBox = $('#fileUploadInput'); //从页面获取到需要上传的文件列表,当然html是一个多文件上传 var fileList = fileBox[0].files; var objName = 'example'; // 这里定义一个压缩文件的名字,以供后台使用,当然也可以动态获取 // var flag = false; for (const fileObject of fileList) { zip = await zipFileAsync(zip, fileObject); //这是设置异步上传,await关键字使得后面的zipFileAsync方法执行结束后才将对象返回给zip变量 } sendFileAsync(zip, objName); console.log(zip); return false; // 设置return false防止表单提交}); 这部分代码就是异步压缩的核心,以及如何调用的下面的异步压缩算法,上面需要Async与await关键字缺一不可,一开始也尝试过使用同步压缩的方式,但是会出现压缩还没有全部完成,就已经开始提交文件的现象,特别感谢lrh3321的指导,才完整的实现了这个功能。 因为异步压缩的时候我们上传的文件的数量不定,所以上面需要使用await关键字来修饰压缩过程。而下面的压缩过程的实现,最终返回一个promise对象,当压缩过程已完成后,完整的生成的文件存于其中。123456789101112131415161718192021222324/** * 异步压缩文件 * @param zip file */function zipFileAsync(zip, file) { const promise = new Promise((resolve) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onloadend = function(e) { var result = reader.result; // 读完转一下格式 result = convertBase64UrlToBlob(result); console.log(zip); console.log(file.name); console.log(file.size); // resolve 方法保证异步压缩完成后才返回promise resolve( zip.file(file.name, result, { type: 'blob', })); }; }); return promise;} 这里的demo请查看JSZip给的例子1234567891011121314151617181920212223242526/** * 异步发送文件 * @param zip file */function sendFileAsync(zip, objName) { zip.generateAsync({ type: 'blob', compression: 'DEFLATE', // force a compression for this file compressionOptions: { // 使用压缩等级,1-9级,1级压缩比最低,9级压缩比最高 level: 6, }, }).then( function(content) { var formData = new FormData(); formData.append('Blobfile', content); // 获取上文中压缩的内容,并放入formdata formData.append('objName', objName); // 将objName一起放入formdata progressBar('Model/UploadModel', formData, content.size); } ); return false;}/** * 将以base64的图片url数据转换为Blob * @param urlData * 用url方式表示的base64图片数据 */ http读取图片的时候会已base64编码形式对到服务器,如果不进行重新编码,则无法在图片查看器中查看图片 12345678910function convertBase64UrlToBlob(urlData) { var bytes = window.atob(urlData.split(',')[1]); // 处理异常,将ascii码小于0的转换为大于0 var ab = new ArrayBuffer(bytes.length); var ia = new Uint8Array(ab); for (var i = 0; i < bytes.length; i++) { ia[i] = bytes.charCodeAt(i); } return new Blob([ab], {type: 'image/jpg'});}","categories":[{"name":"JS","slug":"JS","permalink":"https://stonema.github.io/categories/JS/"}],"tags":[{"name":"JSZip","slug":"JSZip","permalink":"https://stonema.github.io/tags/JSZip/"},{"name":"前端压缩","slug":"前端压缩","permalink":"https://stonema.github.io/tags/前端压缩/"}]},{"title":"使用Git Pages 搭建可多终端编辑的个人博客","slug":"使用Git-Pages-搭建可多终端编辑的个人博客","date":"2018-03-09T08:57:57.000Z","updated":"2020-01-18T01:45:05.202Z","comments":true,"path":"2018/03/09/使用Git-Pages-搭建可多终端编辑的个人博客/","link":"","permalink":"https://stonema.github.io/2018/03/09/使用Git-Pages-搭建可多终端编辑的个人博客/","excerpt":"","text":"使用Git Pages 搭建可多终端编辑的个人博客[TOC]Hexo作为一个优秀的静态博客框架,与传统的博客网站不同的是,Hexo的博客源文件是保存在本地的,Hexo是博客生成工具,将博客源文件,与页面展示分离,分开管理,使用Markdown语法编辑博客,然后使用Hexo 提供的 hexo generate 和 hexo deploy 命令将markdown文件生成相应的html文件。 并不想在自己电脑上搭建服务 也不想去购买云服务 也不想用一些博客网站提供的个人博客功能,一来广告多,二来受人制约所以最好的办法是使用GitHub提供的GitHub-Pages服务来免费搭建自己的博客。1. 提出问题查看文件目录: 我们通过Hexo deploy 将文件发布到GitHub-Pages的时候[此处实现方式省略],会将public目录(HTML源文件)自动push到远程仓库的master分支。但是这个对于多终端博客同步来说没有任何意义,因为我们每次进行Hexo generate 的时候,都会根据source目录下的markdown源文件重新生成HTML文件,所以当我们在当前目录使用git同步的时候,master分支下会出现博客目录下的所有文件,而我们真正应该关心的是source目录下的markdown源文件,要维护的也是这部分文件的同步。所以问题的关键是:如何同步source目录下的源文件,以及配置文件_config.yml, scanffolds目录,themes目录。 生成HTML页的目录: 2.解决问题首先在GitHub上创建一个同名的远程库,例如上图中我的repo名为:[username].github.io,使用这个库的master分支来管理public目录下的内容,(实际是.deploy_git中的内容,也就是说博客根目录git 初始化完毕后,里面的内容不需要使用git push 来提交到master分支,因为hexo d这条指令已经包含了相关的操作,>具体请百度或google查看hexo github 配置,而且如果在本地博客根目录下使用git pull 会把master分支下的public目录中的内容都拉到本地,导致文件错乱)然后另新建一个名为source的分支来管理博客根目录下的无需发布,但需要多终端配置的内容(包括source文件夹下的.md文件.gitignore文件中的内容说明的是当执行git push的时候忽略的文件,其他文件都要上传到source分支下进行同步。)到这里,解决问题的思路就清晰了: master分支来管理发布的博客内容 source分支来管理Hexo博客的工作空间3.具体步骤: 在blog根目录初始化仓库,并切换到source分支,关联远程仓库,并push到远程仓库的source分支,这里会忽略 ignore掉的文件夹。1234567cd bloggit init //在当前目录下初始化git仓库git checkout -b source //新建一个source分支,并切换到该分之下git add . //添加blog目录下的所有文件夹(.gitignore声明的文件除外)git commit -m 'write some message'git remote add origin [email protected]:username/username.github.io.git //如果已经配置了源,这一条可以忽略git push origin source //将本地更新push到远程库 至此,远程库中source分支下,已经有了我们本地的Hexo配置以及blog的源文件:.md文件 将远程内容同步到另外一台机器 B主机在另外一台电脑上,先把node环境配好,安装hexo。同时找个位置建立一个Blog的根目录文件夹。1npm install hexo-cli -g 安装好Hexo后,注意不要进行初始化,因为这样又会生成Hexo的新的配置文件,而我们所需的内容都在source分支上管理,所以这里直接进行如下操作:123git clone -b source [email protected]:username/username.github.io.gitcd username.github.io // packeage.json在clone下来的项目目录中npm install //根据package.json来下载依赖包,这也是为什么我们不需要同步node_modules中的内容的原因! 这时我们已经有了远程的.md文件,也就是博客的源文件,到这里,我们A端,git服务器端,以及B端,已经有相同的工作空间与blog源文件内容了。(B端还没有Hexo g,所以没有public目录,没有Hexo d所以没有deploy文件,) 我们可以开始在B终端写博客了 123456hexo new \"about hexo sync\"hexo generatehexo deploygit add .git commit -m \"add blog\"git push origin source 记住:以后git push的时候都要在source分支下,并且向source分支push,不需要在master分支做任何操作)。 B端上传博客并发布后,回到A端,同样再到source分支下进行同步。 12git checkout sourcegit pull 然后在A端同样写博客: 123456hexo new \"about hexo sync again\"hexo generatehexo deploygit add .git commit -m \"add blog\"git push origin source 查看个人主页就能看到相应的变化,然后A端新增的内容同样再回到B端在source分之下进行git pull。 至此多终端同步工作完美实现。","categories":[{"name":"技术","slug":"技术","permalink":"https://stonema.github.io/categories/技术/"}],"tags":[{"name":"GitPages","slug":"GitPages","permalink":"https://stonema.github.io/tags/GitPages/"},{"name":"Hexo","slug":"Hexo","permalink":"https://stonema.github.io/tags/Hexo/"}]},{"title":"2018再出发!","slug":"2018第一天","date":"2018-02-25T16:00:00.000Z","updated":"2020-01-18T01:45:05.152Z","comments":true,"path":"2018/02/26/2018第一天/","link":"","permalink":"https://stonema.github.io/2018/02/26/2018第一天/","excerpt":"","text":"2018年要出点成绩了……请了两天假年后工作第一天被公司和导师的任务压得喘不过气来……希望今年能有所收获,能有较大的进步! 18年计划: 深入三维检索领域,发一篇高水平的论文。 点全WEB开发技能点 找个稳定的工作 减肥20斤加油!珍惜时间,加油!","categories":[{"name":"随笔","slug":"随笔","permalink":"https://stonema.github.io/categories/随笔/"}],"tags":[{"name":"2018","slug":"2018","permalink":"https://stonema.github.io/tags/2018/"},{"name":"随笔","slug":"随笔","permalink":"https://stonema.github.io/tags/随笔/"}]}]}