I am attempting to modify an animated GIF. That is, I need to make modifications to certain sequences of frames. In my case I need to add some text depending on what a user has done.
Mathew Sachin provided sample animated GIF code for loading and saving an image in a similar question on 2012-11-13. This code is considerably more compact than other offerings and is reproduced here:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
/// <summary>
/// Uses default .net GIF encoding and adds animation headers.
/// </summary>
public class Gif : IDisposable, IEnumerable<Image>
{
#region Header Constants
const byte FileTrailer = 0x3b,
ApplicationBlockSize = 0x0b,
GraphicControlExtensionBlockSize = 0x04;
const int ApplicationExtensionBlockIdentifier = 0xff21,
GraphicControlExtensionBlockIdentifier = 0xf921;
const long SourceGlobalColorInfoPosition = 10,
SourceGraphicControlExtensionPosition = 781,
SourceGraphicControlExtensionLength = 8,
SourceImageBlockPosition = 789,
SourceImageBlockHeaderLength = 11,
SourceColorBlockPosition = 13,
SourceColorBlockLength = 768;
const string ApplicationIdentification = "NETSCAPE2.0",
FileType = "GIF",
FileVersion = "89a";
#endregion
class GifFrame
{
public GifFrame(Image image, double delay, int xOffset, int yOffset)
{
Image = image;
Delay = delay;
XOffset = xOffset;
YOffset = yOffset;
}
public Image Image;
public double Delay;
public int XOffset, YOffset;
}
List<GifFrame> Frames = new List<GifFrame>();
public Gif() { DefaultFrameDelay = 500; }
public Gif(Stream InStream, int Repeat = 0, int Delay = 500)
{
using (Image Animation = Bitmap.FromStream(InStream))
{
int Length = Animation.GetFrameCount(FrameDimension.Time);
DefaultFrameDelay = Delay;
this.Repeat = Repeat;
for (int i = 0; i < Length; ++i)
{
Animation.SelectActiveFrame(FrameDimension.Time, i);
var Frame = new Bitmap(Animation.Size.Width, Animation.Size.Height);
Graphics.FromImage(Frame).DrawImage(Animation, new Point(0, 0));
Frames.Add(new GifFrame(Frame, Delay, 0, 0));
}
}
}
#region Properties
public int DefaultWidth { get; set; }
public int DefaultHeight { get; set; }
public int Count { get { return Frames.Count; } }
/// <summary>
/// Default Delay in Milliseconds
/// </summary>
public int DefaultFrameDelay { get; set; }
public int Repeat { get; private set; }
#endregion
/// <summary>
/// Adds a frame to this animation.
/// </summary>
/// <param name="Image">The image to add</param>
/// <param name="XOffset">The positioning x offset this image should be displayed at.</param>
/// <param name="YOffset">The positioning y offset this image should be displayed at.</param>
public void AddFrame(Image Image, double? frameDelay = null, int XOffset = 0, int YOffset = 0)
{
Frames.Add(new GifFrame(Image, frameDelay ?? DefaultFrameDelay, XOffset, YOffset));
}
public void AddFrame(string FilePath, double? frameDelay = null, int XOffset = 0, int YOffset = 0)
{
AddFrame(new Bitmap(FilePath), frameDelay, XOffset, YOffset);
}
public void RemoveAt(int Index) { Frames.RemoveAt(Index); }
public void Clear() { Frames.Clear(); }
public void Save(Stream OutStream)
{
using (var Writer = new BinaryWriter(OutStream))
{
for (int i = 0; i < Count; ++i)
{
var Frame = Frames[i];
using (var gifStream = new MemoryStream())
{
Frame.Image.Save(gifStream, ImageFormat.Gif);
// Steal the global color table info
if (i == 0) InitHeader(gifStream, Writer, Frame.Image.Width, Frame.Image.Height);
WriteGraphicControlBlock(gifStream, Writer, Frame.Delay);
WriteImageBlock(gifStream, Writer, i != 0, Frame.XOffset, Frame.YOffset, Frame.Image.Width, Frame.Image.Height);
}
}
// Complete File
Writer.Write(FileTrailer);
}
}
#region Write
void InitHeader(Stream sourceGif, BinaryWriter Writer, int w, int h)
{
// File Header
Writer.Write(FileType.ToCharArray());
Writer.Write(FileVersion.ToCharArray());
Writer.Write((short)(DefaultWidth == 0 ? w : DefaultWidth)); // Initial Logical Width
Writer.Write((short)(DefaultHeight == 0 ? h : DefaultHeight)); // Initial Logical Height
sourceGif.Position = SourceGlobalColorInfoPosition;
Writer.Write((byte)sourceGif.ReadByte()); // Global Color Table Info
Writer.Write((byte)0); // Background Color Index
Writer.Write((byte)0); // Pixel aspect ratio
WriteColorTable(sourceGif, Writer);
// App Extension Header
unchecked { Writer.Write((short)ApplicationExtensionBlockIdentifier); };
Writer.Write((byte)ApplicationBlockSize);
Writer.Write(ApplicationIdentification.ToCharArray());
Writer.Write((byte)3); // Application block length
Writer.Write((byte)1);
Writer.Write((short)Repeat); // Repeat count for images.
Writer.Write((byte)0); // terminator
}
void WriteColorTable(Stream sourceGif, BinaryWriter Writer)
{
sourceGif.Position = SourceColorBlockPosition; // Locating the image color table
var colorTable = new byte[SourceColorBlockLength];
sourceGif.Read(colorTable, 0, colorTable.Length);
Writer.Write(colorTable, 0, colorTable.Length);
}
void WriteGraphicControlBlock(Stream sourceGif, BinaryWriter Writer, double frameDelay)
{
sourceGif.Position = SourceGraphicControlExtensionPosition; // Locating the source GCE
var blockhead = new byte[SourceGraphicControlExtensionLength];
sourceGif.Read(blockhead, 0, blockhead.Length); // Reading source GCE
unchecked { Writer.Write((short)GraphicControlExtensionBlockIdentifier); }; // Identifier
Writer.Write((byte)GraphicControlExtensionBlockSize); // Block Size
Writer.Write((byte)(blockhead[3] & 0xf7 | 0x08)); // Setting disposal flag
Writer.Write((short)(frameDelay / 10)); // Setting frame delay
Writer.Write((byte)blockhead[6]); // Transparent color index
Writer.Write((byte)0); // Terminator
}
void WriteImageBlock(Stream sourceGif, BinaryWriter Writer, bool includeColorTable, int x, int y, int w, int h)
{
sourceGif.Position = SourceImageBlockPosition; // Locating the image block
var header = new byte[SourceImageBlockHeaderLength];
sourceGif.Read(header, 0, header.Length);
Writer.Write((byte)header[0]); // Separator
Writer.Write((short)x); // Position X
Writer.Write((short)y); // Position Y
Writer.Write((short)w); // Width
Writer.Write((short)h); // Height
if (includeColorTable) // If first frame, use global color table - else use local
{
sourceGif.Position = SourceGlobalColorInfoPosition;
Writer.Write((byte)(sourceGif.ReadByte() & 0x3f | 0x80)); // Enabling local color table
WriteColorTable(sourceGif, Writer);
}
else Writer.Write((byte)(header[9] & 0x07 | 0x07)); // Disabling local color table
Writer.Write((byte)header[10]); // LZW Min Code Size
// Read/Write image data
sourceGif.Position = SourceImageBlockPosition + SourceImageBlockHeaderLength;
var dataLength = sourceGif.ReadByte();
while (dataLength > 0)
{
var imgData = new byte[dataLength];
sourceGif.Read(imgData, 0, dataLength);
Writer.Write((byte)dataLength);
Writer.Write(imgData, 0, dataLength);
dataLength = sourceGif.ReadByte();
}
Writer.Write((byte)0); // Terminator
}
#endregion
public void Dispose()
{
Frames.Clear();
Frames = null;
}
public Image this[int Index] { get { return Frames[Index].Image; } }
public IEnumerator<Image> GetEnumerator() { foreach (var Frame in Frames) yield return Frame.Image; }
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
}
The constructor takes a stream and stores each frame in an array of the class GifFrame.
So given a GIF file in a stream I can store in internally with:-
Gif myGif = new Gif(imageStream);
I can then take the Image from any frame and modify it as required.
I know this works correctly because I can take a random frame and save it away as a single image as here:-
Image frame50 = myGif[50];
frame50.Save("C:\\frame50.gif", ImageFormat.Gif);
My question is specifically how, having modified the animation, do I obtain the animation as a stream or file to display to the user.
In theory the Save method does this.
Stream outStream = new MemoryStream();
myGif.Save(outStream);
Image img = Image.FromStream(outStream);
In practice this crashes with 'cannot access a closed stream'. I think this is because the 'using' clause within the method that loads each frame in turn closes the stream.
Can anybody see a fix for this?
Many thanks
Tony Reynolds (UK)
Related
I am first time working on DirectX, I find difficulty in upgrading the code from DirectX9 to DirectX11. Unable to get the code similar in DirectX 11 since lot of changes from DirectX9 to 11.
Like below is my code for Direct3D9 of SlimDx
using SlimDX;
using SlimDX.Direct3D9;
private readonly Device m_Device;
private readonly Direct3D m_Direct3D;
private readonly PresentParameters m_presentParams;
//renderer
public Renderer() {
m_Direct3D = new Direct3D();
var createFlags = CreateFlags.SoftwareVertexProcessing | CreateFlags.FpuPreserve;
var caps = m_Direct3D.GetDeviceCaps(0, DeviceType.Hardware);
if ((caps.DeviceCaps & DeviceCaps.HWTransformAndLight) != 0) // HW processing supported
{
createFlags = CreateFlags.HardwareVertexProcessing | CreateFlags.FpuPreserve;
}
m_presentParams = new PresentParameters();
m_presentParams.BackBufferFormat = Format.A8R8G8B8;
m_presentParams.BackBufferCount = 1;
m_presentParams.Multisample = MultisampleType.None;
m_presentParams.SwapEffect = SwapEffect.Flip;
m_presentParams.EnableAutoDepthStencil = true;
m_presentParams.AutoDepthStencilFormat = Format.D16;
m_presentParams.PresentFlags = PresentFlags.DiscardDepthStencil;
m_presentParams.PresentationInterval = PresentInterval.One;
m_presentParams.Windowed = true;
m_presentParams.DeviceWindowHandle = handle;
m_Device = new Device(m_Direct3D,
0,
DeviceType.Hardware,
handle,
createFlags,
m_presentParams);
m_Device.Present();
System.Drawing.Rectangle oldBounds = m_Device.ScissorRect;
}
//begins the scene
public void BeginScene() {
m_Device.BeginScene();
}
//create texture
public Texture CreateTextureFromStream(Stream srcStream) {
srcStream.Seek(0, SeekOrigin.Begin);
return Texture.FromStream(m_Device, srcStream, 0, Pool.Managed);
}
//ends the scene
public void EndScene() {
m_Device.EndScene();
}
/// <summary>
/// Gets the size of the max texture.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
public void GetMaxTextureSize(out int width, out int height) {
Capabilities c = m_Direct3D.GetDeviceCaps(0, DeviceType.Hardware);
width = c.MaxTextureHeight;
height = c.MaxTextureWidth;
}
//reset
public bool IsResetRequired() {
return m_Device.TestCooperativeLevel() == ResultCode.DeviceNotReset;
}
//render background
public void RenderBackground() {
m_Device.VertexFormat = VertexFormat.Position | VertexFormat.Diffuse;
m_Device.DrawUserPrimitives(PrimitiveType.TriangleList, m_backgroundRectBuffer.Count / 3, m_backgroundRectBuffer.ToArray());
m_backgroundRectBuffer.Clear();
}
//begin
public void RenderBegin() {
m_Device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.FromArgb(0), 1.0f, 0);
}
Now, i want to move to DirectX 11. Can anyone help me out of this.
To be brief, I'm trying to implement some motion detection algorithm and in my case I'm working on UWP using portable version of AForge library to processing image. Avoiding this issue, I have problem with conversion SoftwareBitmap object (that I get from MediaFrameReader) to Bitmap object (and vice versa) that I use in my code associated with motion detection. In consequence of this conversion, I get proper image with big red X in the foreground. Code below:
private async void FrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
{
var frame = sender.TryAcquireLatestFrame();
if (frame != null && !_detectingMotion)
{
SoftwareBitmap aForgeInputBitmap = null;
var inputBitmap = frame.VideoMediaFrame?.SoftwareBitmap;
if (inputBitmap != null)
{
_detectingMotion = true;
//The XAML Image control can only display images in BRGA8 format with premultiplied or no alpha
if (inputBitmap.BitmapPixelFormat == BitmapPixelFormat.Bgra8
&& inputBitmap.BitmapAlphaMode == BitmapAlphaMode.Premultiplied)
{
aForgeInputBitmap = SoftwareBitmap.Copy(inputBitmap);
}
else
{
aForgeInputBitmap = SoftwareBitmap.Convert(inputBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore);
}
await _aForgeHelper.MoveBackgrounds(aForgeInputBitmap);
SoftwareBitmap aForgeOutputBitmap = await _aForgeHelper.DetectMotion();
_frameRenderer.PresentSoftwareBitmap(aForgeOutputBitmap);
_detectingMotion = false;
}
}
}
class AForgeHelper
{
private Bitmap _background;
private Bitmap _currentFrameBitmap;
public async Task MoveBackgrounds(SoftwareBitmap currentFrame)
{
if (_background == null)
{
_background = TransformToGrayscale(await ConvertSoftwareBitmapToBitmap(currentFrame));
}
else
{
// modifying _background in compliance with algorithm - in this case irrelevant
}
}
public async Task<SoftwareBitmap> DetectMotion()
{
// to check only this conversion
return await ConvertBitmapToSoftwareBitmap(_background);
}
private static async Task<Bitmap> ConvertSoftwareBitmapToBitmap(SoftwareBitmap input)
{
Bitmap output = null;
await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
WriteableBitmap tmpBitmap = new WriteableBitmap(input.PixelWidth, input.PixelHeight);
input.CopyToBuffer(tmpBitmap.PixelBuffer);
output = (Bitmap)tmpBitmap;
});
return output;
}
private static async Task<SoftwareBitmap> ConvertBitmapToSoftwareBitmap(Bitmap input)
{
SoftwareBitmap output = null;
await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
WriteableBitmap tmpBitmap = (WriteableBitmap)input;
output = new SoftwareBitmap(BitmapPixelFormat.Bgra8, tmpBitmap.PixelWidth, tmpBitmap.PixelHeight,
BitmapAlphaMode.Premultiplied);
output.CopyFromBuffer(tmpBitmap.PixelBuffer);
});
return output;
}
private static Bitmap TransformToGrayscale(Bitmap input)
{
Grayscale grayscaleFilter = new Grayscale(0.2125, 0.7154, 0.0721);
Bitmap output = grayscaleFilter.Apply(input);
return output;
}
Certainly, I've tried to detect some errors using try-catch clauses. I've found nothing. Thanks in advance.
EDIT (29/03/2018):
Generally, my app targets at providing some features associated with Kinect Sensor. App user has possibility to choose some feature from features list. First of all, this app have to be available on Xbox One, therefore I've chosen UWP. Due to 'scientific' issues, I've implemented MVVM pattern, using MVVM Light framework. As for PresentSoftwareBitmap() method, it comes from Windows-universal-samples repo and I paste FrameRenderer helper class below:
[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
void GetBuffer(out byte* buffer, out uint capacity);
}
class FrameRenderer
{
private Image _imageElement;
private SoftwareBitmap _backBuffer;
private bool _taskRunning = false;
public FrameRenderer(Image imageElement)
{
_imageElement = imageElement;
_imageElement.Source = new SoftwareBitmapSource();
}
// Processes a MediaFrameReference and displays it in a XAML image control
public void ProcessFrame(MediaFrameReference frame)
{
var softwareBitmap = FrameRenderer.ConvertToDisplayableImage(frame?.VideoMediaFrame);
if (softwareBitmap != null)
{
// Swap the processed frame to _backBuffer and trigger UI thread to render it
softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap);
// UI thread always reset _backBuffer before using it. Unused bitmap should be disposed.
softwareBitmap?.Dispose();
// Changes to xaml ImageElement must happen in UI thread through Dispatcher
var task = _imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
async () =>
{
// Don't let two copies of this task run at the same time.
if (_taskRunning)
{
return;
}
_taskRunning = true;
// Keep draining frames from the backbuffer until the backbuffer is empty.
SoftwareBitmap latestBitmap;
while ((latestBitmap = Interlocked.Exchange(ref _backBuffer, null)) != null)
{
var imageSource = (SoftwareBitmapSource)_imageElement.Source;
await imageSource.SetBitmapAsync(latestBitmap);
latestBitmap.Dispose();
}
_taskRunning = false;
});
}
}
// Function delegate that transforms a scanline from an input image to an output image.
private unsafe delegate void TransformScanline(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes);
/// <summary>
/// Determines the subtype to request from the MediaFrameReader that will result in
/// a frame that can be rendered by ConvertToDisplayableImage.
/// </summary>
/// <returns>Subtype string to request, or null if subtype is not renderable.</returns>
public static string GetSubtypeForFrameReader(MediaFrameSourceKind kind, MediaFrameFormat format)
{
// Note that media encoding subtypes may differ in case.
// https://learn.microsoft.com/en-us/uwp/api/Windows.Media.MediaProperties.MediaEncodingSubtypes
string subtype = format.Subtype;
switch (kind)
{
// For color sources, we accept anything and request that it be converted to Bgra8.
case MediaFrameSourceKind.Color:
return Windows.Media.MediaProperties.MediaEncodingSubtypes.Bgra8;
// The only depth format we can render is D16.
case MediaFrameSourceKind.Depth:
return String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.D16, StringComparison.OrdinalIgnoreCase) ? subtype : null;
// The only infrared formats we can render are L8 and L16.
case MediaFrameSourceKind.Infrared:
return (String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.L8, StringComparison.OrdinalIgnoreCase) ||
String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.L16, StringComparison.OrdinalIgnoreCase)) ? subtype : null;
// No other source kinds are supported by this class.
default:
return null;
}
}
/// <summary>
/// Converts a frame to a SoftwareBitmap of a valid format to display in an Image control.
/// </summary>
/// <param name="inputFrame">Frame to convert.</param>
public static unsafe SoftwareBitmap ConvertToDisplayableImage(VideoMediaFrame inputFrame)
{
SoftwareBitmap result = null;
using (var inputBitmap = inputFrame?.SoftwareBitmap)
{
if (inputBitmap != null)
{
switch (inputFrame.FrameReference.SourceKind)
{
case MediaFrameSourceKind.Color:
// XAML requires Bgra8 with premultiplied alpha.
// We requested Bgra8 from the MediaFrameReader, so all that's
// left is fixing the alpha channel if necessary.
if (inputBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8)
{
System.Diagnostics.Debug.WriteLine("Color frame in unexpected format.");
}
else if (inputBitmap.BitmapAlphaMode == BitmapAlphaMode.Premultiplied)
{
// Already in the correct format.
result = SoftwareBitmap.Copy(inputBitmap);
}
else
{
// Convert to premultiplied alpha.
result = SoftwareBitmap.Convert(inputBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}
break;
case MediaFrameSourceKind.Depth:
// We requested D16 from the MediaFrameReader, so the frame should
// be in Gray16 format.
if (inputBitmap.BitmapPixelFormat == BitmapPixelFormat.Gray16)
{
// Use a special pseudo color to render 16 bits depth frame.
var depthScale = (float)inputFrame.DepthMediaFrame.DepthFormat.DepthScaleInMeters;
var minReliableDepth = inputFrame.DepthMediaFrame.MinReliableDepth;
var maxReliableDepth = inputFrame.DepthMediaFrame.MaxReliableDepth;
result = TransformBitmap(inputBitmap, (w, i, o) => PseudoColorHelper.PseudoColorForDepth(w, i, o, depthScale, minReliableDepth, maxReliableDepth));
}
else
{
System.Diagnostics.Debug.WriteLine("Depth frame in unexpected format.");
}
break;
case MediaFrameSourceKind.Infrared:
// We requested L8 or L16 from the MediaFrameReader, so the frame should
// be in Gray8 or Gray16 format.
switch (inputBitmap.BitmapPixelFormat)
{
case BitmapPixelFormat.Gray16:
// Use pseudo color to render 16 bits frames.
result = TransformBitmap(inputBitmap, PseudoColorHelper.PseudoColorFor16BitInfrared);
break;
case BitmapPixelFormat.Gray8:
// Use pseudo color to render 8 bits frames.
result = TransformBitmap(inputBitmap, PseudoColorHelper.PseudoColorFor8BitInfrared);
break;
default:
System.Diagnostics.Debug.WriteLine("Infrared frame in unexpected format.");
break;
}
break;
}
}
}
return result;
}
/// <summary>
/// Transform image into Bgra8 image using given transform method.
/// </summary>
/// <param name="softwareBitmap">Input image to transform.</param>
/// <param name="transformScanline">Method to map pixels in a scanline.</param>
private static unsafe SoftwareBitmap TransformBitmap(SoftwareBitmap softwareBitmap, TransformScanline transformScanline)
{
// XAML Image control only supports premultiplied Bgra8 format.
var outputBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8,
softwareBitmap.PixelWidth, softwareBitmap.PixelHeight, BitmapAlphaMode.Premultiplied);
using (var input = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
using (var output = outputBitmap.LockBuffer(BitmapBufferAccessMode.Write))
{
// Get stride values to calculate buffer position for a given pixel x and y position.
int inputStride = input.GetPlaneDescription(0).Stride;
int outputStride = output.GetPlaneDescription(0).Stride;
int pixelWidth = softwareBitmap.PixelWidth;
int pixelHeight = softwareBitmap.PixelHeight;
using (var outputReference = output.CreateReference())
using (var inputReference = input.CreateReference())
{
// Get input and output byte access buffers.
byte* inputBytes;
uint inputCapacity;
((IMemoryBufferByteAccess)inputReference).GetBuffer(out inputBytes, out inputCapacity);
byte* outputBytes;
uint outputCapacity;
((IMemoryBufferByteAccess)outputReference).GetBuffer(out outputBytes, out outputCapacity);
// Iterate over all pixels and store converted value.
for (int y = 0; y < pixelHeight; y++)
{
byte* inputRowBytes = inputBytes + y * inputStride;
byte* outputRowBytes = outputBytes + y * outputStride;
transformScanline(pixelWidth, inputRowBytes, outputRowBytes);
}
}
}
return outputBitmap;
}
/// <summary>
/// A helper class to manage look-up-table for pseudo-colors.
/// </summary>
private static class PseudoColorHelper
{
#region Constructor, private members and methods
private const int TableSize = 1024; // Look up table size
private static readonly uint[] PseudoColorTable;
private static readonly uint[] InfraredRampTable;
// Color palette mapping value from 0 to 1 to blue to red colors.
private static readonly Color[] ColorRamp =
{
Color.FromArgb(a:0xFF, r:0x7F, g:0x00, b:0x00),
Color.FromArgb(a:0xFF, r:0xFF, g:0x00, b:0x00),
Color.FromArgb(a:0xFF, r:0xFF, g:0x7F, b:0x00),
Color.FromArgb(a:0xFF, r:0xFF, g:0xFF, b:0x00),
Color.FromArgb(a:0xFF, r:0x7F, g:0xFF, b:0x7F),
Color.FromArgb(a:0xFF, r:0x00, g:0xFF, b:0xFF),
Color.FromArgb(a:0xFF, r:0x00, g:0x7F, b:0xFF),
Color.FromArgb(a:0xFF, r:0x00, g:0x00, b:0xFF),
Color.FromArgb(a:0xFF, r:0x00, g:0x00, b:0x7F),
};
static PseudoColorHelper()
{
PseudoColorTable = InitializePseudoColorLut();
InfraredRampTable = InitializeInfraredRampLut();
}
/// <summary>
/// Maps an input infrared value between [0, 1] to corrected value between [0, 1].
/// </summary>
/// <param name="value">Input value between [0, 1].</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] // Tell the compiler to inline this method to improve performance
private static uint InfraredColor(float value)
{
int index = (int)(value * TableSize);
index = index < 0 ? 0 : index > TableSize - 1 ? TableSize - 1 : index;
return InfraredRampTable[index];
}
/// <summary>
/// Initializes the pseudo-color look up table for infrared pixels
/// </summary>
private static uint[] InitializeInfraredRampLut()
{
uint[] lut = new uint[TableSize];
for (int i = 0; i < TableSize; i++)
{
var value = (float)i / TableSize;
// Adjust to increase color change between lower values in infrared images
var alpha = (float)Math.Pow(1 - value, 12);
lut[i] = ColorRampInterpolation(alpha);
}
return lut;
}
/// <summary>
/// Initializes pseudo-color look up table for depth pixels
/// </summary>
private static uint[] InitializePseudoColorLut()
{
uint[] lut = new uint[TableSize];
for (int i = 0; i < TableSize; i++)
{
lut[i] = ColorRampInterpolation((float)i / TableSize);
}
return lut;
}
/// <summary>
/// Maps a float value to a pseudo-color pixel
/// </summary>
private static uint ColorRampInterpolation(float value)
{
// Map value to surrounding indexes on the color ramp
int rampSteps = ColorRamp.Length - 1;
float scaled = value * rampSteps;
int integer = (int)scaled;
int index =
integer < 0 ? 0 :
integer >= rampSteps - 1 ? rampSteps - 1 :
integer;
Color prev = ColorRamp[index];
Color next = ColorRamp[index + 1];
// Set color based on ratio of closeness between the surrounding colors
uint alpha = (uint)((scaled - integer) * 255);
uint beta = 255 - alpha;
return
((prev.A * beta + next.A * alpha) / 255) << 24 | // Alpha
((prev.R * beta + next.R * alpha) / 255) << 16 | // Red
((prev.G * beta + next.G * alpha) / 255) << 8 | // Green
((prev.B * beta + next.B * alpha) / 255); // Blue
}
/// <summary>
/// Maps a value in [0, 1] to a pseudo RGBA color.
/// </summary>
/// <param name="value">Input value between [0, 1].</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint PseudoColor(float value)
{
int index = (int)(value * TableSize);
index = index < 0 ? 0 : index > TableSize - 1 ? TableSize - 1 : index;
return PseudoColorTable[index];
}
#endregion
/// <summary>
/// Maps each pixel in a scanline from a 16 bit depth value to a pseudo-color pixel.
/// </summary>
/// <param name="pixelWidth">Width of the input scanline, in pixels.</param>
/// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
/// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>
/// <param name="depthScale">Physical distance that corresponds to one unit in the input scanline.</param>
/// <param name="minReliableDepth">Shortest distance at which the sensor can provide reliable measurements.</param>
/// <param name="maxReliableDepth">Furthest distance at which the sensor can provide reliable measurements.</param>
public static unsafe void PseudoColorForDepth(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes, float depthScale, float minReliableDepth, float maxReliableDepth)
{
// Visualize space in front of your desktop.
float minInMeters = minReliableDepth * depthScale;
float maxInMeters = maxReliableDepth * depthScale;
float one_min = 1.0f / minInMeters;
float range = 1.0f / maxInMeters - one_min;
ushort* inputRow = (ushort*)inputRowBytes;
uint* outputRow = (uint*)outputRowBytes;
for (int x = 0; x < pixelWidth; x++)
{
var depth = inputRow[x] * depthScale;
if (depth == 0)
{
// Map invalid depth values to transparent pixels.
// This happens when depth information cannot be calculated, e.g. when objects are too close.
outputRow[x] = 0;
}
else
{
var alpha = (1.0f / depth - one_min) / range;
outputRow[x] = PseudoColor(alpha * alpha);
}
}
}
/// <summary>
/// Maps each pixel in a scanline from a 8 bit infrared value to a pseudo-color pixel.
/// </summary>
/// /// <param name="pixelWidth">Width of the input scanline, in pixels.</param>
/// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
/// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>
public static unsafe void PseudoColorFor8BitInfrared(
int pixelWidth, byte* inputRowBytes, byte* outputRowBytes)
{
byte* inputRow = inputRowBytes;
uint* outputRow = (uint*)outputRowBytes;
for (int x = 0; x < pixelWidth; x++)
{
outputRow[x] = InfraredColor(inputRow[x] / (float)Byte.MaxValue);
}
}
/// <summary>
/// Maps each pixel in a scanline from a 16 bit infrared value to a pseudo-color pixel.
/// </summary>
/// <param name="pixelWidth">Width of the input scanline.</param>
/// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
/// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>
public static unsafe void PseudoColorFor16BitInfrared(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes)
{
ushort* inputRow = (ushort*)inputRowBytes;
uint* outputRow = (uint*)outputRowBytes;
for (int x = 0; x < pixelWidth; x++)
{
outputRow[x] = InfraredColor(inputRow[x] / (float)UInt16.MaxValue);
}
}
}
// Displays the provided softwareBitmap in a XAML image control.
public void PresentSoftwareBitmap(SoftwareBitmap softwareBitmap)
{
if (softwareBitmap != null)
{
// Swap the processed frame to _backBuffer and trigger UI thread to render it
softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap);
// UI thread always reset _backBuffer before using it. Unused bitmap should be disposed.
softwareBitmap?.Dispose();
// Changes to xaml ImageElement must happen in UI thread through Dispatcher
var task = _imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
async () =>
{
// Don't let two copies of this task run at the same time.
if (_taskRunning)
{
return;
}
_taskRunning = true;
// Keep draining frames from the backbuffer until the backbuffer is empty.
SoftwareBitmap latestBitmap;
while ((latestBitmap = Interlocked.Exchange(ref _backBuffer, null)) != null)
{
var imageSource = (SoftwareBitmapSource)_imageElement.Source;
await imageSource.SetBitmapAsync(latestBitmap);
latestBitmap.Dispose();
}
_taskRunning = false;
});
}
}
}
This is output image, that I've got after conversion issues and grayscale process:
Output image
As for VS version - Visual Studio Enterprise 2017, Version 15.6.1 (to be precise). Once more, thanks in advance for help.
I'm saving strokes to a database and I can retrieve them. Now I want to save the color, width and transparency of the strokes as well.
This is the what I have in my code
private void AddFloorPlan()
{
MyCustomStrokes customStrokes = new MyCustomStrokes();
customStrokes.StrokeCollection = new Point[FloorPlanStrokes.Count][];
for (int i = 0; i < FloorPlanStrokes.Count; i++)
{
customStrokes.StrokeCollection[i] =
new Point[FloorPlanStrokes[i].StylusPoints.Count];
for (int j = 0; j < FloorPlanStrokes[i].StylusPoints.Count; j++)
{
customStrokes.StrokeCollection[i][j] = new Point();
customStrokes.StrokeCollection[i][j].X = FloorPlanStrokes[i].StylusPoints[j].X;
customStrokes.StrokeCollection[i][j].Y = FloorPlanStrokes[i].StylusPoints[j].Y;
}
}
MemoryStream ms = new MemoryStream();
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(ms, customStrokes);
byte[] bytes = ms.ToArray();
ms.Dispose();
}
[Serializable]
public sealed class MyCustomStrokes
{
public MyCustomStrokes() { }
/// <SUMMARY>
/// The first index is for the stroke no.
/// The second index is for the keep the 2D point of the Stroke.
/// </SUMMARY>
public Point[][] StrokeCollection;
}
Store the color information as string (Color.ToString() do the job) and convert it to Color when deserialized as follows:
(Color)ColorConverter.ConvertFromString(STORED_STRING);
You would make new Brush and Freeze it if their color is not changed later.
Width (StrokeThickness?) is another thing. Just save it as double separately.
Example:
[Serializable]
public sealed class MyCustomStrokes
{
public string[][] ColorCodeCollection;
[NonSerialized()]
private Dictionary<string, SolidColorBrush> _memo;
public Brush GetBrush(int i, int j)
{
var code = ColorCodeCollection[i][j];
if(_memo.ContainsKey(code))
return _memo[code];
var col = (Color)ColorConverter.ConvertFromString(code);
var brush = new SolidColorBrush(col);
brush.Freeze();
_memo[code] = brush;
return brush;
}
}
Note that this is just a quick code. Examine it meets your plan.
My program has a problem with poor coordination between the depth and color images.
The player mask is not in the same place as the person (see the picture below).
void _AllFreamReady(object sender, AllFramesReadyEventArgs e)
{
using (ColorImageFrame _colorFrame = e.OpenColorImageFrame())
{
if (_colorFrame == null) //jezeli pusta ramka nie rob nic
{
return;
}
byte[] _pixels = new byte[_colorFrame.PixelDataLength]; //utworzenie tablicy pixeli dla 1 ramki obrazu o rozmiarach przechwyconej ramki z strumienia
_colorFrame.CopyPixelDataTo(_pixels); //kopiujemy pixele do tablicy
int _stride = _colorFrame.Width * 4; //Kazdy pixel moze miec 4 wartosci Red Green Blue lub pusty
image1.Source =
BitmapSource.Create(_colorFrame.Width, _colorFrame.Height,
96, 96, PixelFormats.Bgr32, null, _pixels, _stride);
if (_closing)
{
return;
}
using (DepthImageFrame _depthFrame = e.OpenDepthImageFrame())
{
if (_depthFrame == null)
{
return;
}
byte[] _pixelsdepth = _GenerateColoredBytes(_depthFrame,_pixels);
int _dstride = _depthFrame.Width * 4;
image3.Source =
BitmapSource.Create(_depthFrame.Width, _depthFrame.Height,
96, 96, PixelFormats.Bgr32, null, _pixelsdepth, _dstride);
}
}
}
private byte[] _GenerateColoredBytes(DepthImageFrame _depthFrame, byte[] _pixels)
{
short[] _rawDepthData = new short[_depthFrame.PixelDataLength];
_depthFrame.CopyPixelDataTo(_rawDepthData);
Byte[] _dpixels = new byte[_depthFrame.Height * _depthFrame.Width * 4];
const int _blueindex = 0;
const int _greenindex = 1;
const int _redindex = 2;
for (int _depthindex = 0, _colorindex = 0;
_depthindex < _rawDepthData.Length && _colorindex < _dpixels.Length;
_depthindex++, _colorindex += 4)
{
int _player = _rawDepthData[_depthindex] & DepthImageFrame.PlayerIndexBitmaskWidth;
if (_player > 0)
{
_dpixels[_colorindex + _redindex] = _pixels[_colorindex + _redindex];
_dpixels[_colorindex + _greenindex] = _pixels[_colorindex + _greenindex];
_dpixels[_colorindex + _blueindex] = _pixels[_colorindex + _blueindex];
};
}
return _dpixels;
}
RGB and depth data are not aligned. This is due to the position of depth sensor and RGB camera in the Kinect case: they are different, so you cannot expect aligned images using different points of view.
However, you problem is quite common, and was solved by the KinectSensor.MapDepthFrameToColorFrame, that was deprecated after SDK 1.6. Now, what you need is the CoordinateMapper.MapDepthFrameToColorFrame method.
The Coordinate Mapping Basics-WPF C# Sample shows how to use this method. You can find some significant parts of the code in the following:
// Intermediate storage for the depth data received from the sensor
private DepthImagePixel[] depthPixels;
// Intermediate storage for the color data received from the camera
private byte[] colorPixels;
// Intermediate storage for the depth to color mapping
private ColorImagePoint[] colorCoordinates;
// Inverse scaling factor between color and depth
private int colorToDepthDivisor;
// Format we will use for the depth stream
private const DepthImageFormat DepthFormat = DepthImageFormat.Resolution320x240Fps30;
// Format we will use for the color stream
private const ColorImageFormat ColorFormat = ColorImageFormat.RgbResolution640x480Fps30;
//...
// Initialization
this.colorCoordinates = new ColorImagePoint[this.sensor.DepthStream.FramePixelDataLength];
this.depthWidth = this.sensor.DepthStream.FrameWidth;
this.depthHeight = this.sensor.DepthStream.FrameHeight;
int colorWidth = this.sensor.ColorStream.FrameWidth;
int colorHeight = this.sensor.ColorStream.FrameHeight;
this.colorToDepthDivisor = colorWidth / this.depthWidth;
this.sensor.AllFramesReady += this.SensorAllFramesReady;
//...
private void SensorAllFramesReady(object sender, AllFramesReadyEventArgs e)
{
// in the middle of shutting down, so nothing to do
if (null == this.sensor)
{
return;
}
bool depthReceived = false;
bool colorReceived = false;
using (DepthImageFrame depthFrame = e.OpenDepthImageFrame())
{
if (null != depthFrame)
{
// Copy the pixel data from the image to a temporary array
depthFrame.CopyDepthImagePixelDataTo(this.depthPixels);
depthReceived = true;
}
}
using (ColorImageFrame colorFrame = e.OpenColorImageFrame())
{
if (null != colorFrame)
{
// Copy the pixel data from the image to a temporary array
colorFrame.CopyPixelDataTo(this.colorPixels);
colorReceived = true;
}
}
if (true == depthReceived)
{
this.sensor.CoordinateMapper.MapDepthFrameToColorFrame(
DepthFormat,
this.depthPixels,
ColorFormat,
this.colorCoordinates);
// ...
int depthIndex = x + (y * this.depthWidth);
DepthImagePixel depthPixel = this.depthPixels[depthIndex];
// scale color coordinates to depth resolution
int X = colorImagePoint.X / this.colorToDepthDivisor;
int Y = colorImagePoint.Y / this.colorToDepthDivisor;
// depthPixel is the depth for the (X,Y) pixel in the color frame
}
}
I am working on this problem myself. I agree with VitoShadow that one solution is in the coordinate mapping, but a section not posted where the ratio between the miss matched depth and color screen resolutions(this.colorToDepthDivisor = colorWidth / this.depthWidth;). This is used with a shift of the data (this.playerPixelData[playerPixelIndex - 1] = opaquePixelValue;) to account for the miss match.
Unfortunately, this can create a border around the masked image where the depthframe isn't stretched to the edge of the color frame. I am trying to not use skeleton mapping and am optimizing my code by tracking depthdata with emgu cv to pass a point as the center of the ROI of the colorframe. I am still working on it.
So I've been sharing some thoughts on the above topic title on my website about fast, unsafe pixel access. A gentlemen gave me a rough example of how he'd do it in C++, but that doesn't help me in C# unless I can interop it, and the interop is fast as well. I had found a class in the internet that was written using MSDN help, to unsafely access pixels. The class is exceptionally fast, but it's not fast enough. Here's the class:
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
namespace DCOMProductions.Desktop.ScreenViewer {
public unsafe class UnsafeBitmap {
Bitmap bitmap;
// three elements used for MakeGreyUnsafe
int width;
BitmapData bitmapData = null;
Byte* pBase = null;
public UnsafeBitmap(Bitmap bitmap) {
this.bitmap = new Bitmap(bitmap);
}
public UnsafeBitmap(int width, int height) {
this.bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
}
public void Dispose() {
bitmap.Dispose();
}
public Bitmap Bitmap {
get {
return (bitmap);
}
}
private Point PixelSize {
get {
GraphicsUnit unit = GraphicsUnit.Pixel;
RectangleF bounds = bitmap.GetBounds(ref unit);
return new Point((int)bounds.Width, (int)bounds.Height);
}
}
public void LockBitmap() {
GraphicsUnit unit = GraphicsUnit.Pixel;
RectangleF boundsF = bitmap.GetBounds(ref unit);
Rectangle bounds = new Rectangle((int)boundsF.X,
(int)boundsF.Y,
(int)boundsF.Width,
(int)boundsF.Height);
// Figure out the number of bytes in a row
// This is rounded up to be a multiple of 4
// bytes, since a scan line in an image must always be a multiple of 4 bytes
// in length.
width = (int)boundsF.Width * sizeof(Pixel);
if (width % 4 != 0) {
width = 4 * (width / 4 + 1);
}
bitmapData =
bitmap.LockBits(bounds, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
pBase = (Byte*)bitmapData.Scan0.ToPointer();
}
public Pixel GetPixel(int x, int y) {
Pixel returnValue = *PixelAt(x, y);
return returnValue;
}
public void SetPixel(int x, int y, Pixel colour) {
Pixel* pixel = PixelAt(x, y);
*pixel = colour;
}
public void UnlockBitmap() {
bitmap.UnlockBits(bitmapData);
bitmapData = null;
pBase = null;
}
public Pixel* PixelAt(int x, int y) {
return (Pixel*)(pBase + y * width + x * sizeof(Pixel));
}
}
}
Basically what I am doing is copying the entire screen and comparing each pixel to and old copy. On a 1680x1050 bitmap, this takes approximately 300 milliseconds using the following code.
private Bitmap GetInvalidFrame(Bitmap frame) {
Stopwatch sp = new Stopwatch();
sp.Start();
if (m_FrameBackBuffer == null) {
return frame;
}
Int32 pixelsToRead = frame.Width * frame.Height;
Int32 x = 0, y = 0;
UnsafeBitmap unsafeBitmap = new UnsafeBitmap(frame);
UnsafeBitmap unsafeBuffBitmap = new UnsafeBitmap(m_FrameBackBuffer);
UnsafeBitmap retVal = new UnsafeBitmap(frame.Width, frame.Height);
unsafeBitmap.LockBitmap();
unsafeBuffBitmap.LockBitmap();
retVal.LockBitmap();
do {
for (x = 0; x < frame.Width; x++) {
Pixel newPixel = unsafeBitmap.GetPixel(x, y);
Pixel oldPixel = unsafeBuffBitmap.GetPixel(x, y);
if (newPixel.Alpha != oldPixel.Alpha || newPixel.Red != oldPixel.Red || newPixel.Green != oldPixel.Green || newPixel.Blue != oldPixel.Blue) {
retVal.SetPixel(x, y, newPixel);
}
else {
// Skip pixel
}
}
y++;
} while (y != frame.Height);
unsafeBitmap.UnlockBitmap();
unsafeBuffBitmap.UnlockBitmap();
retVal.UnlockBitmap();
sp.Stop();
System.Diagnostics.Debug.WriteLine(sp.Elapsed.Milliseconds.ToString());
sp.Reset();
return retVal.Bitmap;
}
Is there any possible method/means/approach that I could speed this up to about 30ms? I can copy the screen in about 30ms using Graphics.CopyFromScreen(), so that produces approximately 30 frames each second. However, a program only runs as fast as its slower counterpart, so the 300ms delay in GetInvalidFrame, slows this down to about 1 - 3 frames each second. This isn't good for a meeting software.
Any advice, approaches, pointers in the right direction would be absolutely wonderful! Also, the code that is used to draw the bitmap on the client-side is below as well.
To comment on Dmitriy's answer/comment:
#region RootWorkItem
private ScreenClient m_RootWorkItem;
/// <summary>
/// Gets the RootWorkItem
/// </summary>
public ScreenClient RootWorkItem {
get {
if (m_RootWorkItem == null) {
m_RootWorkItem = new ScreenClient();
m_RootWorkItem.FrameRead += new EventHandler<FrameEventArgs>(RootWorkItem_FrameRead);
}
return m_RootWorkItem;
}
}
#endregion
private void RootWorkItem_FrameRead(Object sender, FrameEventArgs e) {
if (e.Frame != null) {
if (uxSurface.Image != null) {
Bitmap frame = (Bitmap)uxSurface.Image;
Graphics g = Graphics.FromImage(frame);
g.DrawImage(e.Frame, 0, 0); // Draw only updated pixels
uxSurface.Image = frame;
}
else {
uxSurface.Image = e.Frame; // Draw initial, full image
}
}
else {
uxSurface.Image = null;
}
}
Unsafe approach with usage of integers instead of Pixels and single loop:
private static Bitmap GetInvalidFrame(Bitmap oldFrame, Bitmap newFrame)
{
if (oldFrame.Size != newFrame.Size)
{
throw new ArgumentException();
}
Bitmap result = new Bitmap(oldFrame.Width, oldFrame.Height, oldFrame.PixelFormat);
Rectangle lockArea = new Rectangle(Point.Empty, oldFrame.Size);
PixelFormat format = PixelFormat.Format32bppArgb;
BitmapData oldData = oldFrame.LockBits(lockArea, ImageLockMode.ReadOnly, format);
BitmapData newData = newFrame.LockBits(lockArea, ImageLockMode.ReadOnly, format);
BitmapData resultData = result.LockBits(lockArea, ImageLockMode.WriteOnly, format);
int len = resultData.Height * Math.Abs(resultData.Stride) / 4;
unsafe
{
int* pOld = (int*)oldData.Scan0;
int* pNew = (int*)newData.Scan0;
int* pResult = (int*)resultData.Scan0;
for (int i = 0; i < len; i++)
{
int oldValue = *pOld++;
int newValue = *pNew++;
*pResult++ = oldValue != newValue ? newValue : 0 /* replace with 0xff << 24 if you need non-transparent black pixel */;
// *pResult++ = *pOld++ ^ *pNew++; // if you can use XORs.
}
}
oldFrame.UnlockBits(oldData);
newFrame.UnlockBits(newData);
result.UnlockBits(resultData);
return result;
}
I think you really can use XORed frames here and I hope that this can have better performance on both sides.
private static void XorFrames(Bitmap leftFrame, Bitmap rightFrame)
{
if (leftFrame.Size != rightFrame.Size)
{
throw new ArgumentException();
}
Rectangle lockArea = new Rectangle(Point.Empty, leftFrame.Size);
PixelFormat format = PixelFormat.Format32bppArgb;
BitmapData leftData = leftFrame.LockBits(lockArea, ImageLockMode.ReadWrite, format);
BitmapData rightData = rightFrame.LockBits(lockArea, ImageLockMode.ReadOnly, format);
int len = leftData.Height * Math.Abs(rightData.Stride) / 4;
unsafe
{
int* pLeft = (int*)leftData.Scan0;
int* pRight = (int*)rightData.Scan0;
for (int i = 0; i < len; i++)
{
*pLeft++ ^= *pRight++;
}
}
leftFrame.UnlockBits(leftData);
rightFrame.UnlockBits(rightData);
}
You can use this procedure on both sides in following way:
On server side you need to evaluate difference between old and new frame, send it to client and replace old frame by new. The server code should look something like this:
XorFrames(oldFrame, newFrame); // oldFrame ^= newFrame
Send(oldFrame); // send XOR of two frames
oldFrame = newFrame;
On client side you need to update your current frame with xor frame recieved from server:
XorFrames((Bitmap)uxSurface.Image, e.Frame);
Here: Utilizing the GPU with c# there are mentioned some librarys for using the GPU from C#.
Yes, you can do so by using unsafe code.
BitmapData d = l.LockBits(new Rectangle(0, 0, l.Width, l.Height), ImageLockMode.ReadOnly,l.PixelFormat);
IntPtr scan = d.Scan0;
unsafe
{
byte* p = (byte*)(void*)scan;
//dostuff
}
Check out http://www.codeproject.com/KB/GDI-plus/csharpgraphicfilters11.aspx for some basic examples of this kind of stuff. My code is based on that.
Note:
One of the reasons this will be much faster than yours is that you are separately comparing each channel instead of just comparing the entire byte using one operation. Similarly, changing PixelAt to give you a byte to facilitate this would probably give you an improvement.
Instead of checking each and every pixel, you can just perform a basic memory compare of the 2 bitmaps. In C, something like memcmp().
This would give you a much quicker test to let you know that the images are the same or not. Only when you know they are different do you need to resort to the more expensive code that will help you determine where they are different (if you even need to know that).
I am not a C# person though, so I don't know how easy it is to get access to the raw memory.
Was able to slice off about 60ms. I think this is going to require the GPU. I'm not seeing any solution to this utilizing the CPU, even by comparing more than one byte/pixel at a time, unless someone can whip up a code sample to show me otherwise. Still sits at about 200-260ms, far too slow for 30fps.
private static BitmapData m_OldData;
private static BitmapData m_NewData;
private static unsafe Byte* m_OldPBase;
private static unsafe Byte* m_NewPBase;
private static unsafe Pixel* m_OldPixel;
private static unsafe Pixel* m_NewPixel;
private static Int32 m_X;
private static Int32 m_Y;
private static Stopwatch m_Watch = new Stopwatch();
private static GraphicsUnit m_GraphicsUnit = GraphicsUnit.Pixel;
private static RectangleF m_OldBoundsF;
private static RectangleF m_NewBoundsF;
private static Rectangle m_OldBounds;
private static Rectangle m_NewBounds;
private static Pixel m_TransparentPixel = new Pixel() { Alpha = 0x00, Red = 0, Green = 0, Blue = 0 };
private Bitmap GetInvalidFrame(Bitmap frame) {
if (m_FrameBackBuffer == null) {
return frame;
}
m_Watch.Start();
unsafe {
m_OldBoundsF = m_FrameBackBuffer.GetBounds(ref m_GraphicsUnit);
m_OldBounds = new Rectangle((Int32)m_OldBoundsF.X, (Int32)m_OldBoundsF.Y, (Int32)m_OldBoundsF.Width, (Int32)m_OldBoundsF.Height);
m_OldData = m_FrameBackBuffer.LockBits(m_OldBounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
m_NewBoundsF = m_FrameBackBuffer.GetBounds(ref m_GraphicsUnit);
m_NewBounds = new Rectangle((Int32)m_NewBoundsF.X, (Int32)m_NewBoundsF.Y, (Int32)m_NewBoundsF.Width, (Int32)m_NewBoundsF.Height);
m_NewData = frame.LockBits(m_NewBounds, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
m_OldPBase = (Byte*)m_OldData.Scan0.ToPointer();
m_NewPBase = (Byte*)m_NewData.Scan0.ToPointer();
do {
for (m_X = 0; m_X < frame.Width; m_X++) {
m_OldPixel = (Pixel*)(m_OldPBase + m_Y * m_OldData.Stride + 1 + m_X * sizeof(Pixel));
m_NewPixel = (Pixel*)(m_NewPBase + m_Y * m_NewData.Stride + 1 + m_X * sizeof(Pixel));
if (m_OldPixel->Alpha == m_NewPixel->Alpha // AccessViolationException accessing Property in get {}
|| m_OldPixel->Red == m_NewPixel->Red
|| m_OldPixel->Green == m_NewPixel->Green
|| m_OldPixel->Blue == m_NewPixel->Blue) {
// Set the transparent pixel
*m_NewPixel = m_TransparentPixel;
}
}
m_Y++; //Debug.WriteLine(String.Format("X: {0}, Y: {1}", m_X, m_Y));
} while (m_Y < frame.Height);
}
m_Y = 0;
m_Watch.Stop();
Debug.WriteLine("Time elapsed: " + m_Watch.ElapsedMilliseconds.ToString());
m_Watch.Reset();
return frame;
}