-
Notifications
You must be signed in to change notification settings - Fork 62
Expand file tree
/
Copy pathUsbCamera.cs
More file actions
2469 lines (2128 loc) · 116 KB
/
Copy pathUsbCamera.cs
File metadata and controls
2469 lines (2128 loc) · 116 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* UsbCamera.cs
* Copyright (c) 2019 secile
* This software is released under the MIT license.
* see https://opensource.org/licenses/MIT
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
#if USBCAMERA_WPF
using System.Windows; // Size
using System.Windows.Media; // PixelFormats
using System.Windows.Media.Imaging; // BitmapSource
#else
using System.Drawing;
#endif
#if USBCAMERA_BYTEARRAY
using Bitmap = System.Collections.Generic.IEnumerable<byte>;
#elif USBCAMERA_WPF
using Bitmap = System.Windows.Media.Imaging.BitmapSource;
#else
//using Bitmap = System.Drawing.Bitmap;
#endif
namespace GitHub.secile.Video
{
// [How to use]
// string[] devices = UsbCamera.FindDevices();
// if (devices.Length == 0) return; // no camera.
//
// check format.
// int cameraIndex = 0;
// UsbCamera.VideoFormat[] formats = UsbCamera.GetVideoFormat(cameraIndex);
// for(int i=0; i<formats.Length; i++) Console.WriteLine("{0}:{1}", i, formats[i]);
//
// create usb camera and start.
// var camera = new UsbCamera(cameraIndex, formats[0]);
// camera.Start();
//
// just after camera start, GetBitmap() will fail because image buffer is not ready.
// while (!camera.IsReady) System.Threading.Thread.Sleep(10);
//
// get image.
// var bmp = camera.GetBitmap();
//
// adjust properties.
// UsbCamera.PropertyItems.Property prop;
// prop = camera.Properties[DirectShow.CameraControlProperty.Exposure];
// if (prop.Available)
// {
// prop.SetValue(DirectShow.CameraControlFlags.Manual, prop.Default);
// }
//
// prop = camera.Properties[DirectShow.VideoProcAmpProperty.WhiteBalance];
// if (prop.Available && prop.CanAuto)
// {
// prop.SetValue(DirectShow.CameraControlFlags.Auto, 0);
// }
// [Note]
// By default, GetBitmap() returns image of System.Drawing.Bitmap.
// If WPF, define 'USBCAMERA_WPF' symbol that makes GetBitmap() returns image of BitmapSource.
class UsbCamera
{
/// <summary>Usb camera image size.</summary>
public Size Size { get; private set; }
/// <summary>Start.</summary>
public Action Start { get; private set; }
/// <summary>Stop.</summary>
public Action Stop { get; private set; }
/// <summary>Release resource.</summary>
public void Release()
{
Releasing();
Released();
}
private Action Releasing;
private Action Released;
/// <summary>true if image buffer is ready and you can get bitmap.</summary>
/// <remarks>issue #38</remarks>
public bool IsReady { get { return Streams[StreamType.Capture].IsReady; } }
/// <summary>Get image.</summary>
/// <remarks>Immediately after starting, fails because image buffer is not prepared yet.</remarks>
public Func<Bitmap> GetBitmap { get; private set; }
private enum StreamType { Capture, Preview, Still }
private Dictionary<StreamType, SampleGrabberCallback> Streams = new Dictionary<StreamType, SampleGrabberCallback>();
/// <summary>
/// camera support still image or not.
/// some cameras can produce a still image and often higher quality than capture stream.
/// </summary>
public bool StillImageAvailable { get; private set; }
/// <summary>trigger still image capture.</summary>
public Action StillImageTrigger { get; private set; } = () => { }; // null guard.
/// <summary>called when still image captured by hardware button or software trigger.</summary>
/// <remarks>called by worker thread.</remarks>
public Action<Bitmap> StillImageCaptured
{
get { return Streams[StreamType.Still].Buffered; }
set { Streams[StreamType.Still].Buffered = value; }
}
/// <summary>
/// called when preview image captured. there is a little difference in behavior between WPF and WinForms.
/// in WPF, image is single instance and returns same reference for every call. image is updated internally in the library.
/// the image can only be used to show preview with data binding. DO NOT USE for other purpose.
/// in WinForms, returns different reference for every call.
/// </summary>
/// <remarks>called by worker thread. to maintain throughput on the capture pin, the preview pin drops frames as needed.</remarks>
public Action<Bitmap> PreviewCaptured
{
get
{
// allocate before first acccess.
if (Streams.ContainsKey(StreamType.Preview) == false) SetPreviewCallbackMain();
return Streams[StreamType.Preview].Buffered;
}
set
{
// allocate before first acccess.
if (Streams.ContainsKey(StreamType.Preview) == false) SetPreviewCallbackMain();
Streams[StreamType.Preview].Buffered = value;
}
}
private Action SetPreviewCallbackMain;
private Action<IntPtr> SetPreviewControlMain;
private Action<Size> SetPreviewSizeMain;
/// <summary>Set preview on control. Call before starts.</summary>
/// <param name="handle">control handle.</param>
public void SetPreviewControl(IntPtr handle, Size size)
{
SetPreviewControlMain(handle);
SetPreviewSizeMain(size);
}
/// <summary>Set preview size.</summary>
public void SetPreviewSize(Size size) { SetPreviewSizeMain(size); }
/// <summary>
/// Get available USB camera list.
/// </summary>
/// <returns>Array of camera name, or if no device found, zero length array.</returns>
public static string[] FindDevices()
{
return DirectShow.GetFiltes(DirectShow.DsGuid.CLSID_VideoInputDeviceCategory).ToArray();
}
/// <summary>
/// Get video formats.
/// </summary>
/// <returns>returns supported video formats, or if the supported format is unknown, returns VideoFormat.Default.</returns>
public static VideoFormat[] GetVideoFormat(int cameraIndex)
{
var filter = DirectShow.CreateFilter(DirectShow.DsGuid.CLSID_VideoInputDeviceCategory, cameraIndex);
var pin = DirectShow.FindPin(filter, 0, DirectShow.PIN_DIRECTION.PINDIR_OUTPUT);
try
{
// support the device that has no IAMStreamConfig interface.(issue #25)
return GetVideoOutputFormat(pin);
}
catch (Exception)
{
// can't get video format. maybe no IAMStreamConfig interface.
return new[] { VideoFormat.Default };
}
}
/// <summary>
/// Create USB Camera. If device do not support the size, default size will applied.
/// </summary>
/// <param name="cameraIndex">Camera index in FindDevices() result.</param>
/// <param name="size">
/// Size you want to create. Normally use Size property of VideoFormat in GetVideoFormat() result.
/// </param>
public UsbCamera(int cameraIndex, Size size) : this(cameraIndex, new VideoFormat() { Size = size })
{
}
/// <summary>
/// Create USB Camera. If device do not support the format, default format will applied.
/// </summary>
/// <param name="cameraIndex">Camera index in FindDevices() result.</param>
/// <param name="format">
/// Normally use GetVideoFormat() result.
/// You can change TimePerFrame value from Caps.MinFrameInterval to Caps.MaxFrameInterval.
/// TimePerFrame = 10,000,000 / frame duration. (ex: 333333 in case 30fps).
/// You can change Size value in case Caps.MaxOutputSize > Caps.MinOutputSize and OutputGranularityX/Y is not zero.
/// Size = any value from Caps.MinOutputSize to Caps.MaxOutputSize step with OutputGranularityX/Y.
/// </param>
public UsbCamera(int cameraIndex, VideoFormat format)
{
var camera_list = FindDevices();
if (cameraIndex >= camera_list.Length) throw new ArgumentException("USB camera is not available.", "cameraIndex");
Init(cameraIndex, format);
}
private void Init(int index, VideoFormat format)
{
//----------------------------------
// Create Filter Graph
//----------------------------------
var graph = DirectShow.CreateGraph();
var builder = DirectShow.CoCreateInstance(DirectShow.DsGuid.CLSID_CaptureGraphBuilder2) as DirectShow.ICaptureGraphBuilder2;
builder.SetFiltergraph(graph);
//----------------------------------
// VideoCaptureSource
//----------------------------------
var vcap_source = CreateVideoCaptureSource(index, format);
graph.AddFilter(vcap_source, "VideoCapture");
// PIN_CATEGORY_CAPTURE
//
// [Video Capture Source]
// +--------------------+ +----------------+ +---------------+
// | capture pin|→| Sample Grabber |→| Null Renderer |
// +--------------------+ +----------------+ +---------------+
// ↓GetBitmap()
{
var sample = ConnectSampleGrabberAndRenderer(graph, builder, vcap_source, DirectShow.DsGuid.PIN_CATEGORY_CAPTURE, format);
if (sample != null)
{
// release when finish.
Released += () => { var i_grabber = sample.Grabber; DirectShow.ReleaseInstance(ref i_grabber); };
Size = new Size(sample.Width, sample.Height);
// fix screen tearing problem(issue #2)
// you can use previous method if you swap the comment line below.
// when you use previous method, you can not use IsReady property.(added in issue #38)
/*GetBitmap = GetBitmapFromSampleGrabberBuffer(sample.Grabber, sample.Width, sample.Height, sample.Stride);
Func<Bitmap> GetBitmapFromSampleGrabberBuffer(DirectShow.ISampleGrabber grabber, int width, int height, int stride)
{
var sampler = new SampleGrabberBuffer(grabber, width, height, stride);
return () => sampler.GetBitmap();
}*/
GetBitmap = GetBitmapFromSampleGrabberCallback(sample.Grabber, sample.Width, sample.Height, sample.Stride);
Func<Bitmap> GetBitmapFromSampleGrabberCallback(DirectShow.ISampleGrabber grabber, int width, int height, int stride)
{
Streams[StreamType.Capture] = new SampleGrabberCallback(grabber, width, height, stride, false);
// release SampleGrabberCallback. (issue #49)
Releasing += () => Streams[StreamType.Capture].Release();
return () => Streams[StreamType.Capture].GetBitmap();
}
}
}
// PIN_CATEGORY_STILL
//
// [Video Capture Source]
// +--------------------+ +----------------+ +---------------+
// | still pin|→| Sample Grabber |→| Null Renderer |
// | | +----------------+ +---------------+
// | | +----------------+ +---------------+
// | capture pin|→| Sample Grabber |→| Null Renderer |
// +--------------------+ +----------------+ +---------------+
// ↓GetBitmap()
{
// support still image (issue #16)
// https://learn.microsoft.com/en-us/windows/win32/directshow/capturing-an-image-from-a-still-image-pin
// Some cameras can produce a still image separate from the capture stream,
// and often the still image is of higher quality than the images produced by the capture stream.
// The camera may have a button that acts as a hardware trigger, or it may support software triggering.
// A camera that supports still images will expose a still image pin, which is pin category PIN_CATEGORY_STILL.
var sample = ConnectSampleGrabberAndRenderer(graph, builder, vcap_source, DirectShow.DsGuid.PIN_CATEGORY_STILL, format);
if (sample != null)
{
// release when finish.
Released += () => { var i_grabber = sample.Grabber; DirectShow.ReleaseInstance(ref i_grabber); };
var still_pin = DirectShow.FindPin(vcap_source, 0, DirectShow.PIN_DIRECTION.PINDIR_OUTPUT, DirectShow.DsGuid.PIN_CATEGORY_STILL);
var video_con = vcap_source as DirectShow.IAMVideoControl;
if (video_con != null)
{
StillImageAvailable = true;
Streams[StreamType.Still] = new SampleGrabberCallback(sample.Grabber, sample.Width, sample.Height, sample.Stride, false);
// release SampleGrabberCallback. (issue #49)
Releasing += () => Streams[StreamType.Still].Release();
// To trigger the still pin, use the IAMVideoControl::SetMode method when the graph is running, as follows:
StillImageTrigger = () =>
{
video_con.SetMode(still_pin, DirectShow.VideoControlFlags.Trigger | DirectShow.VideoControlFlags.ExternalTriggerEnable);
};
}
}
}
// PIN_CATEGORY_PREVIEW
//
// [Video Capture Source]
// +--------------------+ +----------------+
// | preview pin|→| Video Renderer |
// | | +----------------+
// | | +----------------+ +---------------+
// | still pin|→| Sample Grabber |→| Null Renderer |
// | | +----------------+ +---------------+
// | | +----------------+ +---------------+
// | capture pin|→| Sample Grabber |→| Null Renderer |
// +--------------------+ +----------------+ +---------------+
// ↓GetBitmap()
//
// https://learn.microsoft.com/en-us/windows/win32/directshow/combining-video-capture-and-preview
// If the capture filter has a preview pin or video port pin, plus a capture pin, RenderStream method simply renders both pins.
// If the filter has only a capture pin, the Capture Graph Builder uses the Smart Tee filter to split the capture stream.
// The Smart Tee filter has a capture pin and a preview pin. It takes a single video stream from the capture filter
// and splits it into two streams, one for capture and one for preview.
// To maintain throughput on the capture pin, the preview pin drops frames as needed.
// Although the Smart Tee splits the stream, it does not physically duplicate the video data.
//
// If your capture graph has a preview window, several things can cause the Filter Graph Manager to stop the entire graph, including the capture stream:
// * Locking the computer.
// * Pressing CTRL+ALT+DELETE on a computer that is a member of a domain.
// * Running a full-screen Direct3D application, such as a game or screen saver.
// * Switching monitors or changing the display resolution.
// * Running a program that causes Windows to display a User Account Control (UAC) dialog. // (Windows Vista or later.)
// * Running a full-screen DOS window.
// Any of these events might interrupt the capture session, possibly causing data loss.
// preview by SetPreviewControl.
var makePreviewRender = false;
SetPreviewControlMain = (controlHandle) =>
{
var vw = graph as DirectShow.IVideoWindow;
if (vw == null) return;
// render stream only the first time.
if (makePreviewRender == false)
{
makePreviewRender = true;
var pinCategory = DirectShow.DsGuid.PIN_CATEGORY_PREVIEW;
var mediaType = DirectShow.DsGuid.MEDIATYPE_Video;
builder.RenderStream(ref pinCategory, ref mediaType, vcap_source, null, null);
}
vw.put_Owner(controlHandle);
vw.put_MessageDrain(controlHandle); // receive window messages sent to the video renderer.(issue #21)
};
// resize by SetPreviewSize.
SetPreviewSizeMain = (clientSize) =>
{
var vw = graph as DirectShow.IVideoWindow;
if (vw == null) return;
// calc window size and position with keep aspect.
var w = clientSize.Width;
var h = Size.Height * w / Size.Width;
if (h > clientSize.Height)
{
h = clientSize.Height;
w = Size.Width * h / Size.Height;
}
var x = (clientSize.Width - w) / 2;
var y = (clientSize.Height - h) / 2;
// set window owner.
const int WS_CHILD = 0x40000000; // cannot have a menu bar.
const int WS_CLIPSIBLINGS = 0x04000000; // clips child windows relative to each other when receives a WM_PAINT.
vw.put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS);
vw.SetWindowPosition((int)x, (int)y, (int)w, (int)h);
};
// preview by PreviewCaptured.(issue #18)
SetPreviewCallbackMain = () =>
{
var sample = ConnectSampleGrabberAndRenderer(graph, builder, vcap_source, DirectShow.DsGuid.PIN_CATEGORY_PREVIEW, format);
if (sample != null)
{
// release when finish.
Released += () => { var i_grabber = sample.Grabber; DirectShow.ReleaseInstance(ref i_grabber); };
Streams[StreamType.Preview] = new SampleGrabberCallback(sample.Grabber, sample.Width, sample.Height, sample.Stride, true);
// release SampleGrabberCallback. (issue #49)
Releasing += () => Streams[StreamType.Preview].Release();
}
};
// Start, Stop, Release, the filter graph.
Start = () => DirectShow.PlayGraph(graph, DirectShow.FILTER_STATE.Running);
Stop = () => DirectShow.PlayGraph(graph, DirectShow.FILTER_STATE.Stopped);
Releasing += () => Stop();
Released += () =>
{
DirectShow.ReleaseInstance(ref builder);
DirectShow.ReleaseInstance(ref graph);
};
// Properties.
Properties = new PropertyItems(vcap_source);
}
private class SampleGrabberInfo
{
public DirectShow.ISampleGrabber Grabber { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int Stride { get; set; }
}
private SampleGrabberInfo ConnectSampleGrabberAndRenderer(DirectShow.IFilterGraph graph, DirectShow.ICaptureGraphBuilder2 builder, DirectShow.IBaseFilter vcap_source, Guid pinCategory, VideoFormat format)
{
//------------------------------
// SampleGrabber
//------------------------------
var grabber = CreateSampleGrabber(format);
graph.AddFilter(grabber, "SampleGrabber");
var i_grabber = (DirectShow.ISampleGrabber)grabber;
i_grabber.SetBufferSamples(true);
//---------------------------------------------------
// Null Renderer
//---------------------------------------------------
var renderer = DirectShow.CoCreateInstance(DirectShow.DsGuid.CLSID_NullRenderer) as DirectShow.IBaseFilter;
graph.AddFilter(renderer, "NullRenderer");
//---------------------------------------------------
// Connects vcap_source to SampleGrabber and Renderer
//---------------------------------------------------
try
{
//var pinCategory = DirectShow.DsGuid.PIN_CATEGORY_CAPTURE;
var mediaType = DirectShow.DsGuid.MEDIATYPE_Video;
builder.RenderStream(ref pinCategory, ref mediaType, vcap_source, grabber, renderer);
}
catch (Exception)
{
// if camera does not support pin, an exception was raised.
// some camera do not support still pin.
return null;
}
// after render stream, retrieve SampleGrabber image information.
// stride changes depending on VideoFormat.
// +--------------+-----------------+-----------+
// | Video Format | Sample Grabber | Stride |
// +--------------+-----------------+-----------+
// | Y800 | do not convert | width * 1 |
// | Y16 | do not convert | width * 2 |
// | else | RGB24(bottomup) | width * 3 |
// +--------------+-----------------+-----------+
{
var mt = new DirectShow.AM_MEDIA_TYPE();
i_grabber.GetConnectedMediaType(mt);
var header = (DirectShow.VIDEOINFOHEADER)Marshal.PtrToStructure(mt.pbFormat, typeof(DirectShow.VIDEOINFOHEADER));
var width = header.bmiHeader.biWidth;
var height = header.bmiHeader.biHeight;
var stride = width * (header.bmiHeader.biBitCount / 8);
DirectShow.DeleteMediaType(ref mt);
return new SampleGrabberInfo() { Grabber = i_grabber, Width = width, Height = height, Stride = stride };
}
}
/// <summary>Properties user can adjust.</summary>
public PropertyItems Properties { get; private set; }
public class PropertyItems
{
public PropertyItems(DirectShow.IBaseFilter vcap_source)
{
// Pan, Tilt, Roll, Zoom, Exposure, Iris, Focus
this.CameraControl = Enum.GetValues(typeof(DirectShow.CameraControlProperty)).Cast<DirectShow.CameraControlProperty>()
.Select(item =>
{
PropertyItems.Property prop = null;
try
{
var cam_ctrl = vcap_source as DirectShow.IAMCameraControl;
if (cam_ctrl == null) throw new NotSupportedException("no IAMCameraControl Interface."); // will catched.
int min = 0, max = 0, step = 0, def = 0, flags = 0;
cam_ctrl.GetRange(item, ref min, ref max, ref step, ref def, ref flags); // COMException if not supports.
Action<DirectShow.CameraControlFlags, int> set = (flag, value) => cam_ctrl.Set(item, value, (int)flag);
Func<int> get = () => { int value = 0; cam_ctrl.Get(item, ref value, ref flags); return value; };
prop = new Property(min, max, step, def, flags, set, get);
}
catch (Exception) { prop = new Property(); } // available = false
return new { Key = item, Value = prop };
}).ToDictionary(x => x.Key, x => x.Value);
// Brightness, Contrast, Hue, Saturation, Sharpness, Gamma, ColorEnable, WhiteBalance, BacklightCompensation, Gain
this.VideoProcAmp = Enum.GetValues(typeof(DirectShow.VideoProcAmpProperty)).Cast<DirectShow.VideoProcAmpProperty>()
.Select(item =>
{
PropertyItems.Property prop = null;
try
{
var vid_ctrl = vcap_source as DirectShow.IAMVideoProcAmp;
if (vid_ctrl == null) throw new NotSupportedException("no IAMVideoProcAmp Interface."); // will catched.
int min = 0, max = 0, step = 0, def = 0, flags = 0;
vid_ctrl.GetRange(item, ref min, ref max, ref step, ref def, ref flags); // COMException if not supports.
Action<DirectShow.CameraControlFlags, int> set = (flag, value) => vid_ctrl.Set(item, value, (int)flag);
Func<int> get = () => { int value = 0; vid_ctrl.Get(item, ref value, ref flags); return value; };
prop = new Property(min, max, step, def, flags, set, get);
}
catch (Exception) { prop = new Property(); } // available = false
return new { Key = item, Value = prop };
}).ToDictionary(x => x.Key, x => x.Value);
}
/// <summary>Camera Control properties.</summary>
private Dictionary<DirectShow.CameraControlProperty, Property> CameraControl;
/// <summary>Video Processing Amplifier properties.</summary>
private Dictionary<DirectShow.VideoProcAmpProperty, Property> VideoProcAmp;
/// <summary>Get CameraControl Property. Check Available before use.</summary>
public Property this[DirectShow.CameraControlProperty item] { get { return CameraControl[item]; } }
/// <summary>Get VideoProcAmp Property. Check Available before use.</summary>
public Property this[DirectShow.VideoProcAmpProperty item] { get { return VideoProcAmp[item]; } }
public class Property
{
public int Min { get; private set; }
public int Max { get; private set; }
public int Step { get; private set; }
public int Default { get; private set; }
public DirectShow.CameraControlFlags Flags { get; private set; }
public Action<DirectShow.CameraControlFlags, int> SetValue { get; private set; }
public Func<int> GetValue { get; private set; }
public bool Available { get; private set; }
public bool CanAuto { get; private set; }
public Property()
{
this.SetValue = (flag, value) => { };
this.Available = false;
}
public Property(int min, int max, int step, int @default, int flags, Action<DirectShow.CameraControlFlags, int> set, Func<int> get)
{
this.Min = min;
this.Max = max;
this.Step = step;
this.Default = @default;
this.Flags = (DirectShow.CameraControlFlags)flags;
this.CanAuto = (Flags & DirectShow.CameraControlFlags.Auto) == DirectShow.CameraControlFlags.Auto;
this.SetValue = set;
this.GetValue = get;
this.Available = true;
}
public override string ToString()
{
return string.Format("Available={0}, Min={1}, Max={2}, Step={3}, Default={4}, Flags={5}", Available, Min, Max, Step, Default, Flags);
}
}
}
private class SampleGrabberCallback : DirectShow.ISampleGrabberCB
{
private byte[] Buffer;
private object BufferLock = new object();
public Action<Bitmap> Buffered { get; set; }
private System.Threading.AutoResetEvent BufferedEvent;
private BitmapBuilder BmpBuilder;
public bool IsReady { get { return Buffer != null; } }
public SampleGrabberCallback(DirectShow.ISampleGrabber grabber, int width, int height, int stride, bool useCache)
{
this.BmpBuilder = new BitmapBuilder(width, height, stride, useCache);
// create Buffered.Invoke thread.
BufferedEvent = new System.Threading.AutoResetEvent(false);
// use new Thread instead of ThreadPool. (issue #30)
var thread = new System.Threading.Thread(x =>
{
ThreadActive = true;
BufferedEvent.WaitOne(); // wait event.
while (ThreadActive)
{
Buffered?.Invoke(GetBitmap()); // fire!
BufferedEvent.WaitOne(); // wait next event.
}
});
thread.IsBackground = true;
thread.Start();
grabber.SetCallback(this, 1); // WhichMethodToCallback = BufferCB
}
private volatile bool ThreadActive;
/// <remarks>release SampleGrabberCallback. (issue #49)</remarks>
public void Release()
{
ThreadActive = false;
BufferedEvent.Set();
}
public Bitmap GetBitmap()
{
if (Buffer == null) return BmpBuilder.EmptyBitmap;
lock (BufferLock)
{
return BmpBuilder.BufferToBitmap(Buffer);
}
}
// called when each sample completed.
// The data processing thread blocks until the callback method returns. If the callback does not return quickly, it can interfere with playback.
public int BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
{
if (Buffer == null || Buffer.Length != BufferLen)
{
Buffer = new byte[BufferLen];
}
// replace lock statement to Monitor.TryEnter. (issue #14)
var locked = false;
try
{
System.Threading.Monitor.TryEnter(BufferLock, 0, ref locked);
if (locked)
{
Marshal.Copy(pBuffer, Buffer, 0, BufferLen);
}
}
finally
{
if (locked) System.Threading.Monitor.Exit(BufferLock);
}
// notify buffered to worker thread. (issue #16)
if (Buffered != null) BufferedEvent.Set();
return 0;
}
// never called.
public int SampleCB(double SampleTime, DirectShow.IMediaSample pSample)
{
throw new NotImplementedException();
}
}
private class SampleGrabberBuffer
{
private DirectShow.ISampleGrabber Grabber;
private IntPtr BufPtr;
private byte[] Buffer;
private BitmapBuilder BmpBuilder;
public SampleGrabberBuffer(DirectShow.ISampleGrabber grabber, int width, int height, int stride)
{
this.Grabber = grabber;
this.BmpBuilder = new BitmapBuilder(width, height, stride, false);
}
public Bitmap GetBitmap()
{
try
{
return GetBitmapMain();
}
catch (COMException ex)
{
const uint VFW_E_WRONG_STATE = 0x80040227;
if ((uint)ex.ErrorCode == VFW_E_WRONG_STATE)
{
// image data is not ready yet. return empty bitmap.
return BmpBuilder.EmptyBitmap;
}
throw;
}
}
private Bitmap GetBitmapMain()
{
// サンプルグラバから画像を取得するためには
// まずサイズ0でGetCurrentBufferを呼び出しバッファサイズを取得し
// バッファ確保して再度GetCurrentBufferを呼び出す。
// 取得した画像は逆になっているので反転させる必要がある。
int size = 0;
Grabber.GetCurrentBuffer(ref size, IntPtr.Zero); // IntPtr.Zeroで呼び出してバッファサイズ取得
if (size == 0) return null;
if (Buffer == null || size != Buffer.Length)
{
// メモリ確保
if (BufPtr != IntPtr.Zero) Marshal.FreeCoTaskMem(BufPtr);
BufPtr = Marshal.AllocCoTaskMem(size);
Buffer = new byte[size];
}
// 画像データ取得しbyte配列に入れなおす
Grabber.GetCurrentBuffer(ref size, BufPtr);
Marshal.Copy(BufPtr, Buffer, 0, size);
// 画像を作成
var result = BmpBuilder.BufferToBitmap(Buffer);
return result;
}
}
private class BitmapBuilder
{
private int Width, Height, Stride;
#if USBCAMERA_BYTEARRAY
// EmptyBitmap returns new instance. (issue #34)
public Bitmap EmptyBitmap
{
get { return new byte[Height * Stride]; }
}
public BitmapBuilder(int width, int height, int stride, bool dummy)
{
this.Width = width;
this.Height = height;
this.Stride = stride;
}
public Bitmap BufferToBitmap(byte[] buffer)
{
var result = new byte[buffer.Length];
Buffer.BlockCopy(buffer, 0, result, 0, result.Length);
return result;
}
#elif USBCAMERA_WPF
private readonly WriteableBitmap BmpCache;
private byte[] BufCache;
private const double dpi = 96.0;
// EmptyBitmap returns new instance. (issue #34)
public Bitmap EmptyBitmap
{
get
{
var bmp = new WriteableBitmap(Width, Height, dpi, dpi, PixelFormats.Bgr24, null);
bmp.Freeze();
return bmp;
}
}
public BitmapBuilder(int width, int height, int stride, bool useCache)
{
this.Width = width;
this.Height = height;
this.Stride = stride;
// less GC. do not allocate memory every time. (issue #18)
if (useCache)
{
// overwrite with BmpCache.
BmpCache = new WriteableBitmap(width, height, dpi, dpi, PixelFormats.Bgr24, null);
BufCache = null;
}
else
{
// createnew with BufCache.
BmpCache = null;
BufCache = new byte[Height * Width * 3]; // Bgr24 is 3 byte per pixel.
}
}
public Bitmap BufferToBitmap(byte[] buffer)
{
// if (useCache)
if (BmpCache != null)
{
// overwrite with BmpCache.
return BufferToBitmapOverwrite(buffer);
}
else
{
// createnew with BufCache.
return BufferToBitmapCreateNew(buffer);
}
}
private Bitmap BufferToBitmapOverwrite(byte[] buffer)
{
WriteableBitmap result = BmpCache;
// overwrite WritableBitmap in UI thread. (issue #18)
Application.Current?.Dispatcher.Invoke(() =>
{
// [in UI thread]
result.Lock();
{
// copy from last row.
for (int y = 0; y < Height; y++)
{
var src_idx = buffer.Length - (Stride * (y + 1));
var dst_ptr = IntPtr.Add(result.BackBuffer, result.BackBufferStride * y);
Marshal.Copy(buffer, src_idx, dst_ptr, Stride);
}
result.AddDirtyRect(new Int32Rect(0, 0, Width, Height));
}
result.Unlock();
});
return result;
}
private Bitmap BufferToBitmapCreateNew(byte[] buffer)
{
var result = new WriteableBitmap(Width, Height, dpi, dpi, PixelFormats.Bgr24, null);
// copy from last row.
for (int y = 0; y < Height; y++)
{
var src_idx = buffer.Length - (Stride * (y + 1));
var dst = result.BackBufferStride * y;
Buffer.BlockCopy(buffer, src_idx, BufCache, dst, Stride);
}
result.WritePixels(new Int32Rect(0, 0, Width, Height), BufCache, result.BackBufferStride, 0);
// if no Freeze(), StillImageCaptured image is not displayed in WPF.
result.Freeze();
return result;
}
#else
// EmptyBitmap returns new instance. (issue #34)
public Bitmap EmptyBitmap
{
get { return new Bitmap(Width, Height); }
}
public BitmapBuilder(int width, int height, int stride, bool dummy)
{
this.Width = width;
this.Height = height;
this.Stride = stride;
}
public Bitmap BufferToBitmap(byte[] buffer)
{
return UsbCamera.ByteArrayUtility.RGB24.CreateBitmap(buffer, this.Width, this.Height);
}
#endif
}
public static class ByteArrayUtility
{
public static class RGB24
{
#if USBCAMERA_WPF
/// <summary>create Bitmap from buffer.</summary>
/// <param name="buffer">data buffer.</param>
/// <param name="width">buffer width.</param>
/// <param name="height">buffer height.</param>
/// <returns>created BitmapSource.</returns>
public static BitmapSource CreateBitmap(byte[] buffer, int width, int height)
{
const double dpi = 96.0;
int stride = width * 3; // Bgr24 is 3 byte per pixel.
var result = new WriteableBitmap(width, height, dpi, dpi, PixelFormats.Bgr24, null);
var bmp_data = new byte[height * stride];
// copy from last row.
for (int y = 0; y < height; y++)
{
var src_idx = buffer.Length - (stride * (y + 1));
var dst = result.BackBufferStride * y;
Buffer.BlockCopy(buffer, src_idx, bmp_data, dst, stride);
}
result.WritePixels(new Int32Rect(0, 0, width, height), bmp_data, result.BackBufferStride, 0);
// if no Freeze(), StillImageCaptured image is not displayed in WPF.
result.Freeze();
return result;
}
#else
/// <summary>create Bitmap from buffer.</summary>
/// <param name="buffer">data buffer.</param>
/// <param name="width">buffer width.</param>
/// <param name="height">buffer height.</param>
/// <returns>created Bitmap.</returns>
public static System.Drawing.Bitmap CreateBitmap(byte[] buffer, int width, int height)
{
int stride = width * 3; // Format24bppRgb is 3 byte per pixel.
var result = new System.Drawing.Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
var bmp_data = result.LockBits(new Rectangle(Point.Empty, result.Size), System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
// copy from last row.
for (int y = 0; y < height; y++)
{
var src_idx = buffer.Length - (stride * (y + 1));
var dst = IntPtr.Add(bmp_data.Scan0, bmp_data.Stride * y);
Marshal.Copy(buffer, src_idx, dst, stride);
}
result.UnlockBits(bmp_data);
return result;
}
#endif
/// <summary>get the luminance value of the specified coordinates.</summary>
/// <param name="buffer">data buffer.</param>
/// <param name="width">buffer width.</param>
/// <param name="height">buffer height.</param>
/// <param name="x">x position.</param>
/// <param name="y">y position.</param>
/// <returns>luminance value.</returns>
public static int GetValue(byte[] buffer, int width, int height, int x, int y)
{
int stride = width * 3;
int bufptr = buffer.Length - (stride * (y + 1));
byte b = buffer[bufptr + (x * 3) + 0];
byte g = buffer[bufptr + (x * 3) + 1];
byte r = buffer[bufptr + (x * 3) + 2];
var result = (r << 16) + (g << 8) + b;
return result;
}
/// <summary>set the luminance value to the specified coordinates.</summary>
public static void SetValue(byte[] buffer, int width, int height, int x, int y, int value)
{
int stride = width * 3;
int bufptr = buffer.Length - (stride * (y + 1));
byte b = (byte)((value >> 0) & 0xFF);
byte g = (byte)((value >> 8) & 0xFF);
byte r = (byte)((value >> 16) & 0xFF);
buffer[bufptr + (x * 3) + 0] = b;
buffer[bufptr + (x * 3) + 1] = g;
buffer[bufptr + (x * 3) + 2] = r;
}
}
/// <summary>
/// The Y800 is an 8-bit, monochrome format. Every pixel is represented by a single byte.
/// The pixels in the image buffer are organized from left to right and from top to bottom.
/// </summary>
public static class Y800
{
#if USBCAMERA_WPF
/// <summary>create Bitmap from buffer.</summary>
/// <param name="buffer">data buffer.</param>
/// <param name="width">buffer width.</param>
/// <param name="height">buffer height.</param>
/// <returns>created BitmapSource.</returns>
public static BitmapSource CreateBitmap(byte[] buffer, int width, int height)
{
const double dpi = 96.0;
var result = new WriteableBitmap(width, height, dpi, dpi, PixelFormats.Bgr24, null);
var bmp_data = new byte[height * width * 3]; // Bgr24 is 3 byte per pixel.
// copy from first row.
int cnt = 0;
for (int y = 0; y < height; y++)
{
var dst_idx = y * result.BackBufferStride;