多数民众赞成我是如何编写你的漂亮代码的(为了便于理解我的一些简单修改)
private void Form1_Load(object sender, EventArgs e)
{
prev = GetDesktopImage();//get a screenshot of the desktop;
cur = GetDesktopImage();//get a screenshot of the desktop;
var locked1 = cur.LockBits(new Rectangle(0, 0, cur.Width, cur.Height),
ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
var locked2 = prev.LockBits(new Rectangle(0, 0, prev.Width, prev.Height),
ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
ApplyXor(locked1, locked2);
compressionBuffer = new byte[1920* 1080 * 4];
// Compressed buffer -- where the data goes that we'll send.
int backbufSize = LZ4.LZ4Codec.MaximumOutputLength(this.compressionBuffer.Length) + 4;
backbuf = new CompressedCaptureScreen(backbufSize);
MessageBox.Show(compressionBuffer.Length.ToString());
int length = Compress();
MessageBox.Show(backbuf.Data.Length.ToString());//prints the new buffer size
}
Run Code Online (Sandbox Code Playgroud)
压缩缓冲区长度是例如 8294400
,backbuff.Data.length是8326947
我不喜欢压缩建议,所以这就是我要做的.
你不想压缩视频流(所以MPEG,AVI等是不可能的 - 这些不一定是实时的)你不想压缩单个图片(因为那只是愚蠢的).
基本上你想要做的是检测事情是否发生变化并发送差异.你正走在正确的轨道上; 大多数视频压缩器都这样做.您还需要一种快速压缩/解压缩算法; 特别是如果你去更多相关的FPS.
差异.首先,消除代码中的所有分支,并确保内存访问是顺序的(例如,在内循环中迭代x).后者将为您提供缓存局部性.至于差异,我可能会使用64位异或; 它简单,无分支,快速.
如果你想要性能,那么在C++中做这件事可能会更好:当前的C#实现没有对你的代码进行矢量化,这对你有很大帮助.
做这样的事情(我假设是32位像素格式):
for (int y=0; y<height; ++y) // change to PFor if you like
{
ulong* row1 = (ulong*)(image1BasePtr + image1Stride * y);
ulong* row2 = (ulong*)(image2BasePtr + image2Stride * y);
for (int x=0; x<width; x += 2)
row2[x] ^= row1[x];
}
Run Code Online (Sandbox Code Playgroud)
快速压缩和解压缩通常意味着更简单的压缩算法.https://code.google.com/p/lz4/就是这样一种算法,并且还有适当的.NET端口.您可能想要了解它是如何工作的; 在LZ4中有一个流媒体功能,如果你可以让它处理2个图像而不是1个,这可能会给你一个很好的压缩提升.
总而言之,如果你试图压缩白噪声,它将无法工作,你的帧速率将下降.解决此问题的一种方法是,如果框架中有太多"随机性",则减少颜色.随机性的度量是熵,有几种方法可以衡量图片的熵(https://en.wikipedia.org/wiki/Entropy_(information_theory)) )).我会坚持一个非常简单的方法:检查压缩图片的大小 - 如果它高于某个限制,减少位数; 如果在下面,增加位数.
注意,在这种情况下,不会通过移位来增加和减少位.你不需要删除你的位,你只需要你的压缩工作更好.使用带有位掩码的简单"AND"可能同样出色.例如,如果要删除2位,可以这样做:
for (int y=0; y<height; ++y) // change to PFor if you like
{
ulong* row1 = (ulong*)(image1BasePtr + image1Stride * y);
ulong* row2 = (ulong*)(image2BasePtr + image2Stride * y);
ulong mask = 0xFFFCFCFCFFFCFCFC;
for (int x=0; x<width; x += 2)
row2[x] = (row2[x] ^ row1[x]) & mask;
}
Run Code Online (Sandbox Code Playgroud)
PS:我不确定我会用alpha组件做什么,我会把它留给你的实验.
祝好运!
答案很长
我有空闲时间,所以我只测试了这种方法.这里有一些支持它的代码.
这段代码通常运行超过130 FPS,笔记本电脑上有一个很好的恒定内存压力,所以瓶颈不应该在这里了.请注意,您需要LZ4来实现这一功能,并且LZ4的目标是高速,而不是高压缩比.稍后再说一点.
首先,我们需要一些东西来保存我们要发送的所有数据.我不是在这里实现套接字的东西(虽然这应该是非常简单的使用它作为一个开始),我主要专注于获取你需要发送一些东西的数据.
// The thing you send over a socket
public class CompressedCaptureScreen
{
public CompressedCaptureScreen(int size)
{
this.Data = new byte[size];
this.Size = 4;
}
public int Size;
public byte[] Data;
}
Run Code Online (Sandbox Code Playgroud)
我们还需要一个能掌握所有魔力的课程:
public class CompressScreenCapture
{
Run Code Online (Sandbox Code Playgroud)
接下来,如果我正在运行高性能代码,我会习惯首先预先分配所有缓冲区.这将节省您在实际算法期间的时间.4个1080p的缓冲区大约是33 MB,这很好 - 所以让我们分配它.
public CompressScreenCapture()
{
// Initialize with black screen; get bounds from screen.
this.screenBounds = Screen.PrimaryScreen.Bounds;
// Initialize 2 buffers - 1 for the current and 1 for the previous image
prev = new Bitmap(screenBounds.Width, screenBounds.Height, PixelFormat.Format32bppArgb);
cur = new Bitmap(screenBounds.Width, screenBounds.Height, PixelFormat.Format32bppArgb);
// Clear the 'prev' buffer - this is the initial state
using (Graphics g = Graphics.FromImage(prev))
{
g.Clear(Color.Black);
}
// Compression buffer -- we don't really need this but I'm lazy today.
compressionBuffer = new byte[screenBounds.Width * screenBounds.Height * 4];
// Compressed buffer -- where the data goes that we'll send.
int backbufSize = LZ4.LZ4Codec.MaximumOutputLength(this.compressionBuffer.Length) + 4;
backbuf = new CompressedCaptureScreen(backbufSize);
}
private Rectangle screenBounds;
private Bitmap prev;
private Bitmap cur;
private byte[] compressionBuffer;
private int backbufSize;
private CompressedCaptureScreen backbuf;
private int n = 0;
Run Code Online (Sandbox Code Playgroud)
首先要做的是捕获屏幕.这是一个简单的部分:只需填写当前屏幕的位图:
private void Capture()
{
// Fill 'cur' with a screenshot
using (var gfxScreenshot = Graphics.FromImage(cur))
{
gfxScreenshot.CopyFromScreen(screenBounds.X, screenBounds.Y, 0, 0, screenBounds.Size, CopyPixelOperation.SourceCopy);
}
}
Run Code Online (Sandbox Code Playgroud)
正如我所说,我不想压缩'原始'像素.相反,我宁愿压缩先前和当前图像的XOR蒙版.大多数情况下,这将给你很多0,这很容易压缩:
private unsafe void ApplyXor(BitmapData previous, BitmapData current)
{
byte* prev0 = (byte*)previous.Scan0.ToPointer();
byte* cur0 = (byte*)current.Scan0.ToPointer();
int height = previous.Height;
int width = previous.Width;
int halfwidth = width / 2;
fixed (byte* target = this.compressionBuffer)
{
ulong* dst = (ulong*)target;
for (int y = 0; y < height; ++y)
{
ulong* prevRow = (ulong*)(prev0 + previous.Stride * y);
ulong* curRow = (ulong*)(cur0 + current.Stride * y);
for (int x = 0; x < halfwidth; ++x)
{
*(dst++) = curRow[x] ^ prevRow[x];
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
对于压缩算法,我只是将缓冲区传递给LZ4并让它发挥其魔力.
private int Compress()
{
// Grab the backbuf in an attempt to update it with new data
var backbuf = this.backbuf;
backbuf.Size = LZ4.LZ4Codec.Encode(
this.compressionBuffer, 0, this.compressionBuffer.Length,
backbuf.Data, 4, backbuf.Data.Length-4);
Buffer.BlockCopy(BitConverter.GetBytes(backbuf.Size), 0, backbuf.Data, 0, 4);
return backbuf.Size;
}
Run Code Online (Sandbox Code Playgroud)
这里需要注意的一点是,我习惯将所有内容都放在我需要通过TCP/IP套接字发送的缓冲区中.如果我可以轻松地避开数据,我不想移动数据,所以我只是将我需要的所有内容放在另一边.
至于套接字本身,你可以在这里使用同步TCP套接字(我愿意),但是如果你这样做,你将需要添加一个额外的缓冲区.
唯一剩下的就是将所有内容粘合在一起并在屏幕上显示一些统计信息:
public void Iterate()
{
Stopwatch sw = Stopwatch.StartNew();
// Capture a screen:
Capture();
TimeSpan timeToCapture = sw.Elapsed;
// Lock both images:
var locked1 = cur.LockBits(new Rectangle(0, 0, cur.Width, cur.Height),
ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
var locked2 = prev.LockBits(new Rectangle(0, 0, prev.Width, prev.Height),
ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
try
{
// Xor screen:
ApplyXor(locked2, locked1);
TimeSpan timeToXor = sw.Elapsed;
// Compress screen:
int length = Compress();
TimeSpan timeToCompress = sw.Elapsed;
if ((++n) % 50 == 0)
{
Console.Write("Iteration: {0:0.00}s, {1:0.00}s, {2:0.00}s " +
"{3} Kb => {4:0.0} FPS \r",
timeToCapture.TotalSeconds, timeToXor.TotalSeconds,
timeToCompress.TotalSeconds, length / 1024,
1.0 / sw.Elapsed.TotalSeconds);
}
// Swap buffers:
var tmp = cur;
cur = prev;
prev = tmp;
}
finally
{
cur.UnlockBits(locked1);
prev.UnlockBits(locked2);
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,我减少了控制台输出,以确保不是瓶颈.:-)
简单的改进
压缩所有这些0会有点浪费,对吗?使用简单的布尔值跟踪包含数据的最小和最大y位置非常容易.
ulong tmp = curRow[x] ^ prevRow[x];
*(dst++) = tmp;
hasdata |= tmp != 0;
Run Code Online (Sandbox Code Playgroud)
Compress
如果你不需要,你也可能不想打电话.
添加此功能后,您将在屏幕上看到以下内容:
迭代:0.00s,0.01s,0.01s 1 Kb => 152.0 FPS
使用其他压缩算法也可能有所帮助.我坚持使用LZ4,因为它使用简单,速度快,压缩效果非常好 - 还有其他选项可能更好用.有关比较,请参见http://fastcompression.blogspot.nl/.
如果连接错误或者通过远程连接传输视频,则所有这些都无法正常工作.最好在这里减少像素值.这很简单:在xor期间将一个简单的64位掩码应用于前一个和当前的图片......你也可以尝试使用索引颜色 - 无论如何,你可以在这里尝试很多不同的东西; 我只是保持简单,因为这可能足够好了.
你也可以Parallel.For
用于xor循环; 我个人并不在乎.
更具挑战性
如果您有1台服务器为多个客户端提供服务,那么事情会变得更具挑战性,因为它们会以不同的速率刷新.我们希望最快速刷新客户端来确定服务器速度 - 而不是最慢.:-)
为了实现这一点,之间的关系prev
,并cur
有可能改变.如果我们像这里一样"离开",我们最终会在较慢的客户端上看到完全乱码的图片.
为了解决这个问题,我们不再需要交换prev
,因为它应该保存关键帧(当压缩数据变得太大时你会刷新)并且cur
将保存来自'xor'结果的增量数据.这意味着你可以基本上抓住任意'xor'red帧并通过线发送它 - 只要prev
位图是最近的.