Jak pomocí Azure vyrobit aplikaci na rozpoznávání obličeje

10. 6. 2021 Azure, Programming

Chcete si vytvořit aplikaci, která bude vyhledávat obličeje na fotografii, rozpoznávat emoce, barvu vlasů, věk, pohlaví a zda daná osoba nosí brýle? Tento návod vás provede celým postupem tvorby algoritmu na rozpoznávání obličeje pomocí Azure.

Ukázka aplikace na rozpoznávání obličeje
Ukázka užívání aplikace

Požadavky

Přidání služby Face

V následujících několika krocích si ukážeme, jak si přidat službu Face i na váš účet.

Přihlásíte se na stránce portal.azure.com a kliknete na tlačítko Create a resource.

Vytvoření nové recourse group na Azure portálu
Vyhledávání prostředku v Azure Portal | zdroj: portal.azure.com

Vyhledáme si službu Face.

Vyhledání služby Face na Azure portálu
Tvorba prostředku v Azure Portal | zdroj: portal.azure.com
Tvorba prostředku Face v Azure portálu
Tvorba prostředku v Azure Portal | zdroj: portal.azure.com

Doplníme potřebné údaje.

Vyplnění potřebných údajů k vytvoření služby Face
Doplnění informací | zdroj: portal.azure.com

Instalace Visual Studia

Pokud ještě nemáte Visual Studio nainstalované, navštivte náš článek, ve kterém je celý postup pospaný.

Programovací část

Připravení složek a tříd

Spustíme si Visual Studio a založíme si složku Models, do které přidáme tři třídy, ve kterých budeme později pracovat.

Struktura:

Struktura složek a tříd ve Visual Studio
Grafické znázornění struktury, kterou budeme vytvářet

A jak si vlastně vytvořit složku a přidat do ní třídy?

Postuú přidání nové složky do Visual Studio
Tvorba prostředí | zdroj: Visual Studio
Přidání předmětů do složky Models
Tvorba prostředí | zdroj: Visual Studio

NuGet

NuGet je balíček, který obsahuje opakovaný, nebo hodně používaný kód. Má za úkol zjednodušit programovací proces. Pokud chcete zjistit více, klikněte na následující odkaz dokumentace.

Spravování balíčku NuGet ve Visual Studio
Importování NuGet | zdroj: Visual Studio
Instalace balíčku kognitivních služeb do Visual Studio
Instalace balíčku NuGet | zdroj: Visual Studio

Připojení API k naší aplikaci

Přejdeme do classy FaceDetectionHelper, kde si vytvoříme proměnné _endpoint_secret typu string, který je určen pro textové řetězce. Do těchto proměnných uložíme hodnoty z naší založené služby.

Zkopírování endpointu a klíče služby Face
Zkopírování endpointu a klíče | zdroj: portal.azure.com

private string _endpoint = ""; <- vložte zde endpoint

private string _secret = ""; <- vložte zde klíč k naší službě

Přidání ovládacích komponentů

Popis kódu: Rozdělíme si okno (grid) na dvě části, konkrétně pomocí ColumnDefinition.

Přidáme si Button, neboli tlačítko, přes které budeme do našeho program nahrávat obrázek nebo fotku na rozpoznání obličeje. Dále Image, do kterého budeme vykreslovat zvolený obrázek. Jako poslední ListBox, do kterého budeme vykreslovat rozpoznané obličeje + informace.

 <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="7*"/>
            <ColumnDefinition Width="3*"/>
        </Grid.ColumnDefinitions>
        <Image Stretch="Uniform" x:Name="FaceImage" HorizontalAlignment="Left" Grid.Column="0" Grid.Row="0"/>
        <Button x:Name="BtnUpload" VerticalAlignment="Bottom" Content="Select image file" Margin="20,0,20,5" Height="20" Click="BtnUpload_Click" />
        <ListBox x:Name="FaceDetails" Grid.Column="1">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
                        <DockPanel HorizontalAlignment="Stretch" >
                            <Image Margin="3" Source="{Binding Image}" Width="100" Height="100"/>
                            <StackPanel Orientation="Vertical">
                                <DockPanel HorizontalAlignment="Stretch" >
                                    <Label Content="Age: "/>
                                    <TextBlock Margin="3" Text="{Binding Attributes.Age}"/>
                                </DockPanel>
                                <DockPanel HorizontalAlignment="Stretch" >
                                    <Label Content="Gender: "/>
                                    <TextBlock Margin="3" Text="{Binding Attributes.Gender}"/>
                                </DockPanel>
                                <DockPanel HorizontalAlignment="Stretch" >
                                    <Label Content="Glasses: "/>
                                    <TextBlock Margin="3" Text="{Binding Attributes.Glasses}"/>
                                </DockPanel>
                                <DockPanel HorizontalAlignment="Stretch">
                                    <Label Content="Dominant hair colour: "/>
                                    <TextBlock Margin="3" Text="{Binding MainHairColor}"/>
                                </DockPanel>
                                <DockPanel HorizontalAlignment="Stretch" >
                                    <Label Content="Emotion: "/>
                                    <StackPanel Orientation="Vertical">
                                        <DockPanel HorizontalAlignment="Stretch" >
                                            <Label Content="Anger: "/>
                                            <TextBlock Margin="3" Text="{Binding Attributes.Emotion.Anger}"/>
                                        </DockPanel>
                                        <DockPanel HorizontalAlignment="Stretch" >
                                            <Label Content="Contempt: "/>
                                            <TextBlock Margin="3" Text="{Binding Attributes.Emotion.Contempt}"/>
                                        </DockPanel>
                                        <DockPanel HorizontalAlignment="Stretch" >
                                            <Label Content="Disgust: "/>
                                            <TextBlock Margin="3" Text="{Binding Attributes.Emotion.Disgust}"/>
                                        </DockPanel>
                                        <DockPanel HorizontalAlignment="Stretch" >
                                            <Label Content="Fear: "/>
                                            <TextBlock Margin="3" Text="{Binding Attributes.Emotion.Fear}"/>
                                        </DockPanel>
                                        <DockPanel HorizontalAlignment="Stretch" >
                                            <Label Content="Happiness: "/>
                                            <TextBlock Margin="3" Text="{Binding Attributes.Emotion.Happiness}"/>
                                        </DockPanel>
                                        <DockPanel HorizontalAlignment="Stretch" >
                                            <Label Content="Neutral: "/>
                                            <TextBlock Margin="3" Text="{Binding Attributes.Emotion.Neutral}"/>
                                        </DockPanel>
                                        <DockPanel HorizontalAlignment="Stretch" >
                                            <Label Content="Sadness: "/>
                                            <TextBlock Margin="3" Text="{Binding Attributes.Emotion.Sadness}"/>
                                        </DockPanel>
                                        <DockPanel HorizontalAlignment="Stretch">
                                            <Label Content="Surprise: "/>
                                            <TextBlock Margin="3" Text="{Binding Attributes.Emotion.Surprise}"/>
                                        </DockPanel>
                                    </StackPanel>
                                </DockPanel>
                            </StackPanel>
                        </DockPanel>
                        <Separator />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>

MainWindow.xaml.cs

Vytvoříme si metodu na nahrání obrázku, která se vyvolá v moment kliknutí na tlačítko. V MainWindow.xaml.cs si klikneme 2x na tlačítko, nebo přidáme Event handler – CLICK.

Přidání eventu ve Visual Studio
Přidání eventu | zdroj: Visual Studio

Dále si do ní vložíme jednoduchý OpenFileDialog, který nám otevře klasické okno průzkumníka pro zvolení našeho obrázku.

Ukázka Open File Dialogu | zdroj: Visual Studio

Popis kódu: Až se obrázek nahraje, je na čase rozpoznat obličeje. S tím nám pomůže metoda DetectFace v třídě FaceDetectionHelper. Poté všechny obličeje projedeme foreachem, který nám vykreslí na hlavním obrázku bílé kruhy okolo obličejů. Dále si všechny obličeje ořízne a vloží je do listboxu, kde má každý obličej vlastní řádek s oříznutým obličejem a vlastnostmi.

facesDetails.Add(new Models.FaceDetails()
                    {
                        Image = croppedFace,
                        Attributes = faceDetails
                    });
        private async void BtnUpload_Click(object sender, RoutedEventArgs e)
        {

            var openFileDialog = new OpenFileDialog()
            {
                Filter = "Image files (*.png;*.jpeg)|*.png;*.jpeg|All files (*.*)|*.*"
            };

            if (openFileDialog.ShowDialog() == true)
            {
                var filePath = openFileDialog.FileName;
                var imageBitmap = new BitmapImage(new Uri(filePath));
                FaceImage.Source = imageBitmap;

                var drwVisual = new DrawingVisual();
                var drwContext = drwVisual.RenderOpen();
                drwContext.DrawImage(imageBitmap, new Rect(0, 0, imageBitmap.Width, imageBitmap.Height));

                var dpi = imageBitmap.DpiX;
                var resizeFactor = 96 / dpi;

                var faceHelper = new FaceDetectionHelper(filePath, resizeFactor);

                var faces = await faceHelper.DetectFaces();

                var facesDetails = new List<FaceDetails>();

                foreach (var face in faces)
                {
                    var rectangleDetails = face.FaceRectangle;

                    faceHelper.DrawFaceCircle(rectangleDetails, drwContext);

                    var croppedFace = faceHelper.GetCroppedImage(rectangleDetails);
                    var faceDetails = face.FaceAttributes;

                    facesDetails.Add(new Models.FaceDetails()
                    {
                        Image = croppedFace,
                        Attributes = faceDetails
                    });
                }
                FaceDetails.ItemsSource = facesDetails;
                drwContext.Close();
                var renderToImageCtrl = new RenderTargetBitmap((int)(imageBitmap.PixelWidth * resizeFactor), (int)(imageBitmap.PixelHeight * resizeFactor), 96, 96, PixelFormats.Pbgra32);
                renderToImageCtrl.Render(drwVisual);
                FaceImage.Source = renderToImageCtrl;
            }
        }

FaceDetectionHelper.cs

Popis kódu: Zde máme metodu, která nám určuje, jaká data chceme získat z API, konkrétně jaké vlastnosti obličeje chceme vypsat. Dále si tu ještě pomáháme se změnou velikosti jednotlivých obrázků a pozicování kruhů kolem tváří.

private readonly string _imageSrc;
        private readonly double _resizeFactor;

        public FaceDetectionHelper(string imageFilePath, double resizeFactor)
        {
            _imageSrc = imageFilePath;
            _resizeFactor = resizeFactor;
        }


        private IFaceClient GetClient()
        {
            return new FaceClient(new ApiKeyServiceClientCredentials(this._secret)) { Endpoint = this._endpoint, };
        }

        public async Task<IEnumerable<DetectedFace>> DetectFaces()
        {
            var client = GetClient();

            var requiredFaceAttributes = new FaceAttributeType[] {
                     FaceAttributeType.Age,
                     FaceAttributeType.Gender,
                     FaceAttributeType.Smile,
                     FaceAttributeType.FacialHair,
                     FaceAttributeType.Hair,
                     FaceAttributeType.HeadPose,
                     FaceAttributeType.Glasses,
                     FaceAttributeType.Emotion,
                };

            try
            {
                using (var imgStream = File.OpenRead(_imageSrc))
                {
                    var faces = await client.Face.DetectWithStreamAsync(imgStream, returnFaceAttributes: requiredFaceAttributes);
                    return faces;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw e;
            }

        }

        public Rect GetFaceRectangle(FaceRectangle faceRectangle)
        {
            var rectangle = new Rect(faceRectangle.Left * _resizeFactor, faceRectangle.Top * _resizeFactor, faceRectangle.Width * _resizeFactor, faceRectangle.Height * _resizeFactor);
            return rectangle;
        }

        public Point GetCenter(double x, double y, double radius)
        {
            var center = new Point(x + radius, y + radius);
            return center;
        }

        public void DrawFaceCircle(FaceRectangle faceRectangle, DrawingContext drawingContext)
        {
            var diameter = faceRectangle.Width;
            var radius = diameter / 2;
            var xPosition = faceRectangle.Left * _resizeFactor;
            var yPosition = faceRectangle.Top * _resizeFactor;
            var center = GetCenter(xPosition, yPosition, radius);

            drawingContext.DrawEllipse(Brushes.Transparent,
                new Pen(Brushes.White, 7),
                center,
                diameter * _resizeFactor,
                diameter * _resizeFactor);

        }

        public BitmapImage GetCroppedImage(FaceRectangle faceRectangle)
        {
            var xCenter = (int)(faceRectangle.Left * _resizeFactor);
            var yCenter = (int)(faceRectangle.Top * _resizeFactor);
            var center = new System.Drawing.Point(xCenter, yCenter);

            var diameter = (int)(faceRectangle.Width * _resizeFactor);
            var size = new System.Drawing.Size(diameter, diameter);
            var rectangleToCrop = new System.Drawing.Rectangle(center, size );
            var image = System.Drawing.Image.FromFile(_imageSrc);

            var origBitmap = new System.Drawing.Bitmap(_imageSrc);
            var croppedImage = ImageHelper.CropAtRect(origBitmap, rectangleToCrop);
            var imageSource = ImageHelper.BitmapToImageSource(croppedImage);
            return imageSource;
        }

FaceDetails.cs

Popis kódu: Tuto třídu máme vytvořenou pouze pro získávání určitých dat z API. Pro to využíváme tzv. getter, setter, neboli vlastnosti.

        public FaceAttributes Attributes { get; set; }
        public BitmapImage Image { get; set; }
        public string MainHairColor => this.Attributes.Hair.HairColor.FirstOrDefault().Color.ToString();

ImageHelper.cs

Popis kódu: Zde máme několik metod na ořezávání, načítání a pozicování obrázků.

public static Image CropImage(Image img, Rectangle cropArea)
        {
            Bitmap bmpImage = new Bitmap(img);
            return bmpImage.Clone(cropArea, bmpImage.PixelFormat);
        }

        public static Bitmap CropAtRect(this Bitmap b, Rectangle r)
        {
            Bitmap nb = new Bitmap(r.Width, r.Height);
            Graphics g = Graphics.FromImage(nb);
            g.DrawImage(b, -r.X, -r.Y);
            return nb;
        }

        public static BitmapImage BitmapToImageSource(Bitmap bitmap)
        {
            using (MemoryStream memory = new MemoryStream())
            {
                bitmap.Save(memory, System.Drawing.Imaging.ImageFormat.Bmp);
                memory.Position = 0;
                BitmapImage bitmapimage = new BitmapImage();
                bitmapimage.BeginInit();
                bitmapimage.StreamSource = memory;
                bitmapimage.CacheOption = BitmapCacheOption.OnLoad;
                bitmapimage.EndInit();

                return bitmapimage;
            }
        }

Závěr

Doufáme, že se vám bude aplikace líbit. Rozpoznávání obličeje pomocí Azure je jen jednou z možností jak lze umělou inteligenci využívat. Kdyby se vám aplikace líbila, ale netroufli byste si na programování, nezoufejte, protože tento program je dostupný na našem GitHubu.