Kinect开发 —— 基础知识
转自:http://www.cnblogs.com/yangecnu/archive/2012/04/02/KinectSDK_Application_Fundamentals_Part2.html
1,性能改进
如果使用Bitmap对象,对于每一个彩色图像帧,都会创建一个新的Bitmap对象。由于Kinect视频摄像头默认采集频率为每秒30幅,所以应用程序每秒会创建30个bitmap对象,产生30次的Bitmap内存创建,对象初始化,填充像素数据等操作。这些对象很快就会变成垃圾等待垃圾回收器进行回收。对数据量小的程序来说可能影响不是很明显,但当数据量很大时,其缺点就会显现出来。
改进方法是使用WriteableBitmap对象。它位于System.Windows.Media.Imaging命名空间下面,该对象被用来处理需要频繁更新的像素数据。当创建WriteableBitmap时,应用程序需要指定它的高度,宽度以及格式,以使得能够一次性为WriteableBitmap创建好内存,以后只需根据需要更新像素即可。
private WriteableBitmap colorImageBitmap;
private Int32Rect colorImageBitmapRect;
private int colorImageStride;
private byte[] colorImagePixelData; if (kinectSensor != null)
{
ColorImageStream colorStream=kinectSensor.ColorStream;
colorStream.Enable();
this.colorImageBitMap = new WriteableBitmap(colorStream.FrameWidth, colorStream.FrameHeight,
, , PixelFormats.Bgr32, null);
this.colorImageBitmapRect = new Int32Rect(, , colorStream.FrameWidth, colorStream.FrameHeight);
this.colorImageStride = colorStream.FrameWidth * colorStream.FrameBytesPerPixel;
ColorImageElement.Source = this.colorImageBitMap; kinectSensor.ColorFrameReady += kinectSensor_ColorFrameReady;
kinectSensor.Start();
}
private void Kinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
{
using (ColorImageFrame frame = e.OpenColorImageFrame())
{
if (frame != null)
{
byte[] pixelData = new byte[frame.PixelDataLength];
frame.CopyPixelDataTo(pixelData);
this.colorImageBitmap.WritePixels(this.colorImageBitmapRect, pixelData, this.colorImageStride, );
}
}
}
基于Kinect的应用程序在无论是在显示ColorImageStream数据还是显示DepthImageStream数据的时候,都应该使用WriteableBitmap对象来显示帧影像。在最好的情况下,彩色数据流会每秒产生30帧彩色影像,这意味着对内存资源的消耗比较大。WriteableBitmap能够减少这种内存消耗,减少需要更新影响带来的内存开辟和回收操作。毕竟在应用中显示帧数据不是应用程序的最主要功能,所以在这方面减少内像存消耗显得很有必要。
2,简单的图像处理
每一帧ColorImageFrame都是以字节序列的方式返回原始的像素数据。应用程序必须以这些数据创建图像。这意味这我们可以对这些原始数据进行一定的处理,然后再展示出来。
void kinectSensor_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
{
using (ColorImageFrame frame = e.OpenColorImageFrame())
{
if (frame != null)
{
byte[] pixelData = new byte[frame.PixelDataLength];
frame.CopyPixelDataTo(pixelData);
for (int i = ; i < pixelData.Length; i += frame.BytesPerPixel)
{
pixelData[i] = 0x00;//蓝色
pixelData[i + ] = 0x00;//绿色
}
this.colorImageBitMap.WritePixels(this.colorImageBitmapRect, pixelData,this.colorImageStride,);
}
}
}
for循环遍历每个像素,使得i的起始位置重视该像素的第一个字节。由于数据的格式是Bgr32,即RGB32位(一个像素共占4个字节,每个字节8位),所以第一个字节是蓝色通道,第二个是绿色,第三个是红色。循环体类,将第一个和第二个通道设置为0.所以输出的代码中只用红色通道的信息。这类操作通常很消耗计算资源。像素着色通常是GPU上的一些很基础的操作。
Inverted Color 反色
pixelData[i]=(byte)~pixelData[i];
pixelData[i+1]=(byte)~pixelData[i+1];
pixelData[i+2]=(byte)~pixelData[i+2];
Apocalyptic Zombie
pixelData[i]= pixelData[i+1];
pixelData[i+1]= pixelData[i];
pixelData[i+2]=(byte)~pixelData[i+2];
Gray scale
byte gray=Math.Max(pixelData[i],pixelData[i+1])
gray=Math.Max(gray,pixelData[i+2]);
pixelData[i]=gray;
pixelData[i+1]=gray;
pixelData[i+2]=gray;
Grainy black and white movie
byte gray=Math.Min(pixelData[i],pixelData[i+1]);
gray=Math.Min(gray,pixelData[i+2]);
pixelData[i]=gray;
pixelData[i+1]=gray;
pixelData[i+2] =gray;
Washed out color
double gray=(pixelData[i]*0.11)+(pixelData[i+1]*0.59)+(pixelData[i+2]*0.3);
double desaturation=0.75;
pixelData[i]=(byte)(pixelData[i]+desaturation*(gray-pixelData[i]));
pixelData[i+1]=(byte)(pixelData[i+1]+desaturation*(gray-pixelData[i+1]));
pixelData[i+2]=(byte)(pixelData[i+2]+desatuation*(gray-pixelData[i+2]));
High saturation
If (pixelData[i]<0x33||pixelData[i]>0xE5) { pixelData[i]=0x00; } else { pixelData[i]=0Xff; } If (pixelData[i+]<0x33||pixelData[i+]>0xE5) { pixelData[i+]=0x00; } else { pixelData[i+]=0Xff; } If (pixelData[i+]<0x33||pixelData[i+]>0xE5) { pixelData[i+]=0x00; } else { pixelData[i+]=0Xff; }
3,ColorImageStream 对象图
ColorImageStream是KinectSensor对象的一个属性,如同KinectSensorde其它流一样,色彩数据流在使用之前需要调用Enable方法。ColorImageStream有一个重载的Enabled方法,默认的Eanbled方法没有参数,重载的方法有一个ColorImageFormat参数,他是一个枚举类型,可以使用这个参数指定图像格式。下表列出了枚举成员。默认的Enabled将ColorImageStream设置为每秒30帧的640*480的RGB影像数据。一旦调用Enabled方法后,就可以通过对象的Foramt属性获取到图像的格式了。
ColorImageStream 有5个属性可以设置摄像头的视场。这些属性都以Nominal开头,当Stream被设置好后,这些值对应的分辨率就设置好了。一些应用程序可能需要基于摄像头的光学属性比如视场角和焦距的长度来进行计算。ColorImageStream建议程序员使用这些属性,以使得程序能够面对将来分辨率的变化。
ImageStream是ColorImageStream的基类。因此ColorImageStream集成了4个描述每一帧每一个像素数据的属性。在之前的代码中,我们使用这些属性创建了一个WriteableBitmap对象。这些属性与ColorImageFormat的设置有关。ImageStream中除了这些属性外还有一个IsEnabled属性和Disable方法。IsEnabled属性是一个只读的。当Stream打开时返回true,当调用了Disabled方法后就返回false了。Disable方法关闭Stream流,之后数据帧的产生就会停止,ColorFrameReady事件的触发也会停止。当ColorImageStream设置为可用状态后,就能产生ColorImageFrame对象。ColorImageFrame对象很简单。他有一个Format方法,他是父类的ColorImageFormat值。他只有一个CopyPixelDataTo方法,能够将图像的像素数据拷贝到指定的byte数组中,只读的PixelDataLength属性定义了数组的大小PixelDataLength属性通过对象的宽度,高度以及每像素多少位属性来获得的。这些属性都继承自ImageFrame抽象类。
数据流的格式决定了像素的格式,如果数据流是以ColorImageFormat.RgbResolution640*480Fps30格式初始化的,那么像素的格式就是Bgr32,它表示每一个像素占32位(4个字节),第一个字节表示蓝色通道值,第二个表示绿色,第三个表示红色。第四个待用。当像素的格式是Bgra32时,第四个字节表示像素的alpha或者透明度值。如果一个图像的大小是640*480,那么对于的字节数组有122880个字节(width*height*BytesPerPixel=640*480*4).在处理影像时有时候也会用到Stride这一术语,他表示影像中一行的像素所占的字节数,可以通过图像的宽度乘以每一个像素所占字节数得到。
除了描述像素数据的属性外,ColorImageFrame对象还有一些列描述本身的属性。Stream会为每一帧编一个号,这个号会随着时间顺序增长。应用程序不要假的每一帧的编号都比前一帧恰好大1,因为可能出现跳帧现象。另外一个描述帧的属性是Timestamp。他存储自KinectSensor开机(调用Start方法)以来经过的毫秒数。当每一次KinectSensor开始时都会复位为0。
4,获取数据的方式: 事件 VS “拉”
事件在WPF中应用很广泛,在数据或者状态发生变化时,事件机制能够通知应用程序。对于大多数基于Kinect开发的应用程序来说基于事件的数据获取方式已经足够
当使用事件模型时,应用程序注册数据流的frame-ready事件,为其指定方法。每当事件触发时,注册方法将会调用事件的属性来获取数据帧。例如,在使用彩色数据流时,方法调用ColorImageFrameReadyEventArgs对象的OpenColorImageFrame方法来获取ColorImageFrame对象。程序应该测试获取的ColorImageFrame对象是否为空,因为有可能在某些情况下,虽然事件触发了,但是没有产生数据帧。除此之外,事件模型不需要其他的检查和异常处理
“拉”数据的方式就是应用程序会在某一时间询问数据源是否有新数据,如果有,就加载。每一个Kinect数据流都有一个称之为OpenNextFrame的方法。当调用OpenNextFrame的方式时,应用程序可以给定一个超时的值,这个值就是应用程序愿意等待新数据返回的最长时间,以毫秒记。方法试图在超时之前获取到新的数据帧。如果超时,方法将会返回一个null值。
OpenNextFrame方法在KinectSensor没有运行、Stream没有初始化或者在使用事件获取帧数据的时候都有可能会产生InvalidOperationException异常。应用程序可以自由选择何种数据获取模式,比如使用事件方式获取ColorImageStream产生的数据,同时采用“拉”的方式从SkeletonStream流获取数据。但是不能对同一数据流使用这两种模式。AllFrameReady事件包括了所有的数据流—意味着如果应用程序注册了AllFrameReady事件。任何试图以拉的方式获取流中的数据都会产生InvalidOperationException异常。
在展示如何以拉的模式从数据流中获取数据之前,理解使用模式获取数据的场景很有必要。使用“拉”数据的方式获取数据的最主要原因是性能,只在需要的时候采取获取数据。他的缺点是,实现起来比事件模式复杂。除了性能,应用程序的类型有时候也必须选择“拉”数据的这种模式。SDK也能用于XNA,他不同与WPF,它不是事件驱动的。当需要使用XNA开发游戏时,必须使用拉模式来获取数据。使用SDK也能创建没有用户界面的控制台应用程序。设想开发一个使用Kinect作为眼睛的机器人应用程序,他通过源源不断的主动从数据流中读取数据然后输入到机器人中进行处理,在这个时候,拉模型是比较好的获取数据的方式。
private KinectSensor _Kinect;
private WriteableBitmap _ColorImageBitmap;
private Int32Rect _ColorImageBitmapRect;
private int _ColorImageStride;
private byte[] _ColorImagePixelData;
public MainWindow()
{
InitializeComponent();
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
private void CompositionTarget_Rendering(object sender, EventArgs e)
{
DiscoverKinectSensor();
PollColorImageStream();
}在构造函数中我们将Rendering事件绑定到CompositionTarget对象上。ComposationTarget对象表示应用程序中可绘制的界面。Rendering事件会在每一个渲染周期上触发。我们需要使用循环来取新的数据帧。有两种方式来创建循环。一种是使用线程,将在下一节中介绍。另一种方式是使用普通的循环语句。使用CompositionTarget对象有一个缺点,就是Rendering事件中如果处理时间过长会导致UI线程问题。因为时间处理在主UI线程中。所以不应在事件中做一些比较耗时的操作。Redering 事件中的代码需要做四件事情。必须发现一个连接的KinectSnesor,初始化传感器。响应传感器状态的变化,以及拉取新的数据并对数据进行处理。
private void DiscoverKinectSensor()
{
if(this._Kinect != null && this._Kinect.Status != KinectStatus.Connected)
{
this._Kinect = null;
} if(this._Kinect == null)
{
this._Kinect = KinectSensor.KinectSensors.FirstOrDefault(x => x.Status == KinectStatus.Connected); if(this._Kinect != null)
{
this._Kinect.ColorStream.Enable();
this._Kinect.Start(); ColorImageStream colorStream = this._Kinect.ColorStream;
this._ColorImageBitmap = new WriteableBitmap(colorStream.FrameWidth, colorStream.FrameHeight, , , PixelFormats.Bgr32, null);
this._ColorImageBitmapRect = new Int32Rect(, , colorStream.FrameWidth, colorStream.FrameHeight);
this._ColorImageStride = colorStream.FrameWidth * colorStream.FrameBytesPerPixel;
this.ColorImageElement.Source = this._ColorImageBitmap;
this._ColorImagePixelData = new byte[colorStream.FramePixelDataLength];
}
}
}下面的代码列出了PollColorImageStream方法的实现。代码首先判断是否有KinectSensor可用.然后调用OpneNextFrame方法获取新的彩色影像数据帧。代码获取新的数据后,然后更新WriteBitmap对象。这些操作包在using语句中,因为调用OpenNextFrame对象可能会抛出异常。在调用OpenNextFrame方法时,将超时时间设置为了100毫秒。合适的超时时间设置能够使得程序在即使有一两帧数据跳过时仍能够保持流畅。我们要尽可能的让程序每秒产生30帧左右的数据。
private void PollColorImageStream()
{
if(this._Kinect == null)
{
//TODO: Display a message to plug-in a Kinect.
}
else
{
try
{
using(ColorImageFrame frame = this._Kinect.ColorStream.OpenNextFrame())
{
if(frame != null)
{
frame.CopyPixelDataTo(this._ColorImagePixelData);
this._ColorImageBitmap.WritePixels(this._ColorImageBitmapRect, this._ColorImagePixelData, this._ColorImageStride, );
}
}
}
catch(Exception ex)
{
//TODO: Report an error message
}
}
}总体而言,采用拉模式获取数据的性能应该好于事件模式。上面的例子展示了使用拉方式获取数据,但是它有另一个问题。使用CompositionTarget对象,应用程序运行在WPF的UI线程中。任何长时间的数据处理或者在获取数据时超时 时间的设置不当都会使得程序变慢甚至无法响应用户的行为,因为这些操作都执行在UI线程上。解决方法是创建一个新的线程,然后在这个线程上执行数据获取和处理操作。 在.net中使用BackgroundWorker类能够简单的解决这个问题。代码如下:
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
if(worker != null)
{
while(!worker.CancellationPending)
{
DiscoverKinectSensor();
PollColorImageStream();
}
}
}
首先,在变量声明中加入了一个BackgroundWorker变量 _Worker。在构造函数中,实例化了一个BackgroundWorker类,并注册了DoWork事件,启动了新的线程。当线程开始时就会触发DoWork事件。事件不断循环知道被取消。在循环体中,会调用DiscoverKinectSensor和PollColorImageStream方法。如果直接使用之前例子中的这两个方法,你会发现会出现InvalidOperationException异常,错误提示为“The calling thread cannot access this object because a different thread owns it”。这是由于,拉数据在background线程中,但是更新UI元素却在另外一个线程中。在background线程中更新UI界面,需要使用Dispatch对象。WPF中每一个UI元素都有一个Dispathch对象。下面是两个方法的更新版本:
private void DiscoverKinectSensor()
{
if(this._Kinect != null && this._Kinect.Status != KinectStatus.Connected)
{
this._Kinect = null;
} if(this._Kinect == null)
{
this._Kinect = KinectSensor.KinectSensors
.FirstOrDefault(x => x.Status == KinectStatus.Connected);
if(this._Kinect != null)
{
this._Kinect.ColorStream.Enable();
this._Kinect.Start();
ColorImageStream colorStream = this._Kinect.ColorStream;
this.ColorImageElement.Dispatcher.BeginInvoke(new Action(() =>
{
this._ColorImageBitmap = new WriteableBitmap(colorStream.FrameWidth, colorStream.FrameHeight, , , PixelFormats.Bgr32, null);
this._ColorImageBitmapRect = new Int32Rect(, , colorStream.FrameWidth, colorStream.FrameHeight);
this._ColorImageStride = colorStream.FrameWidth * colorStream.FrameBytesPerPixel;
this._ColorImagePixelData = new byte[colorStream.FramePixelDataLength]; this.ColorImageElement.Source = this._ColorImageBitmap;
}));
}
}
}
private void PollColorImageStream()
{
if(this._Kinect == null)
{
//TODO: Notify that there are no available sensors.
}
else
{
try
{
using(ColorImageFrame frame = this._Kinect.ColorStream.OpenNextFrame())
{
if(frame != null)
{
frame.CopyPixelDataTo(this._ColorImagePixelData); this.ColorImageElement.Dispatcher.BeginInvoke(new Action(() =>
{
this._ColorImageBitmap.WritePixels(this._ColorImageBitmapRect, this._ColorImagePixelData, this._ColorImageStride, );
}));
}
}
}
catch(Exception ex)
{
//TODO: Report an error message
}
}
}
“拉”模式获取数据跟事件模式相比有很多独特的好处,但它增加了代码量和程序的复杂度。在大多数情况下,事件模式获取数据的方法已经足够,我们应该使用该模式而不是“拉”模式。唯一不能使用事件模型获取数据的情况是在编写非WPF平台的应用程序的时候。比如,当编写XNA或者其他的采用拉模式架构的应用程序。建议在编写基于WPF平台的Kinect应用程序时采用事件模式来获取数据。只有在极端注重性能的情况下才考虑使用“拉”的方式。
完整代码:
namespace TestBasic_Poll
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
private KinectSensor _kinect;
private WriteableBitmap _colorImageBitmap;
private Int32Rect _colorImageBitmapRect;
private int _colorImageStride;
private byte[] _colorImagePixelData;
private BackgroundWorker bWorker; public MainWindow()
{
InitializeComponent();
bWorker = new BackgroundWorker();
bWorker.DoWork += new DoWorkEventHandler(Worker_DoWork);
bWorker.RunWorkerAsync(this);
} private void Worker_DoWork(Object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
if (worker!=null)
{
while (!worker.CancellationPending)
{
DiscoverKinectSensor();
PollColorImageStream();
}
}
} private void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (null != this._kinect)
{
this._kinect.Stop();
}
} private void DiscoverKinectSensor()
{
if (this._kinect!=null&&this._kinect.Status!=KinectStatus.Connected)
{
this._kinect = null;
}
if (this._kinect==null)
{
this._kinect = KinectSensor.KinectSensors.FirstOrDefault(x=>x.Status==KinectStatus.Connected); if (this._kinect!=null)
{
this._kinect.ColorStream.Enable();
this._kinect.Start();
ColorImageStream colorStream = this._kinect.ColorStream;
this.ColorImageElement.Dispatcher.BeginInvoke(new Action(() =>
{ // 匿名内部类
this._colorImageBitmap = new WriteableBitmap(colorStream.FrameWidth, colorStream.FrameHeight, , , PixelFormats.Bgr32, null);
this._colorImageBitmapRect = new Int32Rect(, , colorStream.FrameWidth, colorStream.FrameHeight);
this._colorImageStride = colorStream.FrameWidth * colorStream.FrameBytesPerPixel;
this._colorImagePixelData = new byte[colorStream.FramePixelDataLength];
this.ColorImageElement.Source = this._colorImageBitmap;
}));
}
}
} private void PollColorImageStream()
{
if (this._kinect==null)
{
}
else
{
try
{
using (ColorImageFrame frame = this._kinect.ColorStream.OpenNextFrame())
{
if (frame!=null)
{
frame.CopyPixelDataTo(this._colorImagePixelData);
this.ColorImageElement.Dispatcher.BeginInvoke(new Action(() =>
{
this._colorImageBitmap.WritePixels(this._colorImageBitmapRect, this._colorImagePixelData, this._colorImageStride, );
}
));
}
}
}
catch (System.Exception ex)
{ }
}
} }
}
Kinect开发 —— 基础知识的更多相关文章
- 3D开发基础知识和简单示例
引言 现在物联网概念这么火,如果监控的信息能够实时在手机的客服端中以3D形式展示给我们,那种体验大家可以发挥自己的想象. 那生活中我们还有很多地方用到这些,如上图所示的Kinect 在医疗上的应用,当 ...
- IOS开发基础知识碎片-导航
1:IOS开发基础知识--碎片1 a:NSString与NSInteger的互换 b:Objective-c中集合里面不能存放基础类型,比如int string float等,只能把它们转化成对象才可 ...
- iOS开发——总结篇&IOS开发基础知识
IOS开发基础知识 1:Objective-C语法之动态类型(isKindOfClass, isMemberOfClass,id) 对象在运行时获取其类型的能力称为内省.内省可以有多种方法实现. 判断 ...
- Ext常用开发基础知识
Ext常用开发基础知识 组件定义 //这种方法可以缓存所需要的组件 调用起来比较方便(方法一 ) Ext.define('MySecurity.view.home.HomePanel', { //添加 ...
- IM开发基础知识补课:正确理解前置HTTP SSO单点登陆接口的原理
1.前言 一个安全的信息系统,合法身份检查是必须环节.尤其IM这种以“人”为中心的社交体系,身份认证更是必不可少. 一些PC时代小型IM系统中,身份认证可能直接做到长连接中(也就是整个IM系统都是以长 ...
- IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列
1.引言 消息是互联网信息的一种表现形式,是人利用计算机进行信息传递的有效载体,比如即时通讯网坛友最熟悉的即时通讯消息就是其具体的表现形式之一. 消息从发送者到接收者的典型传递方式有两种: 1)一种我 ...
- [No0000138]软件开发基础知识
1. 本文目的 本文目的在于,介绍软件开发的各种基础知识 以实现,看了之后,对于软件开发的很多领域的基础知识有所了解 如此在进行后续的真正的软件开发时,遇到各种细节知识,才会明白由来和背景知识 第 1 ...
- IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token
本文引用了简书作者“骑小猪看流星”技术文章“Cookie.Session.Token那点事儿”的部分内容,感谢原作者. 1.前言 众所周之,IM是个典型的快速数据流交换系统,当今主流IM系统(尤其移动 ...
- IM开发基础知识补课(七):主流移动端账号登录方式的原理及设计思路
1.引言 在即时通讯网经常能看到各种高大上的高并发.分布式.高性能架构设计方面的文章,平时大家参加的众多开发者大会,主题也都是各种高大上的话题——什么5G啦.AI人工智能啦.什么阿里双11分分钟多少万 ...
随机推荐
- Sublime Text 3破解
----- BEGIN LICENSE ----- sgbteam Single User License EA7E- 8891CBB9 F1513E4F 1A3405C1 A865D53F 115F ...
- 洛谷 P2542 [AHOI2005]航线规划 树链剖分_线段树_时光倒流_离线
Code: #include <map> #include <cstdio> #include <algorithm> #include <cstring&g ...
- [国家集训队]整数的lqp拆分 数学推导 打表找规律
题解: 考场上靠打表找规律切的题,不过严谨的数学推导才是本题精妙所在:求:$\sum\prod_{i=1}^{m}F_{a{i}}$ 设 $f(i)$ 为 $N=i$ 时的答案,$F_{i}$ 为斐波 ...
- vue-router路由配置
转自http://www.cnblogs.com/padding1015/ 两种配置方法:在main.js中 || 在src/router文件夹下的index.js中 src/router/index ...
- Sqlite 命令行导出、导入数据(直接支持CSV)
打开命令行 导出数据到data.csv D:\project>sqlite3.exe old.db SQLite version 3.21.0 2017-10-24 18:55:49 Enter ...
- springMVC的一些配置解析
<mvc:annotation-driven /> <!-- 启动注解驱动的Spring MVC功能,注册请求url和注解POJO类方法的映射--> 是一种简写形式,完全可以手 ...
- Container详解
Container是一个拥有绘制.定位.调整大小的widget. padding和margin padding和margin分别设置Container的内边距和外边距.可取值包括下面四个: EdgeI ...
- Lambda表达式详细总结
(一)输入参数 在Lambda表达式中,输入参数是Lambda运算符的左边部分.它包含参数的数量可以为0.1或者多个.只有当输入参数为1时,Lambda表达式左边的一对小括弧才可以省略.输入参数的数量 ...
- 记录一下Memcached的用法:
首先就是先要配置Memcached,这个回头再写. https://zhidao.baidu.com/question/809745125827797732.html https://www.cnbl ...
- 分享js中 pageY = clientY + document.body.scrollTop 之间的关系
//这里没有考虑兼容ie模式下 兼容一般主流浏览器 var $1 = document.getElementById('main') $1.onclick = function(e){ console ...