/// <summary> /// Retuns true if these two blocks the same /// </summary> public bool IsMatch(int x1, int y1, int width, int height, Bitmap32Bits bm2, int x2, int y2) { Debug.Assert(x1 >= 0 && x1 + width <= mWidth); Debug.Assert(y1 >= 0 && y1 + height <= mHeight); Debug.Assert(x2 >= 0 && x2 + width <= bm2.mWidth); Debug.Assert(y2 >= 0 && y2 + height <= bm2.mHeight); // Check for match int *bmPtr1 = mScan0 + y1 * mStrideInts + x1; int *bmPtr2 = bm2.mScan0 + y2 * bm2.mStrideInts + x2; while (--height >= 0) { int w = width; while (--w >= 0) { if (*bmPtr1++ != *bmPtr2++) { return(false); } } bmPtr1 += mStrideInts - width; bmPtr2 += bm2.mStrideInts - width; } return(true); }
public BlockWriter(Bitmap32Bits source, Bitmap32Bits target, int blockSize, Dictionary <int, int> duplicates) { mSource = source; mTarget = target; mBlockSize = blockSize; // The duplicates map from source index to source index, // but we want to map directly to target index. mDuplicates = duplicates; mDuplicatedSourceToTarget = new Dictionary <int, int>(); foreach (var duplicate in mDuplicates) { mDuplicatedSourceToTarget[duplicate.Value] = -1; } // Number of blocks visible (even if partially visible) mSourceBlocksX = (source.Width + mBlockSize - 1) / mBlockSize; mTargetBlocksX = (target.Width + mBlockSize - 1) / mBlockSize; // Write bitmap header info - screen resolution and block size mSb.Append('X'); mSb.Append(source.Width); mSb.Append('Y'); mSb.Append(source.Height); mSb.Append('B'); mSb.Append(mBlockSize); }
private bool IsDuplicate(Bitmap32Bits bm, Dictionary <int, int> duplicateHashToIndex, int sourceIndex, out int duplicateIndex) { int x = ItoX(sourceIndex); int y = ItoY(sourceIndex); int width = Math.Min(mBlockSize, bm.Width - x); int height = Math.Min(mBlockSize, bm.Height - y); int hash = bm.HashInt(x, y, width, height); if (duplicateHashToIndex.ContainsKey(hash)) { // Possibly a match, but needs verification just in case there is a hash collison duplicateIndex = duplicateHashToIndex[hash]; if (bm.IsMatch(x, y, width, height, bm, ItoX(duplicateIndex), ItoY(duplicateIndex))) { return(true); } // Hash collision. This is very rare. mHashCollisions++; duplicateIndex = 0; return(false); } // First time this block has been seen duplicateHashToIndex[hash] = sourceIndex; duplicateIndex = 0; return(false); }
/// <summary> /// Build a bitmap with any of the given score bits set /// </summary> Frame BuildBitmap(Bitmap32Bits frame, List <Block> blocks, Dictionary <int, int> duplicates, Score score, ImageFormat format) { // Count blocks to write to target int numTargetBlocks = 0; foreach (var block in blocks) { if ((block.Score & score & (Score.Jpg | Score.Png)) != Score.None) { numTargetBlocks++; } } // Generate the bitmap int size = (int)Math.Sqrt(numTargetBlocks) + 1; var bmTarget = new Bitmap(size * mBlockSize, size * mBlockSize, PixelFormat.Format32bppArgb); var bmdTarget = new Bitmap32Bits(bmTarget, ImageLockMode.WriteOnly); var blockWriter = new BlockWriter(frame, bmdTarget, mBlockSize, duplicates); foreach (var block in blocks) { if ((block.Score & score) != Score.None) { blockWriter.Write(block); } } bmdTarget.Dispose(); // Compress frame to stream var targetStream = new MemoryStream(); bmTarget.Save(targetStream, format); bmTarget.Dispose(); return(new Frame(blockWriter.GetDrawString(), targetStream, format)); }
/// <summary> /// Copy the screen, block for up to 2 seconds if nothing changes. /// </summary> void CopyScreen(RemoteEvents request, FrameCollector collector, CollectRequest collectRequest) { if (mOptions.FullThrottle) { // Need the lock to wait for previous throttle (if any) to end mLastScreenHash = 0; // Ask CopyScreen blocking to end early lock (mLockScreenCopy) mLastScreenHash = 0; collector.CopyScreen(); } else { // Block until screen changes or timeout // NOTE: Holding this lock (even without the hash function // in the while loop) incurs a time penalty. This // code is the big bottleneck causing a slow frame // rate. And most of the time is spent in CopyScreen. lock (mLockScreenCopy) { long hash = 0; var screenCopyTimeout = DateTime.Now; while ((DateTime.Now - screenCopyTimeout).TotalMilliseconds < WAIT_FOR_SCREEN_CHANGE_TIMEOUT_MS) { collector.CopyScreen(); using (var bm = new Bitmap32Bits(collector.Screen, System.Drawing.Imaging.ImageLockMode.ReadOnly)) hash = bm.HashLong(0, 0, bm.Width, bm.Height); if (hash != mLastScreenHash) { break; } Thread.Sleep(20); } mLastScreenHash = hash; } } collector.ScaleScreen(mOptions.Width, mOptions.Height); }
/// <summary> /// Copy a block from this bitmap to the other bitmap. /// </summary> public void Copy(int x, int y, int width, int height, Bitmap32Bits toBm, int toX, int toY) { Debug.Assert(x >= 0 && x + width <= mWidth); Debug.Assert(y >= 0 && y + height <= mHeight); Debug.Assert(toX >= 0 && toX + width <= toBm.Width); Debug.Assert(toY >= 0 && toY + height <= toBm.Height); // Copy pixels int *fromPtr = mScan0 + y * mStrideInts + x; int *toPtr = toBm.mScan0 + toY * toBm.mStrideInts + toX; while (--height >= 0) { int w = width; while (--w >= 0) { *toPtr++ = *fromPtr++; } // Move to next line fromPtr += mStrideInts - width; toPtr += toBm.mStrideInts - width; } }
/// <summary> /// Score a frame, creating a list of blocks /// </summary> private void ScoreFrame(Bitmap32Bits frame, bool disableDelta, out List <Block> blocks, out Dictionary <int, int> duplicates, out int jpgCount, out int pngCount) { blocks = new List <Block>(); duplicates = new Dictionary <int, int>(); jpgCount = 0; pngCount = 0; var sourceIndex = -1; var duplicateHashToIndex = new Dictionary <int, int>(); for (int y = 0; y < frame.Height; y += mBlockSize) { for (int x = 0; x < frame.Width; x += mBlockSize) { sourceIndex++; Debug.Assert(ItoX(sourceIndex) == x); Debug.Assert(ItoY(sourceIndex) == y); // Skip clear blocks int width = Math.Min(mBlockSize, frame.Width - x); int height = Math.Min(mBlockSize, frame.Height - y); if (!disableDelta && frame.IsMatch(x, y, width, height, mBackground, x, y)) { continue; // Nothing changed, skip it } frame.Copy(x, y, width, height, mBackground, x, y); // Check for solid blocks if (frame.IsSolid(x, y, width, height)) { blocks.Add(new Block(sourceIndex, Score.Solid)); continue; } // Check for duplicates int duplicateIndex; if (IsDuplicate(frame, duplicateHashToIndex, sourceIndex, out duplicateIndex)) { blocks.Add(new Block(sourceIndex, Score.Duplicate)); duplicates[sourceIndex] = duplicateIndex; continue; } // Compression: SmartPNG, JPG, or PNG bool usePng = mCompression == CompressionType.Png; var score = Score.None; if (mCompression == CompressionType.SmartPng && width >= 4 && height >= 4) { // SmartPng - Check to see if the block would compress well with PNG var rleCount = frame.RleCount(x, y, width, height); var rleTotal = (height - 1) * (width - 1); var rleCompression = rleCount / (float)rleTotal; score = (Score)((int)(rleCompression * 9.99) << (int)Score.ScoreShift) & Score.ScoreMask; if (rleCompression > mPngCompressionThreshold) { if (!frame.LowFrequency(x, y, width, height)) { usePng = true; // Good compression, not low frequency } else { score |= Score.LowFrequency; } } } blocks.Add(new Block(sourceIndex, score | (usePng ? Score.Png : Score.Jpg))); if (usePng) { pngCount++; } else { jpgCount++; } } } mDuplicates = duplicates.Count; }
/// <summary> /// Analyze a bitmap, and keep a copy as the new background. If the bitmap is not /// the same size as the previous one, a new comparison is made. You must /// dispose the bitmap when done with it. Returns 1 or 2 frames (PNG/JPG) /// depending on settings /// </summary> public Frame [] Compress(Bitmap frame) { // Generate full frame JPG or PNG (debugging output) if (mOutput == OutputType.FullFrameJpg || mOutput == OutputType.FullFramePng) { var f = GenerateFullFrame((Bitmap)frame, mOutput == OutputType.FullFrameJpg ? ImageFormat.Jpeg : ImageFormat.Png); return(new Frame[] { f }); } // Force full frame analysis if requested bool disableDelta = mForceDisableDelta || mFullFrameAnalysis || Output == OutputType.HideJpg || Output == OutputType.HidePng; mForceDisableDelta = false; // Optionally create background if it doesn't exist, or the size changed if (mBackground == null || mBackground.Width != frame.Width || mBackground.Height != frame.Height) { // New background, do not compare with previous disableDelta = true; if (mBackground != null) { mBackground.Dispose(); } mBackground = new Bitmap32Bits(frame.Width, frame.Height); } // Number of blocks visible (even if partially visible) mBlocksX = (frame.Width + mBlockSize - 1) / mBlockSize; // Score bitmap mFrameIndex++; var frameBits = new Bitmap32Bits(frame, ImageLockMode.ReadOnly); List <Block> blocks; Dictionary <int, int> duplicates; int jpgCount; int pngCount; ScoreFrame(frameBits, disableDelta, out blocks, out duplicates, out jpgCount, out pngCount); // Build the bitmaps (JPG and PNG) Frame[] frames; if (jpgCount == 0 || pngCount == 0) { // Build one frame (both PNG and JPG) frames = new Frame[] { BuildBitmap(frameBits, blocks, duplicates, Score.Solid | Score.Duplicate | Score.Jpg | Score.Png, pngCount == 0 ? ImageFormat.Jpeg : ImageFormat.Png) }; } else { // Build two frames (PNG first, and JPG second) frames = new Frame[] { BuildBitmap(frameBits, blocks, duplicates, Score.Solid | Score.Duplicate | Score.Png, ImageFormat.Png), BuildBitmap(frameBits, blocks, duplicates, Score.Duplicate | Score.Jpg, ImageFormat.Jpeg) }; } frameBits.Dispose(); // Optionally create compression maps and other debug output if (mOutput == OutputType.CompressionMap) { CopyDuplicateAttributesForDebug(blocks, duplicates); var map = GenerateCompressionMap(frame.Width, frame.Height, blocks); var f = GenerateFullFrame(map, ImageFormat.Png); map.Dispose(); return(new Frame[] { f }); } else if (mOutput == OutputType.HideJpg || mOutput == OutputType.HidePng) { CopyDuplicateAttributesForDebug(blocks, duplicates); var map = GenerateHideMap(frame, blocks, mOutput == OutputType.HideJpg ? Score.Jpg : Score.Png); var f = GenerateFullFrame(map, ImageFormat.Jpeg); map.Dispose(); return(new Frame[] { f }); } return(frames); }