티스토리 뷰

반응형

Home Automation 프로젝트로 기획했던 Home Security Solution에 대한 그 네 번째 시간이다. 두 번째 시간에는 인체감지 센서에 의해서 움직임이 감지되면 촬영을 한후 Play 하는 걸 해봤었고, 이전 시간엔 사진을 MS Cloud 에서 제공하는 Face API를 아용하여 얼굴을 찾아주고 사진에 얼굴 영역을 그리는 것 까지 진행했다. 오늘은 두 번째, 세 번째 시간에 했던걸 하나로 통합하는 시간을 가져보려 하는데 결국 Home Security Solution의 윤곽이 드러날 수 있을 것이며 추가적으로 보완이 필요한 내용도 도출될 수 있을 것 같다. 그 전에 다시 한 번 전체 시나리오를 확인해보자.

문 앞에 누군가 감지되었을 때, 카메라를 통해서 촬영하고 낯선 사람인 경우 사용자에게 알림을 준다.

이전까지의 진행사항으로 움직임이 감지했을시 촬영하는 것과, 별도의 사진을 Cloud API로 얼굴을 찾는 것을 했기 때문에 이제 움직임 감지시 촬영해서 얼굴을 찾는 일까지 쭉 이어서 될 수 있게 해 볼 것이며 엄굴이 감지되면 파일을 계속 저장하고 감지되지 않으면 파일을 삭제한다. 또한, 움직임이 없을 때도, 웹 캠의 Preview를 확인 할 수 있도록 할 것이다. 차후 최종 UI로도 사용할 수 있도록 UI Layout도 구성해 볼 것이다.

# Hardware / Software 구성

Hardware의 구성은 http://hero-space.tistory.com/36 의 구성과 달라진 것이 없다. 웹 캠은 USB Host로 RaspberryPI에 연결하고 확장 케이블을 통해 LCD, PIR 센서, LED를 달아 놓는다. 아직은 정돈된 느낌이 없지만 완성되면 전체 외형도 하나의 제품처럼 꾸며 볼 것이다. 그럼 이제 본격적으로 통합 및 추가적인 Software 개발을 위해서 진행한 일들을 하나씩 살펴보자.

1) UI 화면 구성

xaml 파일로 구성한 UI는 위와 같다. 왼쪽에는 웹캠이 초기화 되면 Preview 가 나올 것이고, PIR 센서에 의해서 움직임이 감지되서 얼굴이 인식되면 오른쪽에 사진과, 얼굴영역이 그려진 모습이 보일 것이다. 또한 하단 부의 작은 버튼 2개가 보이는데 이것은 메뉴얼로 사진을 촬영하고 삭제할 수 있는 기능으로 보면 되는데, 개발 중에 PIR 센서 없이 동작 확인을 위해 추가해놓은 것이다. XAML 코드가 이번에는 조금 길기 때문에 전체를 보여드리도록 하겠다.

<Page

    x:Class="PIR_PHOTO_CHECKFACE.MainPage"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="using:PIR_PHOTO_CHECKFACE"

    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

    xmlns:customCanvas="using:Microsoft.Graphics.Canvas.UI.Xaml"

    mc:Ignorable="d" RequestedTheme="Dark">


    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

        <Grid.RowDefinitions>

            <RowDefinition Height="3*" />

            <RowDefinition Height="1*" />

        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="*" />

            <ColumnDefinition Width="*" />

        </Grid.ColumnDefinitions>

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

            <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="0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">

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

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

        </StackPanel>


        <Grid x:Name="DisabledPhotoGrid" 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="photoimage" HorizontalAlignment="Center" VerticalAlignment="Center" Source="Assets/CameraIcon.png" Margin="50"/>

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

        </Grid>

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

            <CaptureElement x:Name='PhotoFeed' />

            <Grid>

                <Image x:Name="captureimage" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="50" />

                <customCanvas:CanvasControl Draw="canvasControl_Draw" x:Name="CustomCanvas" />

            </Grid>

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

        </StackPanel>

        <StackPanel x:Name="AnalysingVisitorGrid" Grid.Column="1" 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"/>

        </StackPanel>

    </Grid>

    <Page.BottomAppBar>


        <CommandBar IsOpen="True" IsSticky="True" IsEnabled="True" Foreground="#FF7C3C3C">

            <AppBarButton x:Name="TaskPhto" Label="Manual Photo" Icon="Camera" Foreground="#FFFFFFFF" Width="64" Click="takePhoto_click"/>

            <AppBarButton x:Name="Power" Label="Preview" Icon="Delete" Foreground="#FFFFFFFF" Width="64" Click="RemovePhoto_click"/>

        </CommandBar>

    </Page.BottomAppBar>

</Page>

 

기존 글에서 설명한 내용이 대부분인데 추가적으로 진행한 내용은 두개의 영역으로 분할하고 촬영된 사진이 Load 되는 부분에서 얼굴영역을 Drawing 해주어야 하기 때문에 Grid 안에 CanvasControl을 사용했다. Preview를 뿌려주는 StackPanel 영역으 보면 Grid가 없다. CanvasControl이 Grid가 아닌영역에서는 적용되지 않아서 이렇게 진행하였다. Page.BottomAppBar 영역에는 AppBarButton 을 2개 추가해서 사용할 수 있도록 했다. 기존에 진행한 2가지 내용을 통합한 것이기 때문에 궁금하신 세세한 내용은 이전 글들을 참고하시면 될 것 같다.

2) Library

참조로 사용한 Library도 기존에서 새롭게 추가된 것은 없다. Microsoft.ProjectOxford.Face, Newtonsoft.Json, Win2D.uwp, Windows IoT Extensions for the UWP 이 정도이고 코드가 점점 많아져서 분리를 했는데, 크게 Main 부와, Definition, ExtraDevice, OpenAPI 부로 나눠진다. Main 부는 말그대로 최초 시작해서 Main 로직이 들어가 있고, Main 로직을 돌면서 필요한 부분은 OpenAPI와 ExtraDevice(LCD, WebCam)으로 분리하였고, const 상수의 경우도 별도로 Class 화 시켰다. Main 코드도 지금까지 한 내용중에 많기 때문에 전체를 붙여넣었다.

 
using System;
using System.Linq;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;                     // 타이머용
using System.Threading.Tasks;               // 타이머용
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.Media.MediaProperties;
using Windows.UI.Xaml.Media.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.Devices.Gpio;                 // GPIO용
using Windows.Devices.Enumeration;
using Windows.UI;

using PIR_PHOTO_CHECKFACE.ExtraDevice;      // User defined class
using PIR_PHOTO_CHECKFACE.Definition;       // User defined constants
using Microsoft.ProjectOxford.Face;
using Microsoft.ProjectOxford.Face.Contract;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Windows.UI.Core;

namespace PIR_PHOTO_CHECKFACE
{
    /// 
    /// 자체적으로 사용하거나 프레임 내에서 탐색할 수 있는 빈 페이지입니다.
    /// 
    public sealed partial class MainPage : Page
    {
        private StorageFile photoFile;
        private string fileName = "photo.jpeg";        // 저장용 파일 이름

        private bool isSensor = false;      // 센서용 플래그
        private GpioPin pin;
        private GpioPin statusPin;
        LCD lcd;
        private WebCam webcam;
        FaceRectangle[] _faceRectangles;
        private readonly IFaceServiceClient faceServiceClient = new FaceServiceClient(OpenAPIConstants.OxfordFaceAPIKey);
        public MainPage()
        {
            this.InitializeComponent();
            initLCD();
            Loaded += MainPage_Loaded;
            LiveFeedPanel.Visibility = Visibility.Collapsed;
            DisabledFeedGrid.Visibility = Visibility.Visible;
        }

        private async void initLCD()
        {
            lcd = new LCD(16, 2);
            await lcd.InitAsync(20, 16, 2, 3, 4, 17); //16X2 LCD and works on 4bits
            await lcd.clearAsync();
        }

        private async void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
           
            await initGpio();
        }

        private async void WebcamFeed_Loaded(object sender, RoutedEventArgs e)
        {
            if (webcam == null || !webcam.IsInitialized())
            {
                // Initialize Webcam Helper
                webcam = new WebCam();
                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
                    LiveFeedPanel.Visibility = Visibility.Visible;
                    DisabledFeedGrid.Visibility = Visibility.Collapsed;
                    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)
                {
                    LiveFeedPanel.Visibility = Visibility.Visible;
                    DisabledFeedGrid.Visibility = Visibility.Collapsed;
                    
                    await webcam.StartCameraPreview();
                }
            }

            if(LiveFeedPanel.Visibility == Visibility.Visible)
            {
                LiveFeedText.Text = "Start Preview.";
            }
        }

        async void UploadAndDetectFaces(string imageFile)
        {
            try
            {           
                var storageFile = await Windows.Storage.KnownFolders.PicturesLibrary.GetFileAsync(imageFile);
                var randomAccessStream = await storageFile.OpenReadAsync();

                using (Stream stream = randomAccessStream.AsStreamForRead())
                {
                    //this is the fragment where face is recognized:
                    var faces = await faceServiceClient.DetectAsync(stream);
                    var faceRects = faces.Select(face => face.FaceRectangle);


                    _faceRectangles = faceRects.ToArray();
                    if(_faceRectangles.Length == 0)
                    {
                        PhotoLiveFeedText.Text = "Not Detected.";
                        PhotoLiveFeedPanel.Visibility = Visibility.Collapsed;
                        DisabledPhotoGrid.Visibility = Visibility.Visible;
                        // If a face is not detected, the file will be deleted.
                        await storageFile.DeleteAsync();
                    }
                    else
                    {
                        
                        PhotoLiveFeedText.Text = "Face Detected.";
                       
                        CustomCanvas.Invalidate();
                    }
                   
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message);
            }

            AnalysingVisitorGrid.Visibility = Visibility.Collapsed;
            isSensor = false;
        }

        void canvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
        {
            /*if (_faceRectangles != null)
                if (_faceRectangles.Length > 0)
                {
                    foreach (var faceRect in _faceRectangles)
                    {
                        args.DrawingSession.DrawRectangle(faceRect.Left, faceRect.Top, faceRect.Width, faceRect.Height, Colors.Red);
                    }
                }*/
        }

        private async void takePhoto_click(object sender, RoutedEventArgs e)
        {
            AnalysingVisitorGrid.Visibility = Visibility.Visible;

            string time = DateTime.Now.ToString("yyyyMMddHHMMss");
            fileName = string.Format("capture_{0}.jpeg", time);

            photoFile = await Windows.Storage.KnownFolders.PicturesLibrary.CreateFileAsync(
                            fileName, Windows.Storage.CreationCollisionOption.GenerateUniqueName);


            ImageEncodingProperties imageProperties = ImageEncodingProperties.CreateJpeg();

            await webcam.mediaCapture.CapturePhotoToStorageFileAsync(imageProperties, photoFile);

            IRandomAccessStream photoStream = await photoFile.OpenReadAsync();
            BitmapImage bitmap = new BitmapImage();
            bitmap.SetSource(photoStream);
            PhotoLiveFeedPanel.Visibility = Visibility.Visible;
            DisabledPhotoGrid.Visibility = Visibility.Collapsed;

            captureimage.Source = bitmap;

            Debug.WriteLine("fileName : " + fileName);
            Debug.WriteLine("PATH : " + photoFile.Path);

            UploadAndDetectFaces(fileName);
        }

        private void RemovePhoto_click(object sender, RoutedEventArgs e)
        {
            Debug.WriteLine("Remove File !!!");
        }


        private async Task initGpio()
        {
            var gpio = GpioController.GetDefault();
            if (gpio == null)
            {
                Debug.WriteLine("Not found GPIO");
                return;
            }

            // Set GPIO
            pin = gpio.OpenPin(GpioConstants.PirSensorPin);

            if (pin.IsDriveModeSupported(GpioPinDriveMode.InputPullUp))
            {
                pin.SetDriveMode(GpioPinDriveMode.Input);
            }
            else
            {
                pin.SetDriveMode(GpioPinDriveMode.Input);
            }

            // ACT LED 설정
            statusPin = gpio.OpenPin(GpioConstants.CheckLedPin);
            statusPin.Write(GpioPinValue.High);
            statusPin.SetDriveMode(GpioPinDriveMode.Output);

            Debug.WriteLine("Initiaizing GPIO");

            // wait
            await Task.Delay(10000);

            // 이벤트 등록
            pin.ValueChanged += Pin_ValueChanged;

            Debug.WriteLine("Initialized GPIO");
            statusPin.Write(GpioPinValue.Low); //If all devices are reday, LED should be turned on
        }

        private async void Pin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
        {
            if (args.Edge == GpioPinEdge.RisingEdge)
            {
                Debug.WriteLine("Signal Detect(Rising)");
                lcd.WriteLine(" ");
                lcd.WriteLine("Detected");
                if (isSensor == false)
                {
                    isSensor = true;
                    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
                    {
                        await TakePhoto();
                    });

                }
            }

            if (args.Edge == GpioPinEdge.FallingEdge)
            {
                Debug.WriteLine("Signal Detect(Falling)");
                lcd.WriteLine(" ");
                lcd.WriteLine("Clear");
            }
        }

        private async Task TakePhoto()
         {
            Debug.WriteLine("Call TaskPhoto due to sensor signal");
            AnalysingVisitorGrid.Visibility = Visibility.Visible;
           
            string time = DateTime.Now.ToString("yyyyMMddHHMMss");
            fileName = string.Format("capture_{0}.jpeg", time);

            photoFile = await Windows.Storage.KnownFolders.PicturesLibrary.CreateFileAsync(
                            fileName, Windows.Storage.CreationCollisionOption.GenerateUniqueName);


            ImageEncodingProperties imageProperties = ImageEncodingProperties.CreateJpeg();

            await webcam.mediaCapture.CapturePhotoToStorageFileAsync(imageProperties, photoFile);

            IRandomAccessStream photoStream = await photoFile.OpenReadAsync();
            BitmapImage bitmap = new BitmapImage();
            bitmap.SetSource(photoStream);
            PhotoLiveFeedPanel.Visibility = Visibility.Visible;
            DisabledPhotoGrid.Visibility = Visibility.Collapsed;

            captureimage.Source = bitmap;

            Debug.WriteLine("fileName : " + fileName);
            Debug.WriteLine("PATH : " + photoFile.Path);

            UploadAndDetectFaces(fileName);
           
         }
    }
}

쭉 보면 Preview를 위한 부분과 Photo를 위한 부분 그리고 PIR 센서, Web Cam을 위한 부분으로 볼 수 있는데, 저마다 결과에 따라 UI가 변경되도록 각각의 방법을 이용해서 변경해 주고 있다. 다시말해서 PIR 센서가 움직임을 포착하여 Signal이 high가 걸리면 Pin_ValueChanged가 호출되고 이때 RisingEdge를 확인 한뒤 TakePhoto Task를 생성하여 불러준다. 반면에 Manual로 Photo를 찍도록 하단부의 버튼을 누르면 takePhoto_click이 호출되면서 간단하게 UI가 업데이트되도록 할수 있는데 그 이유는 메소드의 매개변수로 있는 'RoutedEventArgs' 때문이다. 라우트된 이벤트와 연결된 상태정보 및 이벤트 데이터를 포함하고 있어서 UI 변수 값을 바꿔서 반영시킬수 있다. 또한 xaml 파일을 보면 takePhoto_click이 Click 이러는 속성에 넣어져 있음을 알 수 있다. 반면에 PIR 센서에 의해서 이벤트가 걸리면 RoutedEventArgs를 가지고 있지 않기 때문에 Dispatcher를 통해 UI에 비동기적으로 update 해주도록 한다. 또한 사진이 촬영되면 바로 Cloud 의 Face API를 통해서 얼굴 검출 여부를 확인하고 확인되지 않으면 저장하지 않고 삭제해버려서 불필요한 저장공간 낭비를 줄인다. 이 다음에 진행될 것이지만 등록된 얼굴과 비교 검출하는 작업도 진행될 예정이다. 따라서 네트웍으로 RaspberryPI에 사진 폴더에 가보면 얼굴이 검출된 경우의 사진만 저장되었을 확인할 수 있다.

참고로 얼굴이 검출되는 경우 원래는 빨간색으로 사각형 영역을 그려주려고 했으나, Preview 이미지 사이즈와 저장된 사진의 사이즈가 다른경우 Preview 이미지 사이즈 기준으로 네 꼭지점영역을 리턴해오기 때문에 이부분은 우선 제거했다. 좀 전에 촬영된 사진이다. 실제 처음부터 어떻게 동작이 되는지 동영상으로 확인해보자.

이렇게 해서, PIR 센서에 의해 움직임이 감지되면 Interrupt가 발생해 웹캠으로 사진촬영을 하게 되고 촬영된 이미지는 바로 Cloud Server로 보내져 Face API를 통해 얼굴을 검출한 결과를 알려주게 된다. 알려준디 나는 얼굴이 검출됬는지 여부를 얼굴에 뿌려주고 얼굴이 검출되지 않은 사진은 바로 삭제해버린게 되는 것이다. 다음에 진행할 내용은 이제 DB와의 연동이며 사용자를 등록하고 등록된 사용자가 아닐 경우만 저장하는 식의 시나리오를 가져가야 하는데 우선 오늘은 여기까지~!

반응형
댓글