Microsoft MVP 박문찬입니다. 날씨가 추워졌네요..오전에 보일러 고쳐서 참 다행인듯합니다. -0-;;

이전 포스트들을 진행 한 후에 이번 포스트를 보시면 좀 접근하는데 쉬울 것 같습니다.


Face API 사용해보기


Template10을 이용해서 UWP 앱 개발하기 Part1


MVVM Pattern에 대해서 자세히 알고 싶으시면 아래 내용을 참고하시면 됩니다.

Todo list Universal & UWP app


MVVM pattern 설명 동영상(오래전에 녹화한 내용입니다.)

http://youtu.be/f9aQkuoiPz4
http://youtu.be/uGxboAUwciI
http://youtu.be/2lQQiBEjbtU


MVVM Light Toolkit Fundamentals


삽잡이님 블로그

http://shovelman.tistory.com/695
http://shovelman.tistory.com/697
http://shovelman.tistory.com/705
http://shovelman.tistory.com/706



이전 포스트에서는 사진에서 얼굴 위치를 찾아오는 것 까지만 진행 했습니다. 이번에는 사진 속 인물이 누구인지, 학습을 시킨 후에 전혀 다른 사진을 이용해서 해당 사용자를 찾아내는 작업을 해 보도록 하겠습니다.



1. 참고

How to Identify Faces in Image



2. 인증 과정


1) groupId를 이용해서 그룹을 생성합니다. -> CreatePersonGroupAsync

2) 그룹에 사용자를 추가해서 personId를 반환 받습니다. -> CreatePersonAsync

3) groupId와 personId를 이용해서 이미지를 추가합니다.  -> AddPersonFaceAsync

4) 한 사람당 3장 이상의 이미지를 등록 후 머신 러닝을 실행합니다. -> GetPersonGroupTrainingStatusAsync

5) 인식을 원하는 이미지를 이용해서 DetectAsync를 실행합니다.

6) 찾아낸 FaceId를 모아서 인증을 수행합니다. -> IdentifyAsync

7) 인증된 사용자의 정보를 조회 합니다. -> GetPersonAsync


3. FaceIdentify 앱 만들기 시작


File -> New -> Project


Hambuger(Template 10)을 선택하고, FaceIdentify라고 입력하고 OK를 클릭합니다.



Build 후 비주얼 스튜디오를 종료 하고, 다시 시작합니다. (제사한 사항은 Template 10에 대한 포스트 참고)

F5를 눌러서 시작합니다.


햄버거 템플릿의 기본 화면입니다. 여기에 2개의 화면을 추가할 예정입니다. (아무래도 포스트가 2개 이상 필요할 듯하네요;;)





템플릿으로 프로젝트를 만들었는데 실행 했을 때 앱의 기본 골격이 보여지니 내가 필요한 것 몇개만 추가하면 바로 앱이 완성 됩니다. 물론, 솔루션 탐색기에 여러가지 파일들에 대해서 다 알면 좋겠지만, 몰라도 괜찮습니다. 딱 필요한 것만 알고 넘어 가도록 하겠습니다.



4. GroupPage 추가


그룹을 관리하고, 사람을 추가하고, 사진을 추가할 페이지를 추가하도록 하겠습니다.


솔루션 탐색기 Views 폴더에서 마우스 오른쪽 클릭 -> Add -> New Item...을 선택



GroupPage를 입력하고 Add를 클릭합니다.




GroupPage.xaml


<Page
    x:Class="FaceIdentify.Views.GroupPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:FaceIdentify.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:controls="using:Template10.Controls"
    xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
    xmlns:core="using:Microsoft.Xaml.Interactions.Core"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
       
        <!--페이지 해더-->
        <controls:PageHeader x:Name="pageHeader" Text="Group Page">
            <controls:PageHeader.SecondaryCommands>
                <!--사람 삭제-->
                <AppBarButton Label="Remove Person" Command="{Binding RemovePersonCommand}"/>
            </controls:PageHeader.SecondaryCommands>
            <!--사람 추가-->
            <AppBarButton Icon="AddFriend" Label="Add Person" Command="{Binding AddPersonCommand}"/>
            <!--현재 선택된 사람에 사진 추가-->
            <AppBarButton Icon="Pictures" Label="Add Picture" Command="{Binding AddPictureCommand}"/>
            <!--트레이닝-->
            <AppBarButton Label="Train" Command="{Binding TrainCommand}">
                <AppBarButton.Icon>
                    <FontIcon Glyph="&#xE82E;"/>
                </AppBarButton.Icon>
            </AppBarButton>
        </controls:PageHeader>

        <Grid x:Name="InputName" Grid.Row="1" Margin="0 10 4 0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="Person" VerticalAlignment="Center"/>

            <!--기존 등록된 사람 목록 선택용 콤보 박스-->
            <ComboBox x:Name="comboBox" Grid.Column="1" HorizontalAlignment="Stretch" Margin="10,0,0,0"
                ItemsSource="{Binding Persons}" DisplayMemberPath="Name">
                <interactivity:Interaction.Behaviors>
                    <core:EventTriggerBehavior EventName="SelectionChanged">
                        <core:InvokeCommandAction Command="{Binding SelectionChangedPersonCommand}"
                                                  CommandParameter="{Binding SelectedItem, ElementName=comboBox}"/>
                    </core:EventTriggerBehavior>
                </interactivity:Interaction.Behaviors>
            </ComboBox>

            <!--추가할 사람 이름-->
            <TextBox Grid.Column="2" Margin="6 0 0 0"
                     Text="{Binding PersonName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        </Grid>

        <!--현재 선택된 사람-->
        <Grid x:Name="personName" Grid.Row="2" Margin="0 10 4 0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="Current Person"/>
            <TextBlock Grid.Column="1" Margin="6 0 0 0" Text="{Binding CurrentPerson.Name}"/>
            <TextBlock Grid.Column="2" Text="Reg Pics" Margin="6 0 0 0"/>
            <TextBlock Grid.Column="3" Margin="6 0 0 0"
                       Text="{Binding CurrentPerson.PersistedFaceIds.Length}"
                       HorizontalAlignment="Right"/>
        </Grid>

        <!--현재 선택된 사람에게 추가된 이미지(나중에 다시 보는 기능은 없습니다)-->
        <ListBox Grid.Row="3"
                 Margin="0 10 4 10"
                 ItemsSource="{Binding CurrentPerson.FaceImages}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Image Source="{Binding}" Stretch="UniformToFill"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Page>


XAML 코드 한줄 한줄이 어떻게 만들어 졌는지는..따로 설명을 드리기가 어렵겠네요..ㅜㅜ 지금 진행하고 있는 앱 개발 기초 과정에서 2주간에 걸쳐서 만들어진 코드랍니다.



5. Shell.xaml에 메뉴 추가하기


솔루션 탐색기에서 Shell.xaml 파일을 더블 클릭해서 디자인 화면을 열고, 30라인에 아래 내용을 붙여 넣기 하시면 됩니다. (라인 번호가 보이지 않으면, Tool -> Options -> Text Editor -> All Languages -> Line numbers에 체크를 해주시면 됩니다.)


            <Controls:HamburgerButtonInfo PageType="views:GroupPage">
                <StackPanel Orientation="Horizontal">
                    <SymbolIcon Width="48" Height="48"
                                Symbol="People" />
                    <TextBlock Margin="12,0,0,0" VerticalAlignment="Center"
                               Text="Group" />
                </StackPanel>
            </Controls:HamburgerButtonInfo>


실행해서 디자인 확인하기


다음과 같은 화면이 나오면 정상 입니다. 이제, 내용을 넣도록 하겠습니다.




6. FaceAPIHelper 추가하기

Nuget packages에서 Microsoft.ProjectOxford.Face를 선택해서 추가합니다. (이전 포스트에서 다루었던 내용입니다.)


Services 폴더를 선택하고 마우스 오른쪽, Add -> Class를 선택 -> FaceAPIHelper입력 -> OK


Input your service key에 여러분의 키를 입력하시면 됩니다. 역시 이전 포스트에서 다루는 내용입니다.


using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.ProjectOxford.Face;
using Microsoft.ProjectOxford.Face.Contract;

namespace FaceIdentify.Services
{
    public class FaceAPIHelper
    {
        private static FaceAPIHelper _instance;

        // Create an empty person group
        private readonly string _personGroupId = "myfriends";

        private readonly IFaceServiceClient faceServiceClient =
            new FaceServiceClient("Input your service key");

        private bool _init;

        /// <summary>
        ///     생성자
        /// </summary>
        public FaceAPIHelper()
        {
            Init();
        }

        /// <summary>
        ///     인스턴스
        /// </summary>
        public static FaceAPIHelper Instance
        {
            get { return _instance = _instance ?? new FaceAPIHelper(); }
        }

        /// <summary>
        ///     초기화
        /// </summary>
        public async void Init()
        {
            if (_init) return;

            //사람그룹 목록을 조회한 후에
            var list = await faceServiceClient.ListPersonGroupsAsync();
            //myFriends란 아이디를 가지고 있지 않다면, 사람그룹을 생성한다.
            if (list.All(p => p.PersonGroupId != _personGroupId))
            {
                await faceServiceClient.CreatePersonGroupAsync(_personGroupId, "My Friends");
            }

            _init = true;
        }

        /// <summary>
        ///     사진에서 얼굴 디텍트
        /// </summary>
        /// <param name="fileStream"></param>
        /// <returns></returns>
        public async Task<Face[]> GetDetectAsync(Stream fileStream)
        {
            return await faceServiceClient.DetectAsync(fileStream);
        }

        /// <summary>
        ///     사람 추가
        /// </summary>
        /// <param name="personName"></param>
        /// <returns></returns>
        public async Task<Guid> CreatePersonAsync(string personName)
        {
            var persons = await faceServiceClient.GetPersonsAsync(_personGroupId);
            if (persons.Any(p => p.Name == personName))
            {
                var per = persons.First(p => p.Name == personName);
                return per.PersonId;
            }
            var result = await faceServiceClient.CreatePersonAsync(_personGroupId, personName);
            return result.PersonId;
        }

        /// <summary>
        ///     사람 삭제
        /// </summary>
        /// <param name="personId"></param>
        /// <returns></returns>
        public async Task DeletePersonAsync(Guid personId)
        {
            await faceServiceClient.DeletePersonAsync(_personGroupId, personId);
        }

        /// <summary>
        ///     등록된 사람들 정보 조회
        /// </summary>
        /// <returns></returns>
        public async Task<Person[]> GetPersonsAsync()
        {
            var persons = await faceServiceClient.GetPersonsAsync(_personGroupId);
            return persons;
        }

        /// <summary>
        ///     사람에 얼굴 등록하기
        /// </summary>
        /// <param name="personId"></param>
        /// <param name="fileStream"></param>
        /// <returns></returns>
        public async Task<AddPersistedFaceResult> AddPersonFaceAsync(Guid personId, Stream fileStream)
        {
            try
            {
                // Detect faces in the image and add to Anna
                var result = await faceServiceClient.AddPersonFaceAsync(
                    _personGroupId, personId, fileStream);
                return result;
            }
            catch (FaceAPIException fae)
            {
                Debug.WriteLine(fae.ErrorMessage);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
            return null;
        }

        /// <summary>
        ///     트레이닝 사람그룹
        /// </summary>
        /// <returns></returns>
        public async Task TrainPersonGroupAsync()
        {
            await faceServiceClient.TrainPersonGroupAsync(_personGroupId);

            while (true)
            {
                var trainingStatus = await faceServiceClient.GetPersonGroupTrainingStatusAsync(_personGroupId);
                if (trainingStatus.Status != Status.Running)
                {
                    break;
                }
                await Task.Delay(1000);
            }
        }

        /// <summary>
        ///     인증하기
        /// </summary>
        /// <param name="faceIds"></param>
        /// <returns></returns>
        public async Task<IdentifyResult[]> IdentifyAsync(Guid[] faceIds)
        {
            try
            {
                var result = await faceServiceClient.IdentifyAsync(_personGroupId, faceIds);
                return result;
            }
            catch (FaceAPIException fae)
            {
                Debug.WriteLine(fae.ErrorMessage);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
            return null;
        }

        /// <summary>
        ///     사람 정보 조회
        /// </summary>
        /// <param name="personId"></param>
        /// <returns></returns>
        public async Task<Person> GetPersonAsync(Guid personId)
        {
            var result = await faceServiceClient.GetPersonAsync(_personGroupId, personId);
            return result;
        }
    }
}



7. PersonModel 추가


Models 폴더를 선택하고 마우스 오른쪽, Add -> Class를 선택 -> PersonModel 입력 후 OK


Face API에 있는 Person이라는 클래스를 상속 받아서 몇개의 프로퍼티를 추가하고, 기능을 보강한 모델을 만들어 사용합니다.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using Windows.UI.Xaml.Media.Imaging;
using Microsoft.ProjectOxford.Face.Contract;

namespace FaceIdentify.Models
{
    public class PersonModel : Person, INotifyPropertyChanged
    {
        /// <summary>
        ///     Guid -> Face Guid, BitmapSource -> 사진
        /// </summary>
        public IDictionary<Guid, BitmapSource> Faces = new Dictionary<Guid, BitmapSource>();

        /// <summary>
        ///     바인딩을 하기 위한 프로퍼티
        /// </summary>
        public IList<BitmapSource> FaceImages
        {
            get { return Faces.Select(p => p.Value).ToList(); }
        }

        /// <summary>
        ///     얼굴 추가
        /// </summary>
        /// <param name="faceId"></param>
        /// <param name="bs"></param>
        public void AddFace(Guid faceId, BitmapSource bs)
        {
            Faces.Add(faceId, bs);
            //얼굴 추가 메소드를 이용해서 프로퍼티 체인지 이벤트를 강제로 발생시킴
            OnPropertyChanged("FaceImages");
        }

        /// <summary>
        ///     얼굴 삭제
        /// </summary>
        /// <param name="faceId"></param>
        public void RemoveFace(Guid faceId)
        {
            if (!Faces.ContainsKey(faceId)) return;
            Faces.Remove(faceId);
            OnPropertyChanged("FaceImages");
        }

        #region INotifyPropertyChanged, Person 모델을 상속 받았기 때문에 인터페이스를 직접 구현

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion
    }
}



8. GroupPageViewModel.cs 추가하기


GroupPage.xaml을 추가했던 비슷한 방법으로, ViewModels 폴더를 선택하고 마우스 오른쪽, Add -> Class를 선택 -> GroupPageViewModel 입력 후 OK



using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Input;
using Windows.ApplicationModel;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.UI.Popups;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;
using FaceIdentify.Models;
using FaceIdentify.Services;
using FaceIdentify.Views;
using Microsoft.ProjectOxford.Face.Contract;
using Template10.Mvvm;

namespace FaceIdentify.ViewModels
{
    public class GroupPageViewModel : ViewModelBase
    {
        private PersonModel _currentPerson;
        private string _personName;
        private IList<Person> _persons;

        public GroupPageViewModel()
        {
            if (DesignMode.DesignModeEnabled)
            {
                //디자인 타임 데이터
            }
            else
            {
                //런타임 데이터
                Init();
            }
        }

        /// <summary>
        ///     현재 작업 중인(추가한) 사용자
        /// </summary>
        public PersonModel CurrentPerson
        {
            get { return _currentPerson; }
            set { Set(ref _currentPerson, value); }
        }

        /// <summary>
        ///     등록된 사용자 목록
        /// </summary>
        public IList<Person> Persons
        {
            get { return _persons; }
            set { Set(ref _persons, value); }
        }

        /// <summary>
        ///     콤보박스 사용자가 선택되는 경우 실행되는 커맨드
        /// </summary>
        public ICommand SelectionChangedPersonCommand { get; set; }

        /// <summary>
        ///     트레이닝 커맨드
        /// </summary>
        public ICommand TrainCommand { get; set; }

        /// <summary>
        ///     사람 제거 커맨드
        /// </summary>
        public ICommand RemovePersonCommand { get; set; }

        /// <summary>
        ///     사람 추가
        /// </summary>
        public ICommand AddPersonCommand { get; set; }

        /// <summary>
        ///     사진 추가
        /// </summary>
        public ICommand AddPictureCommand { get; set; }

        /// <summary>
        ///     사람 이름
        /// </summary>
        public string PersonName
        {
            get { return _personName; }
            set { Set(ref _personName, value); }
        }

        /// <summary>
        ///     초기화
        /// </summary>
        private void Init()
        {
            //커맨드 생성
            AddPersonCommand = new DelegateCommand(AddPersonCommandHandler,
                () => !string.IsNullOrEmpty(PersonName));

            AddPictureCommand = new DelegateCommand(AddPictureCommandHandler);

            TrainCommand = new DelegateCommand(async () =>
            {
                //화면에 Busy 표시
                Busy.SetBusy(true, "Training....");
                await FaceAPIHelper.Instance.TrainPersonGroupAsync();
                Busy.SetBusy(false);
            });

            SelectionChangedPersonCommand = new DelegateCommand<object>(obj =>
            {
                //obj로 넘어온 데이터를 이용해서 현재 사람 표시
                var person = obj as Person;
                if (person == null) return;
                CurrentPerson = new PersonModel
                {
                    Name = person.Name,
                    PersonId = person.PersonId,
                    PersistedFaceIds = person.PersistedFaceIds
                };
            });

            RemovePersonCommand = new DelegateCommand(async () =>
            {
                if (CurrentPerson == null) return;

                Busy.SetBusy(true, "Removing...");
                await FaceAPIHelper.Instance.DeletePersonAsync(CurrentPerson.PersonId);

                Persons = await FaceAPIHelper.Instance.GetPersonsAsync();
                CurrentPerson = null;
                Busy.SetBusy(false);
            });

            PropertyChanged += (s, e) =>
            {
                switch (e.PropertyName)
                {
                    case "PersonName":
                        //커맨드 사용 가능 여부 확인
                        ((DelegateCommand) AddPersonCommand).RaiseCanExecuteChanged();
                        break;
                }
            };
        }

        private async void AddPictureCommandHandler()
        {
            //FileOpePicker 설정
            var openDlg = new FileOpenPicker();
            openDlg.FileTypeFilter.Add(".jpg");
            openDlg.FileTypeFilter.Add(".jpeg");
            openDlg.FileTypeFilter.Add(".png");
            //파일 선택 창 출력
            var file = await openDlg.PickSingleFileAsync();
            var basic = await file.GetBasicPropertiesAsync();
            //서버에 올릴 수 있는 이미지의 크기는 4MB까지입니다.
            if (basic.Size > 4*1024*1024)
            {
                var msg = new MessageDialog("It is too big image. It can only be up to 4MB.");
                await msg.ShowAsync();
                return;
            }
            //처음 화면에 출력용
            var bm = new BitmapImage();
            using (var stream = await file.OpenAsync(FileAccessMode.Read))
            {
                await bm.SetSourceAsync(stream);
            }
            //얼굴인식하고 테두리 표시하기
            using (var stream = await file.OpenStreamForReadAsync())
            {
                //얼굴 인식에 실패하는 경우 null을 반환합니다.
                var faceResult = await FaceAPIHelper.Instance.AddPersonFaceAsync(CurrentPerson.PersonId, stream);
                if (faceResult == null) return;

                CurrentPerson.AddFace(faceResult.PersistedFaceId, bm);
            }
        }


        /// <summary>
        ///     사람 추가하기
        /// </summary>
        private async void AddPersonCommandHandler()
        {
            if (string.IsNullOrEmpty(PersonName)) return;
            var personId = await FaceAPIHelper.Instance.CreatePersonAsync(PersonName);

            CurrentPerson = new PersonModel
            {
                Name = PersonName,
                PersonId = personId
            };
        }

        /// <summary>
        ///     Template 10의 기능으로 페이지 네비게이션을 통해서 페이지로 진입하면 실행되는 부분입니다.
        /// </summary>
        /// <param name="parameter"></param>
        /// <param name="mode"></param>
        /// <param name="state"></param>
        /// <returns></returns>
        public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode,
            IDictionary<string, object> state)
        {
            Persons = await FaceAPIHelper.Instance.GetPersonsAsync();
        }
    }
}



9. View와 ViewModel 연결


빌드를 한 후 GroupPage.xaml 디자인 화면으로 이동합니다.


네임스페이스를 추가하고 그 아래 Page.DataContrext에 뷰모델을 넣습니다.


참고로 이 방법은 디자인 타임과 런타임에 동일한 방식으로 뷰모델이 생성되는 방식입니다. 뷰모델을 따로 관리하지 않고, 페이지 네비게이션이 발생할 때마다 뷰모델도 생성되었다가 없어지는 구조입니다. 이런 경우 뷰모델 내부에서 저장해야할 데이터는 SessionState를 이용해서 관리합니다.


    xmlns:ViewModels="using:FaceIdentify.ViewModels"

    x:Class="FaceIdentify.Views.GroupPage"
    mc:Ignorable="d">


    <Page.DataContext>
        <ViewModels:GroupPageViewModel/>
    </Page.DataContext>



10. 실행


사람 이름을 입력하면 상단에 사람 추가 버튼이 활성화 됩니다.



정상적으로 추가가 되면 Current Person에 사람 이름이 표시 됩니다.



빨간 색의 사진 추가 버튼을 눌러서 사진을 추가합니다. 이때 사진은 단독 사진으로 하여야 합니다. (여러 사람 얼굴이 나오는 사진도 가능하기는 하지만, 현재 기능 구현이 되어있지 않습니다.)




그래서 저는 총4장을 추가했습니다. 아래는..추가한 사진들 입니다. 흐흐흐





마지막으로 트레이닝 버튼을 눌러서 학습을 시킵니다.



트레이닝은 금방 종료됩니다. ^^;;


그런데, 오늘은 트레이닝까지만... 진행 하겠습니다. 다음에 우주소녀 12명 중에 성소를 찾는 부분을 진행하도록 하겠습니다. -0-;;;; 시간이 늦어서말이죠..


후다닥 =3 =3



11. 소스 (파일이름 틀렸네요;; part2 소스 맞습니다.)


FaceIdentify_part1.zip


블로그 이미지

MVP kaki104

* Microsoft MVP - Windows Development 2014 ~ 2019 5ring * LINE : kaki104 * facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

티스토리 툴바