forked from secile/UsbCamera
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathUsbCamera.cs
2085 lines (1793 loc) · 96.1 KB
/
UsbCamera.cs
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
using Bitmap = System.Windows.Media.Imaging.BitmapSource;
#elif USBCAMERA_BYTEARRAY
using Bitmap = System.Collections.Generic.IEnumerable<byte>;
using System.Drawing;
#else
using System.Drawing;
#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);
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);
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);
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);
// 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);
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);
}
};
// 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)
{
//------------------------------
// SampleGrabber
//------------------------------
var grabber = CreateSampleGrabber();
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;
}
// SampleGrabber Format.
{
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 =>
{
while (true)
{
BufferedEvent.WaitOne(); // wait event.
Buffered?.Invoke(GetBitmap()); // fire!
}
});
thread.IsBackground = true;
thread.Start();
grabber.SetCallback(this, 1); // WhichMethodToCallback = BufferCB
}
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_WPF
private readonly bool UseCache;
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;
this.UseCache = useCache;
if (UseCache)
{
BmpCache = new WriteableBitmap(width, height, dpi, dpi, PixelFormats.Bgr24, null);
}
else
{
// less GC. do not allocate memory every time. (issue #18)
var lenght = Height * Stride;
BufCache = new byte[lenght];
}
}
public Bitmap BufferToBitmap(byte[] buffer)
{
if (UseCache)
return BufferToBitmapOverwrite(buffer);
else
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));
Marshal.Copy(buffer, src_idx, IntPtr.Add(result.BackBuffer, Stride * y), 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));
Buffer.BlockCopy(buffer, src_idx, BufCache, Stride * y, Stride);
}
result.WritePixels(new Int32Rect(0, 0, Width, Height), BufCache, Stride, 0);
// if no Freeze(), StillImageCaptured image is not displayed in WPF.
result.Freeze();
return result;
}
#elif USBCAMERA_BYTEARRAY
// EmptyBitmap returns new instance. (issue #34)
public Bitmap EmptyBitmap
{
get { return new byte[0]; }
}
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[Width * Height * 3];
// copy from last row.
for (int y = 0; y < Height; y++)
{
var src_idx = buffer.Length - (Stride * (y + 1));
var dst = Stride * y;
Buffer.BlockCopy(buffer, src_idx, result, dst, Stride);
}
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)
{
// in WinForms, always allocate Bitmap memory.
var result = new 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, Stride * y);
Marshal.Copy(buffer, src_idx, dst, Stride);
}
result.UnlockBits(bmp_data);
return result;
}
#endif
}
/// <summary>
/// サンプルグラバを作成する
/// </summary>
private DirectShow.IBaseFilter CreateSampleGrabber()
{
var filter = DirectShow.CreateFilter(DirectShow.DsGuid.CLSID_SampleGrabber);
var ismp = filter as DirectShow.ISampleGrabber;
// サンプル グラバを最初に作成したときは、優先メディア タイプは設定されていない。
// これは、グラフ内のほぼすべてのフィルタに接続はできるが、受け取るデータ タイプを制御できないとうことである。
// したがって、残りのグラフを作成する前に、ISampleGrabber::SetMediaType メソッドを呼び出して、
// サンプル グラバに対してメディア タイプを設定すること。
// サンプル グラバは、接続した時に他のフィルタが提供するメディア タイプとこの設定されたメディア タイプとを比較する。
// 調べるフィールドは、メジャー タイプ、サブタイプ、フォーマット タイプだけである。
// これらのフィールドでは、値 GUID_NULL は "あらゆる値を受け付ける" という意味である。
// 通常は、メジャー タイプとサブタイプを設定する。
// https://msdn.microsoft.com/ja-jp/library/cc370616.aspx
// https://msdn.microsoft.com/ja-jp/library/cc369546.aspx
// サンプル グラバ フィルタはトップダウン方向 (負の biHeight) のビデオ タイプ、または
// FORMAT_VideoInfo2 のフォーマット タイプのビデオ タイプはすべて拒否する。
var mt = new DirectShow.AM_MEDIA_TYPE();
mt.MajorType = DirectShow.DsGuid.MEDIATYPE_Video;
mt.SubType = DirectShow.DsGuid.MEDIASUBTYPE_RGB24;
ismp.SetMediaType(mt);
return filter;
}
/// <summary>
/// Video Capture Sourceフィルタを作成する
/// </summary>
private DirectShow.IBaseFilter CreateVideoCaptureSource(int index, VideoFormat format)
{
var filter = DirectShow.CreateFilter(DirectShow.DsGuid.CLSID_VideoInputDeviceCategory, index);
var pin = DirectShow.FindPin(filter, 0, DirectShow.PIN_DIRECTION.PINDIR_OUTPUT);
// support the device that has no IAMStreamConfig interface.(issue #25)
if (format != VideoFormat.Default)
{
SetVideoOutputFormat(pin, format);
}
return filter;
}
/// <summary>
/// ビデオキャプチャデバイスの出力形式を選択する。
/// </summary>
private static void SetVideoOutputFormat(DirectShow.IPin pin, VideoFormat format)
{
var formats = GetVideoOutputFormat(pin);
// 仕様ではVideoCaptureDeviceはメディア タイプごとに一定範囲の出力フォーマットをサポートできる。例えば以下のように。
// [0]:YUY2 最小:160x120, 最大:320x240, X軸4STEP, Y軸2STEPごと
// [1]:RGB8 最小:640x480, 最大:640x480, X軸0STEP, Y軸0STEPごと
// SetFormatで出力サイズとフレームレートをこの範囲内で設定可能。
// ただし試した限り、手持ちのUSBカメラはすべてサイズ固定(最大・最小が同じ)で返してきた。
// https://msdn.microsoft.com/ja-jp/windows/dd407352(v=vs.80)
// VIDEO_STREAM_CONFIG_CAPSの以下を除くほとんどのメンバーはdeprecated(非推奨)である。
// アプリケーションはその他のメンバーの利用を避けること。かわりにIAMStreamConfig::GetFormatを利用すること。
// - Guid:FORMAT_VideoInfo or FORMAT_VideoInfo2など。
// - VideoStandard:アナログTV信号のフォーマット(NTSC, PALなど)をAnalogVideoStandard列挙体で指定する。
// - MinFrameInterval, MaxFrameInterval:ビデオキャプチャデバイスがサポートするフレームレートの範囲。100ナノ秒単位。
// 上記によると、VIDEO_STREAM_CONFIG_CAPSは現在はdeprecated(非推奨)であるらしい。かわりにIAMStreamConfig::GetFormatを使用することらしい。
// 上記仕様を守ったデバイスは出力サイズを固定で返すが、守ってない古いデバイスは出力サイズを可変で返す、と考えられる。
// 参考までに、VIDEO_STREAM_CONFIG_CAPSで解像度・クロップサイズ・フレームレートなどを変更する手順は以下の通り。
// ①フレームレート(これは非推奨ではない)
// VIDEO_STREAM_CONFIG_CAPS のメンバ MinFrameInterval と MaxFrameInterval は各ビデオ フレームの最小の長さと最大の長さである。
// 次の式を使って、これらの値をフレーム レートに変換できる。
// frames per second = 10,000,000 / frame duration
// 特定のフレーム レートを要求するには、メディア タイプにある構造体 VIDEOINFOHEADER か VIDEOINFOHEADER2 の AvgTimePerFrame の値を変更する。
// デバイスは最小値と最大値の間で可能なすべての値はサポートしていないことがあるため、ドライバは使用可能な最も近い値を使う。
// ②Cropping(画像の一部切り抜き)
// MinCroppingSize = (160, 120) // Cropping最小サイズ。
// MaxCroppingSize = (320, 240) // Cropping最大サイズ。
// CropGranularityX = 4 // 水平方向細分度。
// CropGranularityY = 8 // 垂直方向細分度。
// CropAlignX = 2 // the top-left corner of the source rectangle can sit.
// CropAlignY = 4 // the top-left corner of the source rectangle can sit.
// ③出力サイズ
// https://msdn.microsoft.com/ja-jp/library/cc353344.aspx
// https://msdn.microsoft.com/ja-jp/library/cc371290.aspx
// VIDEO_STREAM_CONFIG_CAPS 構造体は、このメディア タイプに使える最小と最大の幅と高さを示す。
// また、"ステップ" サイズ"も示す。ステップ サイズは、幅または高さを調整できるインクリメントの値を定義する。
// たとえば、デバイスは次の値を返すことがある。
// MinOutputSize: 160 × 120
// MaxOutputSize: 320 × 240
// OutputGranularityX:8 ピクセル (水平ステップ サイズ)
// OutputGranularityY:8 ピクセル (垂直ステップ サイズ)
// これらの数値が与えられると、幅は範囲内 (160、168、176、... 304、312、320) の任意の値に、
// 高さは範囲内 (120、128、136、... 224、232、240) の任意の値に設定できる。
// 出力サイズの可変のUSBカメラがないためデバッグするには以下のコメントを外す。
// I have no USB camera of variable output size, uncomment below to debug.
//size = new Size(168, 126);
//vformat[0].Caps = new DirectShow.VIDEO_STREAM_CONFIG_CAPS()
//{
// Guid = DirectShow.DsGuid.FORMAT_VideoInfo,
// MinOutputSize = new DirectShow.SIZE() { cx = 160, cy = 120 },
// MaxOutputSize = new DirectShow.SIZE() { cx = 320, cy = 240 },
// OutputGranularityX = 4,
// OutputGranularityY = 2
//};
// VIDEO_STREAM_CONFIG_CAPSは現在では非推奨。まずは固定サイズを探す
// VIDEO_STREAM_CONFIG_CAPS is deprecated. First, find just the fixed size.
for (int i = 0; i < formats.Length; i++)
{
var item = formats[i];
// VideoInfoのみ対応する。(VideoInfo2はSampleGrabber未対応のため)
// VideoInfo only... (SampleGrabber do not support VideoInfo2)
// https://msdn.microsoft.com/ja-jp/library/cc370616.aspx
if (item.MajorType != DirectShow.DsGuid.GetNickname(DirectShow.DsGuid.MEDIATYPE_Video)) continue;
if (string.IsNullOrEmpty(format.SubType) == false && format.SubType != item.SubType) continue;
if (item.Caps.Guid != DirectShow.DsGuid.FORMAT_VideoInfo) continue;
if (item.Size.Width == format.Size.Width && item.Size.Height == format.Size.Height)
{
SetVideoOutputFormat(pin, i, format.Size, format.TimePerFrame);
return;
}
}
// 固定サイズが見つからなかった。可変サイズの範囲を探す。
// Not found fixed size, search for variable size.
for (int i = 0; i < formats.Length; i++)
{
var item = formats[i];