티스토리 뷰

반응형

지난번 시간에 Home Automation의 시나리오로 집 앞에 움직임이 감지 되었을 때, 사진 촬영 후 Detect 하여 낯선 사람이면 Azure에 전송하는 시나리오를 염두해 두고 OpenCV 기반에 Test를 진행했었다(http://hero-space.tistory.com/35). 다만 이 경우는 Windows 에서 제공하는 API가 아닌 OpenCV라는 Open source project를 ARM용으로 빌드해서 dll을 link 한뒤 사용해야 하기 때문에 번거로운 일이 아닐 수 없었는데 실제 OpenCV 관련 샘플을 소개하고 있는 곳을 가서 따라서 해보면 Image load 되고 canny 효과 까지 되나 얼굴 Detect시 Exception이 발생한다.(https://developer.microsoft.com/ko-kr/windows/iot/samples/opencv) 그래서 이번 글에서는 OpenCV로 진행시 발생했던 문제점과 그리고 해결하기 위해서 해보았던 것들을 위주로 적으려하며 결국 이번 글에서의 목표는 Windows 10 IoT core 기반에 Web Cam 영상 Preview 이다.

# OpenCV, 한계에 부딪히다.

사실 OpenCV는 굉장히 오래전부터 나온 영상처리 Library로 많은 사람들이 사용해 왔고, 사용되고 있는 Open source project 이다. 현재는 3.0 버전까지 Release 되어서 여러가지의 기능들이 있는데 C++ 기반이므로 C#에서 사용하려면 dll로 빌드하여 link 시켜 사용해야한다. 우선 내가 진행하는 프로젝트는 C# 기반의 Windows 10 IoT Core project로 ARM용으로 빌드되어져야 함에 OpenCV 소스를 받아 ARM용으로 Visual studio에서 빌드하여 dll을 뽑아 내었다. 다양한 API가 OpenCV 내에 존재하기 때문에, dll로 여러개로 나오는데 우선 microsoft 에서 windows 10 IoT Core용으로 설명해 놓은 Sample에는 저장되어 있는 Image를 로드하여 Canny 효과와 Face/Body를 찾는 것을 설명하고 있다. github를 통해 해당 소스는 받아볼 수 있고 OpenCV만 잘 빌드해서 자신의 프로젝트에 link 됬다면 Raspberry에 올리는 것 까지는 진행 할 수 있을 것이다.(이미 이전 글에서 작성 했었다.) 다만 샘플 프로젝트 말고 직접 내 프로젝트에 적용해보려 하니 몇가지 문제가 발생하였다.

1. OpenCV로 Body/Face Detect시 Exception

 - github에 있는 OpenCV 샘플은 Windows 10 IoT core platform 버전은 10.0.10240 버전이다.(작년 하반기 버전정도). 내가 사용하는 버전은 10.10.14393 버전이며 application 작성시 대상 최소 platform 버전을 10.0.10240으로 해놨음에도 github에 있는 OpenCV 샘플과 정확한 동작을 보여주지 않고있다. 샘플에는, '저장된 Image 로드', 'Canny 적용', 'Body, Face 검출' 세가지의 기능을 보여주고 있는데 내가 직접 생성한 Project는 Body/Face Detect시 Exception이 발생한다. 여러가지 방법으로 디버깅 해보았지만 Application에서 문제로는 보이지 않는 것이 동일한 소스이기 때문이며 Platform Version의 영향으로 발생하는 것으로 최종 결론을 내렸다. 최종으로는 Camera의 영상을 실시간으로 뿌려주면서 얼굴을 찾아야하도록 만들어야하는데 만약 이 문제를 풀지 못한다면 OpenCV를 사용할 수 없다고 생각되었다.

2. OpenCV로 Video Capture 시 Exception

- OpenCV를 빌드하면 여라가지 dll이 나온다고 말했었다. 그 중에서 Video capture에 관련된 것은 'opencv_videoio300.dll' 로 생각되는데 github에서 제공하는 sample에는 해당 dll을 include 하고 있지 않기에 동일한 방법으로 link 시켜서 코드를 작성해보았다.

 
 cv::VideoCapture cap(0); 
	
if (!cap.isOpened()) {
	printf("Cannot open the video cam\n");
	return;
}
else
{
	printf("Open the video cam\n");
}
while (1) {
        cv:Mat frame;
	bool bOK = cap.grab();
	if (bOK) {
		cap.read(frame);
		cv::cvtColor(frame, _stored_image, CV_BGR2BGRA);
		UpdateImage(_stored_image);
	}
	else {
		break;
	}
}

cv namespace 안에 VideoCapture라는 Class가 있기 때문에 연결된 camera를 초기화한후 frame을 cpature해서 image 를 window에 뿌려주게 하는 기본적인 코드이다. 여러가지 방법이 있지만 위 코드와 함께 캡쳐시 exception 아래에서 발생한다.

cap_winrt_bridge.cpp 내,

 
// non-blocking
void VideoioBridge::requestForUIthreadAsync(int action)
{
    reporter.report(action);
}

Window RT에서 Video IO 를 위한 작업중 나는 것으로 보이는데 결국 이 Error로 잡지 못했다. 따라서 OpenCV를 빌드하여 dll로 사용 후 C++ Windows 10 IoT Core 프로젝트로 Home Automation 프로젝트를 진행하는 것은 무리가 있다고 판단 했다.

#OpenCV를 위한 NuGet 패키지

앞서 C++ 프로젝트를 생성하여 OpenCV를 dll로 빌드 후 사용해보았었는데 C#에서 OpenCV를 사용하려면 직접 빌드해야할 뿐만 아니라 C++ class 들을 Wrapping 해주어야 한다. 한두개의 API가 아니기 때문에 상당히 이 작업도 번거롭다. 하지만 이미 NuGet 패키지 매니저를 검색하면 C#을 위한 Wrapper 패키지가 존재한다. 

이리저리 검색해보니 OpenCvSharp3-AnyCPU는 일본인이 만든 것으로 참고자료가 많다고 하며 이 외에도 EmguCV라는 OpenCV Wapper도 있다. 사용법은 using으로 OpenCvSharp을 포함하고 내부의 API를 C# 처럼 코딩하면 된다고 나와있어서 기쁜마음에 설치해 보았다.

하지만 당연히 설치는 실패! 이유는 현재 UWP 로 생성한 내 프로젝트와는 UAP(Universal Application Platform)의 호환이 안된다고 메시지가 나온다. 가만히 UWP라는 개념을 다시 돌이켜 보면 Core는 같고 PC든 Raspberry 든, Mobile이던 하나의 App으로 사용할 수 있는게 UWP인데 UAP가 호환이 안된다는건 설치하려는 OpenCvSharp wrapper가 UWP 아키텍쳐에는 적용할 수 없다는 결론이 나온다. 실제로 Windows Form 프로젝트나 클래스 라이브러리 프로젝트로 생성하면 OpenCvSharp Wrapper가 설치된다. 결국 UWP 에서 NuGet으로 C#용 OpenCV wrapper를 설치할 수 없었다.

# 영상처리는 Cloud Platform의 API를 이용하자.

사실 OpenCV를 사용하려던 목적은 영상처리에 있다. 얼굴을 인식하는 것이 주 목적이며 Library를 내포하고 있기 때문에 인식률 개선을 위해선 많은 데이터를 학습시켜야한다. 6000장 정도 학습시키면 60~70%의 인식률이 나오지 않을까 생각된다. 하지만 요즘 Cloud에서 제공하는 영상처리 솔루션을 보면 지속적인 학습의 결과로 인식률이 상당히 높다. 어차피 wifi에 연결이 되어 있다면 Cloud에서 제공하는 영상처리를 이용하는 것도 좋을거라 생각했고, Camera에서 image preview와 Capture후 image만 보낼 수 있다면 굳이 OpenCV를 사용하지 않아도 될듯하였다. 또한 MS에서도 이런 시대의 흐름에 따라 Cloud에서 제공하는 Vision 인식 API를 제공하고 있었다.

(https://www.microsoft.com/cognitive-services/en-us/computer-vision-api), 우선 이에 앞서서 기본 Windows API로 C# 프로젝트 내에 Webcam을 초기화 하고 화면에 Preview 하는 기능을 만들어 볼 것이다.

먼저 UI 화면은 위처럼 구성한다. xaml 파일에 Layer를 구분해서 Webcam에서 뿌려준 영상을 표시할 StackPanel을 지정해 놓는다.  편의를 위해서 DisabledFeedGrid를 생성후 미리 CmeraIcon.png를 준비하여 Asset 폴더에 넣어 놓고 UI 로딩시 Camera가 초기화 되기 전에 이를 뿌려주도록 할 것이다. Webcam이 준비가 되면 LibFeedPanel 영역에 뿌려주도록 할 것인데 WebCamFeed_Loaded라는 mothod를 링크시켜서 UI로딩시 호출되게 한다. 

<Grid x:Name="DisabledFeedGrid" Grid.Column="1">

<Grid.RowDefinitions>

<RowDefinition Height="2*" />

<RowDefinition Height="*" />

</Grid.RowDefinitions>

<Rectangle Margin="10" Stroke="White" StrokeThickness="4" Grid.RowSpan="2"/>

<Image x:Name="image" HorizontalAlignment="Center" VerticalAlignment="Center" Source="Assets/CameraIcon.png" Margin="50"/>

<TextBlock x:Name="DisabledText" TextWrapping="Wrap" Text="Not Initialized." HorizontalAlignment="Center" VerticalAlignment="Top" Grid.Row="1" FontSize="33.333" TextAlignment="Center" Margin="10,0"/>

</Grid>

<StackPanel x:Name="LiveFeedPanel" Margin="0, 10" Grid.Row="0" Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">

<CaptureElement x:Name='WebcamFeed' Loaded="WebcamFeed_Loaded"/>

</StackPanel>

<StackPanel x:Name="AnalysingVisitorGrid" Grid.Column="2" VerticalAlignment="Center" Margin="0,0,30,0" Visibility="Collapsed">

<ProgressRing IsActive="True" Width="113" Height="80" Foreground="White" VerticalAlignment="Center" Margin="0,0,0,10"/>

<TextBlock VerticalAlignment="Center" FontSize="26.667" Grid.Row="1" Text="Analyzing visitor..." TextWrapping="WrapWholeWords" TextAlignment="Center"/>

<TextBlock VerticalAlignment="Center" FontSize="21.333" Grid.Row="1" Text="This may take some time depending on your hardware." TextWrapping="WrapWholeWords" TextAlignment="Center" FontStyle="Italic"/>

</StackPanel>


MainPage.xaml.cs 내의 WebCamFeed_Loaded 메소드를 보면 아래와 같은데 Web cam이 초기화 되어 있지 않으면 초기화 시키고 초기화 후에는 Preview를 Start 시키게 된다. 이때 Webcam Helper 객체를 하나 생성해 주며 Webcam 관련된 것은 별도의 class로 정리 해두었다.
 
        private async void WebcamFeed_Loaded(object sender, RoutedEventArgs e)
        {
            if (webcam == null || !webcam.IsInitialized())
            {
                // Initialize Webcam Helper
                webcam = new WebcamHelper();
                await webcam.InitializeCameraAsync();

                // Set source of WebcamFeed on MainPage.xaml
                WebcamFeed.Source = webcam.mediaCapture;

                // Check to make sure MediaCapture isn't null before attempting to start preview. Will be null if no camera is attached.
                if (WebcamFeed.Source != null)
                {
                    // Start the live feed
                    await webcam.StartCameraPreview();
                }
            }
            else if (webcam.IsInitialized())
            {
                WebcamFeed.Source = webcam.mediaCapture;

                // Check to make sure MediaCapture isn't null before attempting to start preview. Will be null if no camera is attached.
                if (WebcamFeed.Source != null)
                {
                    await webcam.StartCameraPreview();
                }
            }
        }

WebCamHelper.cs 는 아래와 같이 구성되며 기본적으로 Windows에서 제공하는 API를 사용하므로 설명은 자세한 소스 설명은 생략한다. 다만 이렇게 클래스를 별도로 구분하여 사용함으로써 추후에 재사용이 용이해질 것이다.

 
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.Devices.Enumeration;
using Windows.Media.Capture;
using Windows.Media.MediaProperties;
using Windows.Storage;
using Windows.UI.Xaml.Media.Imaging;

namespace csharp_webcam.Helpers
{
    public class WebcamHelper
    {
        public MediaCapture mediaCapture;

        private bool initialized = false;
        public async Task InitializeCameraAsync()
        {
            if (mediaCapture == null)
            {
                // Attempt to get attached webcam
                var cameraDevice = await FindCameraDevice();

                if (cameraDevice == null)
                {
                    // No camera found, report the error and break out of initialization
                    Debug.WriteLine("No camera found!");
                    initialized = false;
                    return;
                }

                // Creates MediaCapture initialization settings with foudnd webcam device
                var settings = new MediaCaptureInitializationSettings { VideoDeviceId = cameraDevice.Id };

                mediaCapture = new MediaCapture();
                await mediaCapture.InitializeAsync(settings);
                initialized = true;
            }
        }

        /// 
        /// Asynchronously looks for and returns first camera device found.
        /// If no device is found, return null
        /// 
        private static async Task FindCameraDevice()
        {
            // Get available devices for capturing pictures
            var allVideoDevices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);


            if (allVideoDevices.Count > 0)
            {
                // If there is a device attached, return the first device found
                return allVideoDevices[0];
            }
            else
            {
                // Else, return null
                return null;
            }
        }

        /// 
        /// Asynchronously begins live webcam feed
        /// 
        public async Task StartCameraPreview()
        {
            try
            {
                await mediaCapture.StartPreviewAsync();
            }
            catch
            {
                initialized = false;
                Debug.WriteLine("Failed to start camera preview stream");

            }
        }

        /// 
        /// Asynchronously ends live webcam feed
        /// 
        public async Task StopCameraPreview()
        {
            try
            {
                await mediaCapture.StopPreviewAsync();
            }
            catch
            {
                Debug.WriteLine("Failed to stop camera preview stream");
            }
        }


        /// 
        /// Asynchronously captures photo from camera feed and stores it in local storage. Returns image file as a StorageFile.
        /// File is stored in a temporary folder and could be deleted by the system at any time.
        /// 
        public async Task CapturePhoto()
        {
            // Create storage file in local app storage
            string fileName = GenerateNewFileName() + ".jpg";
            CreationCollisionOption collisionOption = CreationCollisionOption.GenerateUniqueName;
            StorageFile file = await ApplicationData.Current.TemporaryFolder.CreateFileAsync(fileName, collisionOption);

            // Captures and stores new Jpeg image file
            await mediaCapture.CapturePhotoToStorageFileAsync(ImageEncodingProperties.CreateJpeg(), file);

            // Return image file
            return file;
        }

        /// 
        /// Generates unique file name based on current time and date. Returns value as string.
        /// 
        private string GenerateNewFileName()
        {
            return DateTime.UtcNow.ToString("yyyy.MMM.dd HH-mm-ss") + " Facial Recognition Door";
        }

        /// 
        /// Returns true if webcam has been successfully initialized. Otherwise, returns false.
        /// 
        public bool IsInitialized()
        {
            return initialized;
        }
    }
}

마지막으로 Pakage.appmanifest 에서 기능 tab에 웹캠과 그림라이브러 기능에 체크한후 Raspberry에 배포를 진행한다.

이렇게 해서 보면 Preview 기능이 구현되었는데 지난번 PIR 센서에 의해서 물체의 움직임이 감지되었을때 녹화하는 기능과(http://hero-space.tistory.com/36)는 조금 다르게 계속 찍고 있다가 움직임이 감지되면 촬영해서 Cloud에 보내도록 접근할 것이다. 이미 Capture하는 API도 WebCamHelper 클래스에 구현되어 있으니 실제 Cloud와 연동하는 방법을 다음에 다룰 것이다. 우선 오늘은 여기까지!

반응형
댓글