본문 바로가기

Developement

M$.NET/VS2005/C# 에서 USB camera 해상도 설정 관련 사항.

초접사 USB cam 으로 찍은..

WPF 란걸 써야 하는 조건이라면 어쩔 수 없이 개발자들은 M$ 의 덩치크고 메모리킬러 인 .NET + C# 이라는 조건을 받아 들여야 할 것입니다.
WPF 란 이쁘고 화려한 UI 까지는 다 좋은데, 안타깝게도 이 방식은 제가 볼때 Layered Window 위에다 .NET 이 열심시 이미지와 각종 컴퍼넌트 등을 그리고 있는 걸 합니다.
또한 GDI+ 를 wrapping 한 듯한 rendering 속도를 보여 주는 걸로 보아 조금 답답한 면도 있구요.

일단 C# 선생은 제가 볼때 (절대 제 게인적인 생각입니다) 10년 전 부터 제가 써온 Delphi 가 채용하고 있는 Application Framework + 객체지향 개념과 java 등등이 잘 버무려져 있는 무거운 언어 입니다. 다른 점 이라면, WPF 라는 새로운 UI 개념이 탑재 되어 있다는 거겠지요.
Java 처럼 GC(가비지 콜렉터)가 있어서 new 를 볼 수는 있지만 delete 는 보이지 않습니다...
그래서 WPF + 조금 복잡한 application 이 탄생하면 메모리를 한 100MB 는 기본으로 쓰는 듯 합니다.

잡설을 일단 접고, 현재 C# 에서 DirectX 의 DirectShow 를 쓰려면 OpenSource 인 DShow 라이브러리를 사용해야 합니다.
이 방식은 WPF 의 윈도우 핸들에서는 사용할 수 없으며, 표준 window 에만 적용이 될 수 있습니다.

C# 에서는 C/C++ 과는 달리 #include .. 대신에 using namespace 를 사용합니다. 
(이 using 은 Delphi 에서 이미 uses 라는 표준 Pascal 문법과 매우 유사하다는 건 .. 저만의 생각이겠죠?)
제작하는 project root 에 DShow 라이브러리를 넣고 using DShowLibrary; 와 using System.Runtime.InteropServices.ComTypes; 를 선언 함 으로서 DShow 를 사용할 준비가 완료 됩니다.

DShow 를 쓰려면 일단 해당 클래스가 다음 클래스를 상속받는 구조가 되어야 합니다.
IDisposable
ISampleGrabberCB
IDisposable 은 preview 를 위한 윈도우 컨트롤 후 메모리를 날리기 위해 필요한 .Dispose(); 를 위해 필요하고,
ISampleGrabberCB 는 DShow 에서 기정의 한 이미지 및 동영상 캡쳐를 위한 SampleCB, BufferCB 때문에 필요한 부분 입니다.

이제 DShow 로 USB camera 를 생성 하기 위해서는 다음 사항이 먼저 class 내에 정의 되어 있어야 합니다.
        private IBaseFilter _capFilter; 
        private IGraphBuilder _graphBuilder;
        private ICaptureGraphBuilder2 _captureGraphBuilder; 
        private ISampleGrabber _sampleGrabber;
        private IMediaEventEx _mediaEventEx;
        private IVideoWindow _videoWindow;
        private IBaseFilter _baseGrabFilter;
        private IAMStreamConfig _streamConfig;
위 사항은 기본적으로 USB camera 의 영상을 가져오고 캡쳐 하기 위해 필요한 인터페이스들 이 되며, 이는 모두 COM 에서 얻어 오는 구조가 됩니다.

DShow 에서 가장 처음 알아야 하는 것은 바로 USB 영상 장비의 Index 입니다.
이 USB 장비들은 DShow 내의 DsDevice 클래스로 부터, GetDeivcesCat() 함수로 얻어 올 수 있습니다.
간단히 각 장치를 알아 와서 뭔가 한다면 다음 처럼 만들 수 있겠습니다.
foreach (DsDevice ds in DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice))
{
    deviceNames.Add(ds.Name);
}
C# 부터는 foreach() 가 있어서 for() 대신에 유용하게 쓸 수 있는 것이 있어서 단순 목록은 물론, 검색 등에 다양하게 사용이 가능합니다.
각 장치의 순서가 바로 그 장치의 index 가 되며, 이중 필요한 장비를 최기화 한다면 다음 과 같이 할 수 있겠습니다.
_capFilter = CreateFilter(_deviceIndex);
if (null == _capFilter) return S_FALSE;
USB 카메라를 처음 접근 하기 위해서는 CaptureFilter를 먼저 생성해야 합니다.
그런다음, IGraphBuilder 인터페이스를 통해 새로운 필터그래프를 하나 만들어야 합니다.
_graphBuilder = (IGraphBuilder)new FilterGraph();
if (null != _graphBuilder)
{
    ...................
}
새로운 클래스 나 메서드 등을 만들때 외에 일반적인 type-cast 를 한다면, C# 에서는 C/C++ 처럼 type-cast 를 쓸 수도 있지만 "as" 라는 것이 있어서 특정 캐스팅을 다음과 같이 할 수도 있습니다.
_mediaControl        = _graphBuilder as IMediaControl;
이건 마치 VisualBasic 같은 느낌도 듭니다 .. -_-;;
이제 _graphBuilder 가 생성 되었다면 각 중요한 아이들을 잡아 줘야 겠죠.
다음 처럼 할 수 있습니다.
_captureGraphBuilder = (ICaptureGraphBuilder2)new CaptureGraphBuilder2();
_sampleGrabber       = (ISampleGrabber)new SampleGrabber();
_mediaControl        = _graphBuilder as IMediaControl;
_videoWindow         = _graphBuilder as IVideoWindow;
_mediaEventEx        = _graphBuilder as IMediaEventEx;
_baseGrabFilter      = _sampleGrabber as IBaseFilter;
이걸로 끝이면 참 편리 하겠지만 DShow 는 좀 더 많은 일을 해야 합니다 .. -_-;;;
다음으로 필요 한 것은 _captureGraphBuilder 에 이미 만들어진 _graphBuilder (FilterGraph 임) 를 지정해 줘야 합니다.
hr = _captureGraphBuilder.SetFiltergraph(_graphBuilder);
DsError.ThrowExceptionForHR(hr);
hr 은 C# 에 HRESULT 형이 없으므로 그냥 int 로 선언 해 쓰면 됩니다.
물론 C/C++ 에서는 HRESULT 를 사용해 주시는게 맞습니다만..
DsError 클래스는 DShow 에서 발생하는 오류를 HR 값을 가지고 Throw Exception 처리를 해 줍니다.
try - catch - finally 라는 개념을 잘 안쓰던 C/C++ 개발자라면 조금 생소한 부분일 수 도 있겠습니디만 ..
이 try -- catch 부분은 80386 CPU 부터 CPU 상에서 지원되는 몇가지 기능을 활용하는 훌륭한 기능 입니다. 나온지 꽤 오래 되었지만 쓸 수 있는 언어가 많지 않았지요.

이제 다음으로 필요한 건, _captureGraphBuilder 에 지정한 _graphBuilder 에 또 다시 제일 처음 생성한 _capFilter 를 이름을 붙여 집어 넣어 줘야 합니다 ... 귀찮지만 해 줘야 합니다 ... 아놔 ...
hr = _graphBuilder.AddFilter(_capFilter, "video device");
DsError.ThrowExceptionForHR(hr);
붙이는 이름은 마음대로 넣어도 됩니다만, 나중에 이름으로 찾아 써야 할 때를 고려 한다면 좋게 지어 넣어 주는 센스를 가지는 것 또한 나쁘진 않습니다.
물론 C# 과 .NET 조합은 모든 문자열이 UniCode 임을 고려 해야 합니다. C++ 에서 LPWStr 형과 동일 합니다.

이쯤 오면 대부분 다음엔 바로 _captureGrabber 를 만들고 Stream render 를 설정해 버리는 예제 코드들로 인터넷에 도배가 되어 있을 겁니다.
하지만 이래서는 연결된 USB cam 에 대한 정보를 내가 먼저 알고 뭔가 하기란 어려워 지게 되죠.
그래서!

바로 이 시쯤에서 _streamConfig 을 설정하고, 이로부터 장치가 지원하는 해상도를 알아 오도록 합니다.
가장 중요한 것은 _streamConfig 을 _captureGraphBuilder 로 부터 찾아내 와야 합니다.
COM 에서 가장 많이보는 FindInterface() 함수를 사용합니다.
또한 C# 에서는 IntPtr 이란 것이 있어서 포인터의 주소를 얻어오는 독특한 구조를 가끔 사용함으로 다음 예제에서 처럼 기정의된 구조에 맞춰 사용해 줘야 하는 점도 있습니다.
일단 StreamConfig 을 가져오는 부분이 빈번하게 발생하는 걸 고려 해서 간단히 함수를 만들어 보면 다음과 같은 구조로 되더군요. 물론 더 좋은 방법이 있다는걸 생각해 보셔야 합니다.
자, 이제 지원하는 해상도 정보를 얻어 오려면 다음 루틴 처럼 만들 수 있습니다.

처음 C# 을 쓸대 포인터 에서 구조체/클래스 간으로 전환 하는 부분이 적응이 매우 어려웠었습니다.
Marshal 이라는 게 C# 에서 참 중요한 부분인 듯 하게 느껴지는건 이걸 처음 C/C++ 코드에서 가져올때 인 듯 하네요.
위 코드를 통해서 VideoInfoHeader 를 이용한 지원 해상도와 정보를 알아 왔으니 이젠 해상도를 설정 할 차례 입니다.
해상도 설정은 _streamConfig 에 하게 되며, _streamConfig 에 지정된 해상도는 알아서 _sampleGrabber 에 적용되게 됩니다.

코드를 보면 ...

참고 : 위 코드에서 _videoResolutionList는 List<AMMediaType> 객체로 만들어 두었습니다.
나름 안전한 방법을 생각해서 만든 코드이라 복잡합니다만 , 핵심은 바로 _stramConfig.SetFormat(); 함수 입니다. SetFormat() 을 적용할 수 있는 시점은 바로 _captureGraphBuilder 에 RenderStraem() 을 지정하기 전 입니다.
RenderStream() 을 지정하고 나서는 _streamConfig 에 SetFormat() 을 하게 되면 잘못된 미디어 오류가 발생하게 됩니다.

그래서 RenderStream() 을 지정하기 전에 반드시 _streamConfig 에 먼저 해상도 조절을 해 두어야지만 적용이 가능하게 됩니다.

이 부분에 대해서는 구글링을 아무리 해도 나오는 게 없었던지라 ..
노가다 뛰어서 알아내어야 하더군요 .. OTL...
혹시나 해서 포스팅 해 봅니다.