精华内容
下载资源
问答
  • 如何确定下加光
    千次阅读
    2021-04-18 14:29:54

    在振动信号采集分析中,我们常常使用加速度传感器,是因为加速度传感器具有可靠性高,可测频带宽,结构小巧,抗干扰能力强等优点。但是有些时候,必需要得到速度和位移信号,这个时候就出现了如何通过对加速度积分得到速度和位移的基础问题,这个问题难倒了不少相关人员。在国内的各种教科书上也很少提及,也不知道是为什么。

    不管是线性系统还是非线性系统,所产生的信号,在时域满足如下微分方程

    其解可以写成

    其中v0是积分常数,积分下限t=0表示采样起始时刻,是人为指定的时间轴原点。如果振动没有交流分量,则加速度是常数,可以得到v(t)=v0+at 。对速度积分得到位移的处理类似,所以有

    其中d0是积分常数,由于v0和d0通常是未知的,加速度积分得到速度和位移,就存在积分常数问题。

    积分常数问题是非常麻烦的问题。首先可以看到,对于任意δv,得到

    都对应同样的加速度a(t),也就是说v0和d0取任意值都对应于同一个测量的加速度和测量的速度,所以要想仅仅凭目前的信息通过积分获取速度和位移,是不可能的,不管是数值积分还是模拟电路积分都不行。本文主要讨论数值积分的问题,关于模拟电路积分存在的问题,推荐大家阅读《压电加速度计和振动前置放大电器》一书,结论就是在高频,放大器增益会下降,不是理想放大器;在甚低频,会受到时间常数或者回路电容限制,也不够精确。这个回复大家可能不满意,坊间不是有各种FFT,各种去趋势项,各种滤波的操作嘛?有人发表在《力学学报》上的论文不也是用模拟积分电路积分了么?我的建议是,谨慎看待那些论文中的说法,你应该有自己的思考和判断。

    实际工作中,在无穷多个v0的选择中,可以选择一个最靠谱的数值凑合用一下,因为在这些选项中,可确定最靠谱的那个值一定在某个有限幅值的区间[va,vb]以内。所以,需要增加对被测试物体的状态进行判断,基于这种判断,做出假设,基于这种假设列出约束条件,根据约束条件找到一个方程,然后求出那个看起来好像最靠谱的积分常数。如果假设成立,就得到了一个很Nice的估计;如果假设不成立,误差很大,那就是自娱自乐,你高兴就好。

    从可观测性角度看这个问题

    下面的讨论限于线性系统,是最近关注某个问题的时候顺便做的笔记。

    设振动方程为

    则,对应的状态空间模型为

    可以得到离散以后的状态方程

    可观性的定义:假定A,B,C和D已知,从已经观测的加速度和载荷向量,能否计算得到位移和速度?从式的迭代解可以知道,只需要设法测试得到x(1)即可知道所有后续的状态x(k),k=2,3,4,…。

    现在,考虑如下序列

    写为矩阵形式

    注意到对SDOF模型,上面的矩阵方程始终是一个p×(p+2)矩阵,属于欠估计问题,不可能通过测试加速度同时将载荷和初始状态x(1)求出来。现将该矩阵分割为

    其中Op就是可观测矩阵,可以求得

    也就是说要联合测试加速度响应,知道可观测矩阵Op、系统矩阵E和载荷才能识别得到所有状态变量,而且可观测矩阵不能奇异,必需是满秩的。2015年Computers &Structures上的一篇关于载荷识别的论文,就是依据对式(8)的简化,假定初始状态x(1)=0,得到

    这种零初始条件的假定,在实际问题的应用中,是一厢情愿的。在数据采集器采样了一个数据块处理以后,不能再假定这个数据块采集终了时候的状态变量还是零,所以,至少在后续的计算中,需要考虑 “过去的激励引起的未来的响应的问题”或者x(1)不为0的问题。

    同样的道理,实际情况下,我们并没有测试激励力ḟ,还没有做实验获取系统参数,更不能保证传感器布置点所对应的观测矩阵是Regular的,所以单独想要用加速度响应,就完全构造出速度和位移是不可能实现的。就算你得到了,误差可能在80%左右,正确的是量纲。

    现在的问题是,我们得到了加速度数据,到底应该考虑哪些实际问题,有没有办法得到一个不是很坏的结果?答案是肯定的,只是这些要求太高了或者假设成立的可能性微乎其乎,所以得到的数据的误差通常较大。

    可能的假设

    J.S Bendat和A.G. Piersol对信号划分为确定性的和随机的,一个信号是否是确定性的还是随机的,可能存在争议,短时间可能是确定性的,而长时间就变得随机了,或者长时间以后就出现其他“开关条件”触发了不可预测的因素。对确定的信号和随机信号,又分为了如下几种:

    在确定性信号中,周期信号是稀有的;在随机信号分类中,各态历经的平稳信号是稀有的。最常见的信号是非周期信号和非平稳信号,是实际中遇到的高概率事件。

    所以,下面讨论一些“低概率”出现的假设和“高概率误操作”。

    3.1 假设1:位移是零均值的信号

    被测试结构没有发生明显的位移现象,所以假定是零均值的。然而,我们知道,有限时间采样以后,得到的是离散数据,这些离散数据的均值并不能严格等于采样区间连续信号的真实均值。而且采样区间真实的均值,极有可能是非零均值的,只是均值很小而已。对速度信号,进行数值积分获取零均值位移信号的方法,同下面的对加速度数值积分获取速度的方法。

    3.2 假设2:速度是零均值的信号

    设当加速度不为常数时,采样间隔时间dt足够小,一种离散的数值积分为梯形公式

    v0任意假定。零均值的获取就是去掉趋势项,得到

    上面的式(10)是一种程序语言,代表将有偏估计的速度信号(组成的矩阵),每个元素去除整体上的已知均值,这样修正得到的v就是零均值的。去掉趋势项的操作,可能存在各种优雅的处理,比如Matlab里面的detrend命令等等,这里不再涉及。

    值得一提的是,如果去趋势项的运算是含有非线性因素,那么可能就存在频率混叠的困难,因为采样频率是一定的,这些非线性运算,比如带入平方,开平方根,矩阵求逆,或者取符号运算等等非线性的操作,会引入带宽以上的频率分量,而这可能不是希望的结果,需要后续修正,最好的处理是先保证足够的带宽,最后做无相移的低通滤波。K. Worden教授对去趋势项的研究结论认为,那些“优雅的”去趋势项操作引入的误差很难处理。有趣的是,他早年在MSSP上发表的论文认为,数值积分方案是很Nice的选择;几年以后在他自己的书里对这个问题又做了更细致的论证和分析,他认为数值微分方案才是更好的。

    3.3 假设3:速度是周期信号

    这种情况,对加速度FFT,在频域积分,再IFFT回来就可以了。

    但是我们要知道两点潜在的问题:加速度信号,从时域图上看,一定要是周期的或者Almost周期的,所以分辨率一定要选择得合理才能做到整周期采样;如果频率分辨率选择不合理,可能存在很严重的泄露,加窗号称可以减小泄露,但是,加窗通常对积分以后的速度信号是毁灭性的,加窗的效应保留在时域速度信号里面,无法修正回来;如果有本事做到不加窗,对数据的分辨率选择就提出了很高的要求。如果不是周期信号,怎么选择分辨率都没用啊。

    图1 非周期信号+加窗的FFT积分再IFFT结果,以及和精确解(蓝色)对比

    3.4 假设4:速度为零时,加速度最大

    这个假设,是本人偶然想到的,是在研究某算法时,看到某非线性微分方程强迫响应,然后突然想到,假定速度为零时,对应于动能最小,然后惯性力最大,产生的加速度也是最大的,就这样得到一个结果,在那个例子中,需要在众多的加速度峰值中选择一个,然后确定积分常数,确实产生了较好的效果,但是这个实在太巧合了,再次按照这个方法选择其他峰值做一遍,失效了,现实中是没有这么理想的假设的。

    图2 速度(红色)和加速度(蓝色)时域曲线对比

    当时的做法是,在某一个明显的加速度峰值附近,选取几个离散加速度点;用多项式进行拟合,得到加速度关于时间的表达式,这一步可以表达为

    其中下标k=1,2,3...s只是代表选择的邻近的有限个数据点。

    假定系统的阻尼很小,在最大惯性力和最大弹性恢复力平衡时粘性阻尼力为零,这个假设在某些时候是成立的,那么加速度最大的时候,速度可以为零。令上面的表达式为零,选择离散数据中加速度最大的时刻tmax,解方程找到加速度真正的峰值时刻tp,tp非常靠近tmax,即

    对加速度的表达式积分得到速度的表达式,积分常数v0待求

    tp时刻的速度为零,从而解出积分常数v0

    在这个区间的速度表达式就已知了,回到tmax时刻,可以直接得到这个时刻下的速度,应该是一个非常小的量

    这样,我们终于找到了“估计的速度小量”,可以根据上面的离散公式,继续完成剩下的时间积分。

    因为这个方法并不总是成立,失效以后的某个结果如下所示。

    图3 梯形数值积分结果和参考结果(红色)对比,存在一个整体偏移量δv

    一个数值积分案例

    在网上荡到了一个96年Steven D. Glaser等人在(Preliminary Processing of the Lotung LSST Data报告中,对一个测试到的加速度数据进行数值积分的案例,附录中有源代码,这个数据是地震加速度信号,频率特别低,把源代码贴在这里,给感兴趣的人:

    function[a,v,d]=vd(f,Lcut,dT,pre,name)

    % function[a,v,d]=vd(f,Lcut,dT,pre,name)

    % detrends andfilters the input acc and integrates twice to give velo and disp

    % f is the inputacc vector

    % Lcut is thelow-end cutoff frequence in Hz

    % dT is the timestep

    % n is the orderof the Butterworth filter

    % pre is thepre-event segment length to be zeroed

    zip=zeros(size(zip));

    [b,c]=butter(n,Lcut*dT*2,'high');

    a=filtfilt(b,c,f);

    a=dtrend(a(:,1),1,pre);%%mxl:这个函数已经过时了.

    a=detrend(a);

    a=a-a(pre+1);

    a(1:pre)=zip;

    v=dtrend(v(1,:)',1,pre);%%mxl:这个函数已经过时了

    v=v-v(pre+1);%整体平移量

    v(1:pre)=zip;%前pre个数值设置为0

    d=inttrap(v,dT);%%mxl:这个是数值积分,这个函数过时了

    [b,c]=butter(n,Lcut*dT*2,'high');

    d=filtfilt(b,c,d);

    d=dtrend(d(:,1),1,pre);

    d=detrend(d); %去趋势项

    d=d-d(pre+1); %整体平移量

    d(1:pre)=zip; %前pre个数值设置为0

    可以看到,他们首先使用了低通滤波,做了去趋势项,然后整体平移了一个量,再将前pre个数值人为置为零。

    终极建议

    如果同时测试了加速度和位移,那么就特别好办了。所以,如果有条件,尽可能测试加速度和位移。

    设在某个区间 [t1,t2] 里面,加速度和位移都可以拟合一个表达式(expression)

    在两次积分的情况下可以得到时间常数的表达式,从而解出未知系数

    可见速度也同时得到了。

    测试位移+测试加速度,可以通过数值微分+低通滤波得到速度;当然也可以直接通过Kalman滤波得到速度,方便、快捷、高效又稳健;或者带有微分算法的低通滤波得到速度,等等。方法总是有很多种。但并不建议测试位移,两次数值微分得到加速度,因为数值微分算法是高通滤波器,会引入高频噪声,采样频率越高,高频噪声问题越严重,两次数值微分得到的加速度误差较大,但也并不是完全不能使用,加上低通滤波可以降低这个误差,总比从加速度积分得到位移要靠谱得多。使用低通滤波算法要注意不要引入相移,平滑处理(Smooth)也是一种低通滤波,效果还行。下面是把不同点数差分算法看成是高通滤波器,然后计算其频率响应得到的曲线,注意是对采样频率fs做了归一化以后的结果。结论是:用5点差分算法是可行的,向前差分或者Matlab的diff函数,最糟糕。

    图4 不同差分格式的频响特性

    5.1 部分数值实验结果

    下面贴出一个位移传感器在低频范围内振动,并做了相对位移和相对加速度采集的结果,从而依据这些测试结果进行数据后处理。

    图5 diff会引入高频噪声;加低通滤波能消除这个噪声;smooth是移动平均,也能消除这个噪声

    图6 最小二乘法可能计算不准初速度(m/s),微小的差别最终导致位移无法复现,如图7所示

    图7 两种方法计算得到速度再梯形积分得到的位移(mm)对比,红色为测试位移结果黑色和红色几乎完美重合,蓝色为上面least+低通滤波的结果

    图8 filter函数会引入相移,而filtfilt函数可以消除相移

    图9 最小二乘法初始速度计算不为零,造成了位移恢复结果累积误差较大,产生overshoot现象,精度不及diff+滤波结果

    图10 恢复位移和测试结果比较,黑色为diff恢复结果,蓝色为最小二乘加低通滤波结果,最小二乘法可能产生underestimate现象

    图11用速度恢复位移和测试结果比较,黑色为diff+fir低通滤波结果,蓝色为diff+移动平均结果,说明diff+移动平均结果也不错

    图12 最小二乘法与diff法得到的加速度对比,低通截止0.05

    图13 最小二乘法与diff法得到的加速度对比,低通截止0.5

    提高低通截止,最小二乘法得到的加速度结果明显优于diff,说明这个时候差分的噪声很难消除。

    上面的实验部分实验结果,主要集中在低频,大多数位移测试的也是在低频,少部分如激光测振才能达到较高频率。

    有人可能说会有更好的方案,所以,可以求解一些线性或者非线性ODE,得到精确的位移、速度和加速度数据,可以试试对这些加速度进行积分,你可以试试数值积分或者模拟电路积分,并和标准速度和标准位移对比一下,考验一下你的算法的精度。

    作者简介

    牟小龙,北京理工大学机械与车辆学院博士研究生,擅长线性有限元分析和系统阻抗匹配。

    觉得不错,请点赞!

    扩展阅读

    更多相关内容
  • 光纤光栅速度传感器是利用光纤光栅的应变传感机理来实现速度的测量,并用的波长变化测量速度值,用光纤来传输传感信号,集测量、传输于一体,因而具有强抗电磁干扰能力. 1 光纤光栅的应变传感机理 根据光纤...
  • 该图从上往为格雷码递增级数,每一级为一幅编码投影图案的黑白分布图样,从左至右,编码值从“0000000”按照格雷码编码依次递增至“1000000”。 投影图案设计 采用与投影图案 μ、ν 轴两个方向相同的两个系列...

    格雷码+相移法既可以减少格雷码的编码位数,加快解码速度,也可以弥补单纯的相移法和格雷码法的对不连续位置难以重建的缺点。

    操作过程如下:

    采用格雷码与相移结合的时间编码方法,具体的编码方法为:首先向被测物投射一系列格雷码黑白条纹图案,其中具有相同编码
    的区域作为一个编码周期,然后再采用四步相移法,依次投射四幅相移图案,使得每个编码区域被进一步连续细分。但投射的格雷码图案和相移图案必须满足如下关系:格雷码图案的最小周期为相移图案周期的 4 倍,理论上格雷码周期边界与相移周期边界要严格对应。

    格雷码的编码算法为:

    1,由已有的前面的格雷码生成后面的格雷码

    假设已经生成了k位格雷码,那么k+1位格雷码的生成方式为

    (1) 按序在k位格雷码前插入一位0,生成一组编码

    (2)按逆序在k位格雷码前插入一位1,生成另外一组编码

    (3)两组编码合起来就是k+1位格雷码。

    如下例:

    已有2位格雷码:00, 01, 11, 10,要生成3位格雷码,采用此算法:

    (1)按序在各码前插入0,生成 000,001, 011,010;

    (2)按逆序在各码前插入1,生成 110,111, 101,100;

    (3)将两组编码组合起来:000, 001, 011, 010, 110, 111, 101, 100,为3位格雷码。

    另外一种算法与此算法类似,不同的是插入的位是在格雷码的后面:

    对于k位格雷码,在各格雷码后面分别插入0, 1 或 1, 0,生成两个编码,所有插入完成后组合起来的编码为k+1位格雷码。

    如已有2位格雷码:00,01,11,10,生成3位格雷码,采用此算法:

    (1)在00编码后面分别插入0,1,生成 000, 001;

    (2)在01编码后面分别插入1,0,生成 011, 010;

    (3)在11编码后面分别插入0,1,生成 110, 111;

    (4)在10编码后面分别插入1,0,生成 101,100;

    (5)将生成的编码组合起来:000, 001, 011, 010, 110, 111, 101, 100,为3位格雷码。

    对格雷码编码测试代码如下(有详细的注释):

    #include <iostream>
    #include <vector>
    #include <string>
    #include <time.h>
     
    void GrayCodeOne(int num);
    void GrayCodeTwo(int num);
     
    using namespace std;
     
    int main()
    {
    	int count;
    	cout << "Input Code Number:";  
    	cin >> count;//输入一个整形数
     
    	cout << "Produce Gray Code using method 1" << endl;
    	clock_t beginOne = clock();
    	GrayCodeOne(count);
    	clock_t endOne = clock();
    	cout << "Gray Code First Method using time: " << (endOne - beginOne) << endl;
     
    	cout << "Produce Gray Code using method 2" << endl;
    	clock_t beginTwo = clock();
    	GrayCodeTwo(count);
    	clock_t endTwo = clock();
    	cout << "Gray Code Second Method using time: " << (endTwo - beginTwo) << endl;
     
    	return 0;
    }
     
    // Method to produce gray code using method inserting 0 in front of old gray code by positive  
    //方法1中的已有的格雷码序列,正向前面一位加0,逆向前面一位加1
    // and inserting 1 in front of old gray code by nagative.
    void GrayCodeOne(int num)
    {
    	if (num < 1)
    	{
    		cout << "Error input Integer" << endl;
    		return;
    	}
     
    	vector<string> codeVec;  //定义一个字符串类型的向量 用来存放编码序列
     
    	int cIdx = 1;
    	for (; cIdx <= num; cIdx++)
    	{
                    //如果num=1,则for循环只有一次,codeVec大小就为2,里面放的是0,1
    		//开始向量为空,for循环的第一次,该向量大小一定小于2,直接存放0,1,
                    //存放一次结束后向量大小为2,则进入下面的else里
                    if (codeVec.size() < 2)  
    		{
    			codeVec.push_back("0");
    			codeVec.push_back("1");
    		}
    		else
    		{
                            //如果codeVec大小大于等于2个,则开始在,0和1上进行下面操作
    			vector<string> tranVec;  
    			tranVec.resize(2 * codeVec.size());  //定义一个中转向量,确定tranVec的大小为原有元素向量的2倍
    			int tranIdx = 0;
                            //迭代器对向量进行遍历
    			vector<string>::iterator codeIter = codeVec.begin();
    			for (; codeIter != codeVec.end(); codeIter++)
    			{
                                    //在正序输出的每个元素的前面加0
    				string str = "0";
    				str.append(*codeIter);
    				tranVec[tranIdx++] = str;//将加0后的元素重新存到转移向量
    			}
     
    			vector<string>::reverse_iterator rCodeIter = codeVec.rbegin();  //逆序迭代器,rbegin指向容器c的最后一个元素
    			for (; rCodeIter != codeVec.rend(); rCodeIter++)
    			{
                                    //在逆序输出的每个元素的前面加1
    				string str = "1";
    				str.append(*rCodeIter);
    				tranVec[tranIdx++] = str; 
    			}
     
    			codeVec.assign(tranVec.begin(), tranVec.end());
                            //将区间tranVec[first,last)的元素全部赋值到当前的codeVec容器中
                            //就是当前依序所编码的格雷码
    		}
    	}
     
    	//vector<string>::iterator vecIter = codeVec.begin();
    	//for (; vecIter != codeVec.end(); vecIter++)
    	//{
    	//	cout << *vecIter << endl;
    	//}
     
    	return;
    }
     
    // Method to produce gray code using method inserting 0/1 in the back of first gray code
    // then inserting 1/0 in the back of next gray code.
    void GrayCodeTwo(int num)
    {
    	if (num < 1)
    	{
    		cout << "Input error Integer" << endl;
    		return;
    	}
     
    	vector<string> codeVec;
     
    	int cIdx = 1;
    	for (; cIdx <= num; cIdx++)
    	{
    		if (codeVec.size() < 2)
    		{
    			codeVec.push_back("0");
    			codeVec.push_back("1");
    		}
    		else
    		{
    			vector<string> tranVec;
    			int tranIdx = 0;
    			int cIdx = codeVec.size();
     
    			tranVec.resize(2 * cIdx);
    			for (int vIdx = 0; vIdx < cIdx; vIdx++)
    			{
    				string str = codeVec[vIdx];
    				if (0 == (vIdx % 2))
    				{
    					string str0 = str;
    					str0.append("0");
    					tranVec[tranIdx++] = str0;
     
    					string str1 = str;
    					str1.append("1");
    					tranVec[tranIdx++] = str1;
    				}
    				else
    				{
    					string str0 = str;
    					str0.append("1");
    					tranVec[tranIdx++] = str0;
     
    					string str1 = str;
    					str1.append("0");
    					tranVec[tranIdx++] = str1;
    				}
    			}
     
    			codeVec.assign(tranVec.begin(), tranVec.end());
    		}
    	}
     
    	//vector<string>::iterator vecIter = codeVec.begin();
    	//for (; vecIter != codeVec.end(); vecIter++)
    	//{
    	//	cout << *vecIter << endl;
    	//}
     
    	return;
    }

    2、由二进制转换为格雷码

    这里的二进制数是整型数,直接对整型数进行操作获得格雷码。

    static unsigned int binaryToGray(unsigned int num) {
        return (num >> 1) ^ num;
    }

              unsigned int型数据到格雷码的转换,最高可转换32位自然二进制码

              int型数据到格雷码的转换,最高可转换31位自然二进制码。

    格雷码图案的生成:

    上面测试程序为一维格雷码的生成程序,下面为二维的格雷码图案的生成:

    格雷码图案的生成是在一维格雷生成的原理上将一位编码扩展到一列的编码。

    该图从上往下为格雷码递增级数,每一级为一幅编码投影图案的黑白分布图样,从左至右,编码值从“0000000”按照格雷码编码依次递增至“1000000”。

    投影图案设计

    采用与投影图案 μ、ν  轴两个方向相同的两个系列格雷码编码,其中每个方向最高级别格雷码宽度为 1 个像素,该设计方法具体分如下 2步:

    1)确定投影图案分辨率,假设水平 μ 方向上编码条纹设为 m 级,竖直 ν 方向上编码条纹设为 n 级,则投影图案在 μ 方向上像素个数为 2^m,ν 方向上像素个数为 2^n,即投影图案分辨率是 2^m×2^n。  
    2)投影图案分辨率确定后,使用格雷码编码方法,设计两个方向的编码投影图案,共生成 μ 方向上 m 张格雷码的编码图案,每张格雷码级别依次递增,和 ν 方向上 n 张格雷码的编码图案,每张格雷码级别依次递增。

    设计代码(有注释)如下:

    //设置的水平方向的条纹数是10,垂直方向的条纹数是6
    static unsigned int Nhorz = 10;
    static unsigned int Nvert = 6;
    
    #ifndef log2f
    #define log2f(x) (log(x)/log(2.0))
    #endif
    
    using namespace std;
    
    /*
     * The purpose of this function is to convert an unsigned
     * binary number to reflected binary Gray code.
     *
     * The operator >> is shift right. The operator ^ is exclusive or.
     * Source: http://en.wikipedia.org/wiki/Gray_code
     */
    static unsigned int binaryToGray(unsigned int num) {
        return (num >> 1) ^ num;
    }
    
    /*
     * From Wikipedia: http://en.wikipedia.org/wiki/Gray_code
     * The purpose of this function is to convert a reflected binary
     * Gray code number to a binary number.
    */
    static unsigned grayToBinary(unsigned num, unsigned numBits)
    {
        for (unsigned shift = 1; shift < numBits; shift <<= 1){
            num ^= num >> shift;
        }
        return num;
    }
    
    /*
     * Function takes the decimal number
     * Function takes the Nth bit (1 to 31)
     * Return the value of Nth bit from decimal
     * Source: http://icfun.blogspot.com/2009/04/get-n-th-bit-value-of-any-integer.html
     */
    static int get_bit(int decimal, int N){
    
        // Shifting the 1 for N-1 bits   
        int constant = 1 << (N-1);   //将00000001左移N-1位
    
        // If the bit is set, return 1
        if( decimal & constant ){
            return 1;
        }
    
        // If the bit is not set, return 0
        return 0;
    }
    
    static inline int powi(int num, unsigned int exponent){
        // NOT EQUIVALENT TO pow()
        if(exponent == 0)
            return 1;
    
        float res = num;
        for(unsigned int i=0; i<exponent-1; i++)
            res *= num;
    
        return res;
    }
    
    // Encoder   参数为:行 列 方向
    EncoderGrayCode::EncoderGrayCode(unsigned int _screenCols, unsigned int _screenRows, CodecDir _dir) : Encoder(_screenCols, _screenRows, _dir){
    
        N = 2;  //初始值为2
    
        // Set total pattern number
        if(dir & CodecDirHorizontal)
            this->N += Nhorz;         //如果编码为水平方向,N为2+Nhorz  N=12
    
        if(dir & CodecDirVertical)    //如果编码为水平方向,N为2+Nvert  N=8
            this->N += Nvert;
    
        // Encode every pixel column     //编码每一像素列  NbitsHor=10
        int NbitsHorz = ceilf(log2f((float)screenCols));  //先将列数转变为浮点数,再将列数取log2f运算,将float类型的小数去掉,然后进一,
    
        // Number of vertical encoding patterns  同上 针对每一行  9.58去掉小数进1为10,即NbitsVert=10
        int NbitsVert = ceilf(log2f((float)screenRows));
    
    	//patterns的前两个元素为patternOn和patternOff
        cv::Mat patternOn(1, 1, CV_8UC3, cv::Scalar(0));  //定义一个1x1的patternOn矩阵
        patternOn.at<cv::Vec3b>(0,0) = cv::Vec3b(255, 255, 255);   //矩阵的(0,0)点定义为(255, 255, 255)白色
        patterns.push_back(patternOn); 
    
        cv::Mat patternOff(1, 1, CV_8UC3, cv::Scalar(0));  //定义一个1x1的patternOff矩阵
        patterns.push_back(patternOff);
    
    
        if(dir & CodecDirHorizontal){
            // Precompute horizontally encoding patterns
            for(unsigned int p=0; p<Nhorz; p++){
                cv::Mat patternP(1, screenCols, CV_8UC3);   //定义一个1行xscreenCols列的patternP矩阵
    
                // Loop through columns in first row       从第一行遍历所有列
                for(unsigned int j=0; j<screenCols; j++){
                    unsigned int jGray = binaryToGray(j);   //将二进制转换为格雷码
    
                    // Amplitude of channels   设置每个通道的灰度值
                    float amp = get_bit(jGray, NbitsHorz-p);
                    patternP.at<cv::Vec3b>(0,j) = cv::Vec3b(255.0*amp,255.0*amp,255.0*amp);
                }
                patterns.push_back(patternP);
            }
        }
        if(dir & CodecDirVertical){
            // Precompute vertical encoding patterns     
            for(unsigned int p=0; p<Nvert; p++){
                cv::Mat patternP(screenRows, 1, CV_8UC3);    //定义一个screenRows行x1列的patternP矩阵
    
                // Loop through rows in first column       从第一列遍历所有行
                for(unsigned int i=0; i<screenRows; i++){
    
                    unsigned int iGray = binaryToGray(i);
    
                    // Amplitude of channels
                    float amp = get_bit(iGray, NbitsVert-p);
                    patternP.at<cv::Vec3b>(i,0) = cv::Vec3b(255.0*amp,255.0*amp,255.0*amp);
                }
                patterns.push_back(patternP);
            }
        }
    
        #if 0
            for(unsigned int i=0; i<patterns.size(); i++){
                std::stringstream fileNameStream;
                fileNameStream << "pattern_" << std::setw(2) << std::setfill('0') << i << ".bmp";
                cv::imwrite(fileNameStream.str(), patterns[i]);
            }
    
        #endif
    }

    在上述条纹生成的代码中,以横向的条纹图案,重要的步骤为

    (1)设置两个一个像素大小的矩阵

    (2)先分配一个空矩阵用来存放生成的pattern,从01两位格雷码开始开始

             内层for循环是对第一行的所有像素点按照编码的格雷码值进行灰度赋值,即

             如果P=0,则为第1张条纹图,用第一行每一列数(从0到1023)转换的格雷码值的 第10 位值去填充该列像素灰度值,

             这时,Mat矩阵中存放的是水平排布(竖直条纹)条纹第一行的一维行条纹。

            需要说明的是,这里的生成思路很巧妙

           首先是直接对图像的宽度进行格雷码转换,可以直接对任意的图像尺寸进行编码;

           同时对格雷码值取位操作,所取的位数正好分割了尺寸,每一行被分割成N=1024/2^(NbitsHorz-P)等份,每一份像素个数为

           1024/N。

           如果要生成一张图像,则需要将第一行的条纹进行复制,opencv里的repeat函数,矩阵拷贝的时候指定按x/y方向重复

          这里将第一行的条纹在Y方向按图像的行数重复,在X方向上重复为1次(不做操作),即可得到完整的一张格雷码图案。

    (3)二进制数转为一维格雷码代码

    (4)这个函数主要是将整型数所转换的一维格雷码的第N位对应的0,1状态取出

    最后生成了横向条纹竖直分布的格雷码并存在了patterns中,纵向分布的类似。

    Matlab代码:

    function [P,offset] = graycode(width,height)
    
    % % Define height and width of screen.
    % width  = 1024;
    % height = 768;
    
    % Generate Gray codes for vertical and horizontal stripe patterns.
    % See: http://en.wikipedia.org/wiki/Gray_code
    P = cell(2,1); %胞元
    offset = zeros(2,1);  
    for j = 1:2
       
       % Allocate storage for Gray code stripe pattern.
       if j == 1
          N = ceil(log2(width));
          offset(j) = floor((2^N-width)/2);
       else
          N = ceil(log2(height));
          offset(j) = floor((2^N-height)/2);
       end
       P{j} = zeros(height,width,N,'uint8');
       
       % Generate N-bit Gray code sequence.
       B = zeros(2^N,N,'uint8');
       B_char = dec2bin(0:2^N-1);  %十进制转二进制
       for i = 1:N
          B(:,i) = str2num(B_char(:,i));
       end
       G = zeros(2^N,N,'uint8');
       G(:,1) = B(:,1);
       for i = 2:N
          G(:,i) = xor(B(:,i-1),B(:,i));
       end
       
       % Store Gray code stripe pattern.  B = repmat(A,m,n)  B 由 m×n 个 A 平铺而成
       if j ==1 
          for i = 1:N
             P{j}(:,:,i) = repmat(G((1:width)+offset(j),i)',height,1);
          end
       else
          for i = 1:N
             P{j}(:,:,i) = repmat(G((1:height)+offset(j),i),1,width);
          end
       end
       
    end

    matlab程序的思路:

    此时,格雷码图案完全生成,根据格雷码图案生成相移条纹图案,格雷码的最小周期为T=log2(width),相移条纹的周期为T/4即可,具体生成过程在另一篇博文详细说明,这里不再重复。

    格雷码编码程序实例3(C++),对应于解码程序。

    // Generate Gray codes.     格雷码在生成时 通过sl_scan_cols和sl_scan_rows的状态来判断生成行还是列条纹图案 
    
    int generateGrayCodes(int width, int height,     //以1024 * 768为例
    					  IplImage**& gray_codes,    
    					  int& n_cols, int& n_rows,
    					  int& col_shift, int& row_shift, 
    					  bool sl_scan_cols, bool sl_scan_rows){
    
    	// Determine number of required codes and row/column offsets.
    	if(sl_scan_cols){                            //判断是生成哪种条纹
    		n_cols = (int)ceil(log2(width));         //n_cols  格雷码的张数,即格雷码的最小宽度   n_cols=10 ;ceil是求大于该数的整数
    		col_shift = (int)floor((pow(2.0,n_cols)-width)/2);       //列偏移量  比宽度多出来的?  col_shift=0;foolr是求小于该数的整数
    	}
    	else{
    		n_cols = 0;
    		col_shift = 0;
    	}
    	if(sl_scan_rows){
    		n_rows = (int)ceil(log2(height));      //ceil(9.58) = 10 =n_rows
    		row_shift = (int)floor((pow(2.0,n_rows)-height)/2);      //行偏移量 row_shift = floor((1024-768)/2)=128
    	}
    	else{
    		n_rows = 0;
    		row_shift = 0;
    	}	
    
    	// Allocate Gray codes.   分配内存  
    	gray_codes = new IplImage* [n_cols+n_rows+1];   //为什么要加1?加一张白色图案
    
    	for(int i=0; i<(n_cols+n_rows+1); i++)  //i<21
    		gray_codes[i] = cvCreateImage(cvSize(width,height), IPL_DEPTH_8U, 1);   //创建n_cols+n_rows+1 = 21张空图像内存,其中第一张为 全白图案
    
    	int step = gray_codes[0]->widthStep/sizeof(uchar);  //widthStep=图像的宽度*通道数,即每一行需要的内存长度;step即每一行有多少个uchar类型的数据
    
    	// Define first code as a white image.
    	cvSet(gray_codes[0], cvScalar(255));
    
    	// Define Gray codes for projector columns.    投影仪的列         三层循环!!!
    	for(int c=0; c<width; c++){
    		for(int i=0; i<n_cols; i++){     //i从0到9共循环10次,一行的每一像素都要循环10次,每循环一次就是对每一张图像的操作
    			uchar* data = (uchar*)gray_codes[i+1]->imageData;       //data指针 指向格雷码图像的数据的地址
    
    			if(i>0)  //从第二张图案开始  data[c]是一维数组,只能放一个值,应该是0或者1
    				data[c] = (((c+col_shift) >> (n_cols-i-1)) & 1)^(((c+col_shift) >> (n_cols-i)) & 1);//先右移后与   i=1时,c分为3个部分,0-255,255-767,768-1023,编码分别为0,1,0 
    			else
    				data[c] = (((c+col_shift) >> (n_cols-i-1)) & 1);    //如果i=0时,c<=511,data[c]=0,c>511,data[c]=1
    
    			//col_shift的作用:如果投影仪尺寸为912*1140,n_cols为10,则在编码时是以1024位来进行编码的,以c=0为例,会导致0-511=0,512-912=1,左右不等长
    			//所以仍需要以512为中心,通过将0-912后移56变为56-968,这样512则变为了中心,左右编码位数相等。
    			//像素值全部乘以255
    			data[c] *= 255;  
    
    			for(int r=1; r<height; r++)     //得到一行的编码后执行的平铺操作 
    				data[r*step+c] = data[c];	//r*step为每一行所占的内存
    		}
    	}
    
    	// Define Gray codes for projector rows.       投影仪的行
    	for(int r=0; r<height; r++){
    		for(int i=0; i<n_rows; i++){
    			uchar* data = (uchar*)gray_codes[i+n_cols+1]->imageData; //格雷码的行列图案数据内存地址是连续的  
    			if(i>0)
    				data[r*step] = (((r+row_shift) >> (n_rows-i-1)) & 1)^(((r+row_shift) >> (n_rows-i)) & 1);
    			else
    				data[r*step] = (((r+row_shift) >> (n_rows-i-1)) & 1);
    			data[r*step] *= 255;                   //data[r*step] = data[r*step]*255
    			for(int c=1; c<width; c++)
    				data[r*step+c] = data[r*step];	
    		}
    	}
    
    	// Return without errors.
    	return 0;
    }

    程序解读:

    1,两个bool类型状态变量:sl_scan_cols,sl_scan_rows,判断生成行还是列条纹图案。

    2,根据投影仪的投影图案的尺寸(以912*1140为例)来计算横向和纵向条纹图案的位数和生成的张数以及怕行和列的偏移量。

    3,为格雷码的存储分配内存,并在第一张单独存储全白图案,其余格格雷图案内存连续存储。

    4,通过3层循环来实现格雷码图案的生成,重点在每一行(列)格雷码编码的生成:

            i=0时,编码方式为(((c+col_shift) >> (n_cols-i-1)) & 1);       

            当i从1变到n_cols时,n_cols-i-1和n_cols-i的值为:

           该句代码则是对每一个像素点进行编码,并通过异或来获得编码值0或1,推导过程如下:

    该程序与之前的不同之处在于:

    (1)加入了行和列的偏移量,在编码时是:

    col_shift的作用:如果投影仪尺寸为912*1140,n_cols为10,则在编码时是以1024位来进行编码的,以c=0为例,会导致0-511=0,512-912=1,左右不等长,所以仍需要以512为中心,通过将0-912后移56变为56-968,这样512则变为了中心,左右编码位数相等。

    (2)数据全部在内存中操作,可以减小内存消耗,加快运算速度。

    如果有疑问或者见到其他的不同的格雷码编码程序,请留言或QQ联系博主交流学习(857467352)。

    展开全文
  • 用户注册登录系统加密方案分析与实践

    千次阅读 多人点赞 2019-10-26 10:59:39
    设计良好的注册登录系统可以保证即使在用户客户端被监听、数据网络传输被拦截、服务端数据被泄露的情况,也能最大程度地保障用户的密码安全,从而保障用户的资金财产安全。本文结合工程实践,对用户注册登录系统...

    序言

    对于一个网站而言,用户注册登录系统的重要性不言而喻,而该系统的安全性则可谓是重中之重。设计良好的注册登录系统可以保证即使在用户客户端被监听、数据网络传输被拦截、服务端数据被泄露的情况下,也能最大程度地保障用户的密码安全,从而保障用户的资金财产安全。本文结合工程实践,对用户注册登录系统可能面临的攻击和风险点逐一进行分析,并给出对应的应对措施,最终得到一套切实可行的用户注册登录设计方案。

    五种常见的用户密码泄露方式

    黑客可以通过监听用户的客户端(app或pc浏览器)、拦截用户网络请求、非法入侵服务端数据库、撞库攻击和钓鱼攻击等五种方式窃取用户密码。其中监听客户端包含如下手段(详见黑客破解密码的几种方式):

    1、通过木马对用户使用的设备键盘进行监控,通过分析用户的击键信息即可破解用户密码;

    2、对于使用鼠标和图片录入密码的方式,黑客可以通过控制木马程序对用户的屏幕进行录屏监控,从而获取用户密码。

    拦截用户网络请求可以细分为如下手段:

    1、客户端监听用户请求,抓取数据包,以获取用户明文密码(针对https,详见https真的安全吗);

    2、网络传输链路上拦截用户请求,获取用户密码(仅针对http的明文密码传输);

    3、网络传输链路上拦截用户请求,执行网页替换或部分代码替换,从而骗取用户敏感信息。

    非法入侵服务端数据库是指黑客利用服务器的漏洞,非法访问服务端的数据库并窃取用户账号密码等信息。

    撞库攻击则是利用很多用户在不同网站使用相同的帐号密码,即黑客可以通过获取用户在A网站的账户密码从而尝试登录B网站。也就是说,即使你自身的网站对用户的密码做了各种各样的防护手段,也无法避免该用户在其他网站的密码被黑客窃取,从而导致该用户在你网站上的密码也被窃取(对于这个问题,后面会给出应对策略)。

    钓鱼攻击是指黑客利用欺骗性的电子邮件和伪造的网站登录站点来诱骗用户输入用户名、密码等敏感信息,从而窃取用户密码。其原理和前面提到的拦截用户请求,执行网页替换的方式非常相似。

    密码破解利器——彩虹表

    本人的另一篇博客深入浅出彩虹表原理介绍了密码破解利器——彩虹表。为了更好地理解本文接下来介绍的内容,强烈建议先阅读该博客中的内容。

    参考博客中的方案分析

    参考博客App登录模块密码加密方案中给出了一种设计方案:

    图中的rule1和rule2方法实际上是对应的是MD5、SHA128、SHA256、SHA512、RipeMD、WHIRLPOOL等不可逆的哈希(hash)算法(关于哈希算法不可逆的原理介绍,详见参考博客为什么说MD5是不可逆哈希算法)。

    上述方案存在的致命问题是无法应对前面介绍的“非法入侵服务端数据库”这种攻击(由参考博客2018上半年国内外互联网十大数据库泄露事件可知,非法入侵数据库是黑客批量获取用户账号密码的重要手段之一)。由图中所示,用户注册时,在客户端对明文pass加密得到passStr并保存在数据库中,当黑客非法入侵到服务端数据库并获取到用户的密码passStr后,甚至都不需要破解以得到pass,而直接用passStr登录即可。因而在参考博客加盐hash保存密码的正确方式中提到,即使客户端做了哈希运算,服务端依然需要将得到的密文再进行hash。

    此外,图中的方案在登录时采用了向服务器请求随机盐的方式来对明文进行加密的方案,而参考博客加盐hash保存密码的正确方式中却反对使用这种方式,给出的原因是恶意的攻击者可以通过这个逻辑来判断一个用户名是否有效。对于这个理由,你可能会不以为然,因为你可能会说,对于用户请求随机盐的接口,服务器完全可以不校验该用户名是否存在,而只是临时生成一个随机盐返回给客户端,并将该盐存储到缓存中。当用户用该盐生成的密码提交登录请求时再进行校验,并返回“用户名或密码错误”这样的提示即可避免上述问题。对此,我们需要结合刚刚提到的“即使客户端做了哈希运算,服务端依然需要将得到的密文再进行hash”来分析为什么说这样做是不OK的。由于服务端需要对客户端的密文再进行一次哈希,如下图所示:

    图示中,注册时对明文A只使用了普通的hash,在服务端对密文B使用不同对哈希函数再次进行运算,得到密文C并存储到数据库。登录时,客户端先像服务端请求hash盐,然后对明文A使用加盐的hash,服务端得到了密文D。可问题是,这个时候我们无法验证密文D的正确性!!为此,我们不得不考虑在注册时也使用加盐hash,如下图所示:

    图示中,我们注册和登录时都使用了加盐hash,而且,为了保证登录时能校验明文的正确性,我们必须使用和注册时同样的盐,因此盐值不能只是存储中缓存中,而是需要同密文一起存储到数据库中。试想一下,此时用户登录,请求用户盐值,如果该用户存在,则返回其盐值,如果不存在,则不返回盐值。黑客不就可以以此判断该用户是否合法了吗?

    至此,我们明白了,通过客户端向服务端获取盐值的方案不合适,那么具体应该怎么办呢?参考博客加盐hash保存密码的正确方式中提到:因为我们已经在服务端进行了恰当的加盐的hash。所以这里使用用户名跟特定的字符串(比如域名)拼接作为客户端的盐是可以的。也就是说,参考博客加盐hash保存密码的正确方式推荐如下方案:

    个人对此方案持反对意见。我们在客户端加随机盐的目的是使客户端到服务端之间的密码安全。现在客户端采用了加常量随机盐的方式,由参考博客深入浅出彩虹表原理可知,在彩虹表面前,常量随机盐的意义并不大。那么该如何保障客户端到服务端之间的密码安全呢?

    一个可行的方案是使用非对称加密算法RSA(百度的注册登录使用的就是这个算法,RSA属于非对称加密算法,即加密解密使用的密钥不是同一个。关于RSA算法的详细原理见本人的另一篇博客RSA算法原理及其在HTTPS中的应用),具体来说就是用户注册登录时,在提交注册或登录请求前先请求网站的公钥,并使用公钥对明文进行加密,服务端接收到密文之后,先用私钥进行解密,然后再通过加盐的哈希算法加密并存储到数据库中。即客户端和服务端之间的链路通过RSA算法保证密码的安全,而服务端仍然采用随机盐的哈希算法:

    实际上,上述方案还存在一个问题:在服务端通过私钥解密之后居然能看到用户的明文密码!!!这肯定是不能接受的!因而实际上,我们在客户端使用RSA加密之前,会先使用哈希算法对明文进行加密,具体流程如下所示:

    由图可知,整个链路,除了用户输入时是明文之外,其它地方都是密文,从而可以较好地保证用户的密码安全。为了便于后续描述,我把该方案简称为“哈希+RSA+随机盐哈希”方案。

    其实到这里,密码的安全程度已经非常高了,可以直接应用于企业网站系统了。但如果还想在此基础上进一步提高其安全性,还可以往哪个方向努力呢?

    可以看到,上述方案对于黑客而言,由于RSA算法的公钥是公开的,因而获取到明文A和获取到密文L是等价的!!假设黑客通过非法入侵数据库得到了用户的密文M及其随机盐salt值,在掌握了服务端加密算法hash2的情况下,就可以利用彩虹表对单个用户进行暴力破解(尽管破解成本很高),最终只需要获取密文L即可。

    对此,参考博客加盐hash保存密码的正确方式中也提到了:只要攻击者能够验证一个猜测的密码是正确还是错误,他们就可以使用字典或者暴力攻击破解hash。更深度的防御方法是加入一个保密的key(secret key)进行hash,这样只有知道这个key的人才能验证密码是否正确。这个思路可以通过两种方式来实现。一种是hash通过加密算法加密比如AES,或者使用基于key的hash函数(HMAC)。

    以AES(AES、DES等都属于对称加密算法,即加密解密的秘钥是同一个,详见参考博客用不可逆加密纯客户端实现加密及验证)为例(HMAC方案类似,不再赘述),具体方案如下:

    该方案是在“哈希+RSA+随机盐哈希”方案的基础上再加了一层AES对称加密。对于对称加密的密钥,博客加盐hash保存密码的正确方式中要求将AES使用的加密key单独存储在一个外部系统中,比如专门用来进行密码验证的物理隔离的服务器。

    试想一下,黑客通过非法入侵数据库获取了密文N和随机盐,并且掌握了服务端使用的hash2和AES算法(由于AES的加密key在独立的数据库,因而此处假设黑客只是掌握了加密算法,而没有获取到加密key),那么他是否有可能通过暴力破解以得到密文L呢?

    假设黑客猜测密文N破解后的密文为P,这里需要验证P=L。为此,黑客首先将密文P使用hash2算法执行加盐哈希,得到的密文假设为Q,由于黑客不知道AES算法的加密key,因而无法对Q进行运算,也就无法验证Q经过AES(Q, key)加密之后的结果和密文N是否相等。由此可知,只要保护好了key不被泄密,那么黑客的暴力破解就是无效的,因为他无法验证猜测的密码是否正确。我们将该方案简称为“哈希+RSA+随机盐哈希+AES”方案。当然,对应的还有“哈希+RSA+随机盐哈希+HMAC”方案。

    针对五种泄密方式的分析

    本节内容我们来分析上一节得到的三个方案在面对前面介绍的五种泄密方式时的表现。

    很显然,该方案对于前面提到的监听客户端的键盘和屏幕等攻击手段无能为力;对于拦截用户网络请求的三种方式中,能避免第一和第二种攻击,但无法解决网页替换攻击(所幸采用https可以解决第二种和第三种攻击,因而本文得到的两个方案在基于https进行加密传输的情况下可以避免拦截用户网络请求的各种攻击);对于黑客非法入侵服务端数据库,由于密文都是经过加密的,就算使用彩虹表也难以破解;以上加密方案对于撞库攻击和钓鱼攻击均无能为力。

    至此可知,用户注册登录系统采用本文最终的三种方案结合https进行网络传输,可以解决用户网络请求拦截攻击和服务端数据库非法入侵,但还是无法避免客户端监听攻击、撞库攻击和钓鱼攻击!!!

    其实细想一下,以上三种攻击,已经完全超出了注册登录系统本身所能保护的范围。比如你无法绝对保证用户使用的客户端是安全的,你也无法保证用户在不同网站使用的账号密码是不一样的,你也无法保证用户一定不会访问一个钓鱼网站从而导致密码泄露。因而对于以上三种攻击,我们也不要再奢望通过增加注册登录系统的复杂性而有所改善,完成这个工作的应该另有其人。

    风控系统——网站的保护伞

    绝大多数的大中型互联网公司都有风控系统,该系统最重要的工作就是对用户操作行为进行风险评估,以保证在用户的密码已经被泄漏的情况下,最大限度地保护用户的资金财产安全。风控系统具体的操作思路是:建立用户行为画像,即存储用户日常登录的终端设备的ip地址、设备唯一标识、用户所在地、常用往来交易账户等信息。当用户出现异常操作时,比如异地登录、换新设备登录等,则该操作会被判定为风险操作,从而通过增加邮件、短信等验证机制以确认是用户本人的操作行为,并适时提醒用户更新密码。

    总结

    至此可知,本文最终得到的三个加密方案“哈希+RSA+随机盐哈希”、“哈希+RSA+随机盐哈希+AES”、“哈希+RSA+随机盐哈希+HMAC”结合“基于https的加密传输”方案可以较好地保障系统自身的数据安全,但无法保障自身系统以外的数据安全。而风控系统则负责保证在用户的密码已经被泄漏的情况下,最大限度地保护用户的资金财产安全。两相结合,就可以有效地保障用户的资金财产安全。

    参考博客:

    1、http://www.360doc.com/content/18/0318/06/415885_738036451.shtml 黑客破解密码的几种方式

    2、https真的安全吗,加密登录其实不简单 - 光何 - 博客园 https真的安全吗,加密登录其实不简单

    3、App登录模块密码加密方案 - 简书 App登录模块密码加密方案

    4、深入浅出彩虹表原理_Saintyyu的博客-CSDN博客_彩虹表原理 深入浅出彩虹表原理

    5、加盐hash保存密码的正确方式 - D&G 加盐hash保存密码的正确方式

    6、盘点: 2018上半年国内外互联网十大数据库泄露事件_数量 盘点: 2018上半年国内外互联网十大数据库泄露事件 

    7、RSA算法原理及其在HTTPS中的应用_Saintyyu的博客-CSDN博客 RSA算法原理及其在HTTPS中的应用

    8、为什么说MD5是不可逆哈希算法_Saintyyu的博客-CSDN博客_哈希算法为什么不可逆 为什么说MD5是不可逆哈希算法

    9、HTTPS连接过程以及中间人攻击劫持_Joker_Ye的博客-CSDN博客_https劫持 HTTPS连接过程以及中间人攻击劫持

    10、用不可逆加密纯客户端实现加密及验证 - 简书 用不可逆加密纯客户端实现加密及验证

    11、RSA 与 DSA - chromebook - 博客园 RSA、DSA以及ECC

    12、为什么用抓包工具看HTTPS包是明文的 - _Tenma - 博客园  为什么抓包工具看HTTPS包是明文

    展开全文
  • 效果图第一步:打开PS,Ctrl+O打开素材,Ctrl+J复制一层,得到图层1,回到工具栏,用污点修复画笔工具先做粗修,画笔用硬边,模式:正常,类型:内容识别,如图第二步:回到图层面板,Ctrl+J复制一个图层,进入...

    d29f7809871e224911f2d11577e83b67.png

    今天,教大家如何去除雀斑,美颜,教程主要包括了对图层通道、滤镜的高反差保留,污点修复画笔工具,以及曲线等效果的应用。

    效果图

    db8e46c9ce874d6c8130d3ee0cb78ee6.png

    第一步:打开PS,Ctrl+O打开素材,Ctrl+J复制一层,得到图层1,回到工具栏,用污点修复画笔工具先做粗修,画笔用硬边,模式:正常,类型:内容识别,如下图

    0f4c530ca4fe010513e2d954d832c070.png

    第二步:回到图层面板,Ctrl+J复制一个图层,进入通道,通道分为RGB、红、绿、蓝,拖动蓝色到下面的复制面板进行拷贝,如下图

    6345a4d6d99c9c58829c3fdbc8ea8558.png

    第三步:选中蓝通道副本,回到【菜单栏】-【滤镜】-【其他】-【高反差保留】,半径设置为10.7,按确定,如下图

    c0659fa5986e2da78abe7670339a8ff1.png

    第四步:回到【菜单栏】-【图像】-【计算】,把混合模式改为强光,其他不用变,目的是对蓝副本通道更进一步的加强对比效果,从而更精确的计算出我们所需要的斑点的选区。计算三次,得到Alpha3,如下图

    c98adfc445b722f69a61fe1850d11f52.png

    5375ea7dc075b705f87e39e537347f6e.png

    第五步:此时左手按键盘Ctrl键,鼠标单击Alpha3通道缩略图生成选区,点RGB图层回到图层面板。

    16e922905680b89d895556b96b71e04a.png

    第六步:回到【菜单栏】-【选择】-【反选】,为了不影响我们的视觉效果,便于观察这里我觉得有必要将选区隐藏,快捷键Ctrl+H,如下图

    44ff9e4fa11e78618162060698b08cae.png

    第七步:回到图层面板,点击图层下方的调整图层,选中里面的曲线,进行调整,效果如下图

    2405c3c5b3c07915fb1f768022a011b0.png

    第八步:回到图层面板,选中曲线1的蒙版,前景色设置为黑色,打开画笔工具,把头发和脸部进一步擦亮,,我们再看一下效果图

    da044e83473806492fcb098e5614fb96.png

    ac8f3d35ceef90a099b55da3d302d90e.png

    第九步:回到图层面板,选中图层Ctrl+J连续复制两层,得到图层1拷贝和图层1拷贝2,按住Shift键,同时把图层1拷贝和图层1拷贝2选住,移动到最顶层,效果如下图

    f4364fa877b470024b4cc5fbb702da81.png

    第十步:回到图层面板,把选中图层1拷贝,把图层1拷贝2前面的小眼睛关掉,回到菜单栏,执行【滤镜】-【模糊】-【表面模糊】,不透明度降低到65%,参数设置及效果如下图

    5ac34fab26acb565d934d09ef70705e3.png

    93347ceb696b0ab760f88d5663c0c20f.png

    第十一步:回到图层面板,把图层1拷贝2前面的小眼睛打开,回到菜单栏,执行【图像】-【应用图像】,参数设置及效果如下图

    f54288ae75c89f9f0b4d5dca0eb776fe.png

    第十二步:回到【菜单栏】-【滤镜】-【其他】-【高反差保留】,半径设置为0.3,,按确定,如下图

    04c560b64720580b0ea4ff7ae2957726.png

    第十三步:回到图层面板,把图层1拷贝2的图层混合模式“正常”改为“线性光”,效果如下图

    85276b55a3b5abf68319bb8bd8b266d0.png

    db8e46c9ce874d6c8130d3ee0cb78ee6.png

    素材

    0b728e660fc661553420a5c58d50d464.png

    今天的分享到这里就结束,希望对大家有所帮助~更多教程请关注我的知乎账号和PS专栏

    说明:作者只在知乎和公众号西瓜视觉创作,知乎免费为大家分享教程,公众号西瓜视觉免费为大家分享安装包、插件、笔刷等

    81a569b989b3e5b628b0ba97b3bcff38.png
    展开全文
  • 联想7205 7250一体打印机粉图解教程.doc
  • 实验名称:物体速度、速度的测量 一、实验目的 1. 学会使用气垫导轨和数字毫秒计。 2. 掌握测量速度和速度的方法。 二、实验仪器 气垫导轨、数字毫秒计、挡板、光电门、滑块、垫块
  • 三轴速度传感器原理及应用

    万次阅读 2017-06-25 22:10:44
    三轴速度传感器原理 MEMS换能器(Transducer)可分为传感器(Sensor)和致动器(Actuator)两类。其中传感器会接受外界的传递的物理性输入,通过感测器转换为电子信号,再最终转换为可用的信息,如速度传感器、...
  • 什么是量子加密(一)

    万次阅读 2019-08-30 23:24:28
    同样可以把偏振旋转45°,这样45°方向的偏振就代表0,135°方向振动的光子代表1,不妨称之为B方案。到此,我们已经可以通过A、B两套不同的方案发射0或者1这样的数值了。当然,接收方自然也就可以利用A、B两套...
  • 选项分为“年”,“月”,“日”,“时”,“分”,“秒”,“确认”(立方显示“N”或“Y”) 确认一项 确认键(确认页面) 确认,并退出(“N”不保存时间,退出,“Y”保存时间,退出) 选择键 数字加一 ...
  • 基于无源网络技术(PON,passive optical network)的光纤到户(FTTH,fiber to the home)接入网是一种点到多点的网络结构,即利用分路器将中心局端信号传送到多个终端用户。 基于PON技术的FTTH是未来接入网络...
  • 步骤如下:word2003:选择需要黑点的段落→格式→项目符号和编号→里面有你想要的黑点→确定word2010选择需要黑点的段落→开始标签中段落的第一列(整个标签的中间)是!如果找不到,也可以选择段落后右击,里...
  • 树莓派基础实验31:MPU6050陀螺仪速度传感器实验

    千次阅读 多人点赞 2020-11-06 09:31:55
       它集成了3轴MEMS陀螺仪,3轴MEMS速度计,以及一个可扩展的数字运动处理器 DMP( DigitalMotion Processor),可用I2C接口连接一个第三方的数字传感器,比如磁力计。扩展之后就可以通过其 I2C或SPI接口输出一...
  • 速度计和陀螺仪区别

    千次阅读 2020-01-11 17:23:55
    一言以蔽之,速度计在较长时间的测量值(确定飞机航向)是正确的,而在较短时间内由于信号噪声的存在,而有误差。陀螺仪在较短时间内则比较准确而较长时间则会有与漂移而存有误差。因此,需要两者(相互调整)来...
  • 本文介绍了使用credits1给视频字幕的功能,对credits1函数的语法、参数、返回值及使用注意事项进行了详细介绍,并介绍了credits1使用的字幕文本文件的格式控制方式,并提供了完整案例。大家可以通过本文的介绍熟练...
  • 跨年烟花代码文案,确定不来看看吗

    千次阅读 多人点赞 2021-12-13 17:16:41
    fill="black") cv.pack() root.after(50, show,cv) root.mainloop() 效果展示: 结尾: 我希望约你在放烟花的日子里见一面,这样就能使见面的前后几天都沾着,变成好日子。 我还 希望能一起打牌,烤火,嗑瓜子,...
  • 在透明衬底的情况——根据穿过衬底的信号的出现来确定的。如果在激光作用区的上面放两个电极,而电极由功率电源或供发射光谱分析的电光源给它电压,那末,当电路中达到侵蚀等离子体电极时,产生记录在示波器上...
  • SCENE三维点云拼接 1、插入法如的狗打开桌面软件 2、在文件位置新建项目,项目放到fls文件目录,点击创建 ...按照图设置并点击配置,按照图片更改;点击确定(等待进度读条) ...5、如图,两站重叠...
  • 如何选择 | 青少年渐进多焦点的ADD

    千次阅读 2021-02-05 12:18:28
    ADD又称为下加光,即近用阅读附加度数,是代偿一部分青少年看近时自身调节,减少眼睛调节时的睫状肌的疲劳,从而减缓青少年眼睛近视度数的增加。青少年近用阅读书写我们要求保持33CM的距离,此时正常付...
  • MPU6050集成了3轴陀螺仪和3轴速度计,可以获取物体的姿态和运动信息。陀螺仪可测范围为±250,±500,±1000,±2000°/秒,速度计可测范围为±2,±4,±8,±16g,使用16bitAD采样。使用I2C接口通信。 本节...
  • 是一个BOOST电路,输入Vin是9~12.6V,VOUT输出是12.8V,在芯片的SW管脚,R53和C52后,DC-DC概率性的损坏,不这个RC,试了70块板子没有损坏,基本可以确定是这个导致的,但是是怎么坏的呢? C52和R53是为了...
  • 推导并实现电机曲线减速。
  • 推荐   ♥各位如果想要交流的话,可以加下QQ交流群:974178910,里面有各种你想要的学习资料。♥   ♥欢迎大家关注公众号【不温卜火】,关注公众号即可以提前阅读又可以获取各种干货哦,同时公众号每满1024及...
  • 目的考察制备的Ce-N-TiO2/AC催化剂对有机染料废水的处理效果,确定其去除甲基橙染料废水的工艺条件.方法在自制催化反应器中,利用制备的Ce-N-TiO2/AC催化剂在紫外灯的条件处理染料废水,分析对比在不同...
  • PCB加工文件—Gerber文件的导出

    万次阅读 多人点赞 2019-04-27 13:50:24
    2)出现如下对话框,确认红色选中区域内的格式设置,点击确定 3)生成钻孔文件XXXX.TXT(XXXX为PCB命名) 五.绘文件输出 1.在PCB工程文件中包含了PCB加工需要的绘文件。至此便可将这些文件打包发给PCB厂家。...
  • 联想M7450F打印机粉清零方法

    千次阅读 2020-12-23 06:56:54
    1、粉后将硒鼓放入1653机器版内,权通电,打开前盖(放入硒鼓时打开的前面板)2、粉后将硒鼓放入机器内,通电,打开前盖(放入硒鼓时打开的前面板),按“清除/返回”键,再按“*”,再按2“0”键,再按“确认”键...
  • 主要的物理信号有声波信号、信号(一种特殊的电磁)、电磁信号,他们都是利用这些信号在传播过程中的幅度、频率、相位、能量等宏观的、连续性的特性来承载信息的。 而量子通信是利用量子的叠加态、纠缠效应等这些...
  • 在上篇文章——系列篇|结构三维重建基本原理中,笔者介绍了单目结构三维成像系统把投影仪“看成”相机的模型。基于这个模型,单目结构三维成像系统可以像双目三维成像系统那样来获取空间中物体的三维信息。...
  • 【理解】线结构成像模型

    千次阅读 2019-01-09 20:42:44
    最近由于课题需要,要接触到线结构成像模型的理解,网上找了博客没有写的很详细,找到了一篇写的很好的论文,在这先记录,以供大家学习参考; 1.先理解一下模型是如何建立的 模型主要涉及两个坐标系: 物体坐标...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 107,265
精华内容 42,906
关键字:

如何确定下加光