[UWP] 写真の中にある顔を認識してマスキングするアプリを作る

2019.12.19

はじめに

UWP(ユニバーサルWindowsプラットフォーム)アプリでは簡単に画像やビデオでの顔の検出できるようなので、写真の中にある顔を認識してマスキングするアプリを作ってみました。やってみたら驚くほど簡単にできたので興味のある方はやってみてください。

環境

  • Visual Studio 2019 Enterprise
  • C#

参考

以下のMicrosoftのドキュメントが参考になりました。動画でもできるようですね。面白そうです。

画像やビデオでの顔の検出

上のMicrosoftのドキュメント内にGitHubへのリンクがありました。 GitHubのソースコードも参考にしています。こちらのサンプルでは顔の部分に枠を表示していますが、今回は枠の代わりに画像を表示するように変えています。

Windows-universal-samples/Samples/BasicFaceDetection/cs at master · microsoft/Windows-universal-samples

アプリの説明

「1.マスキング画像の選択」ボタンを押して顔の画像にかぶせる画像を選択します。 次に「2.マスキングしたい画像ファイルの選択」ボタンを押してマスキングしたい画像を選択すると下にマスキングした状態で表示されます。 最後に「3.マスキングした画像を保存する」を押すとマスキングした画像を保存できます。

ソースコード

MainPage.xaml(XAML)

<Page
    x:Class="FaceMasking.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:FaceMasking"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="100"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0" >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="300"/>
                <ColumnDefinition Width="100"/>
                <ColumnDefinition Width="270"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <Button Grid.Column="0" Content="1.マスキング画像の選択" Click="MaskFile_Click" Margin="30,10"/>
            <Image  Grid.Column="1" Name="Preview" Width="80" Height="80"/>
            <Button Grid.Column="2" Content="2.マスキングしたい画像ファイルの選択" Click="OpenFile_Click" Margin="20"/>
            <Button Grid.Column="3" Content="3.マスキングした画像を保存する" Click="SaveFile_Click" Margin="30" Width="200"/>
        </Grid>

        <Canvas Name="PhotoCanvas" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0,10,0,0" SizeChanged="PhotoCanvas_SizeChanged"/>
    </Grid>
</Page>

MainPage.xaml.cs(C#)

using System;
using System.Collections.Generic;

using Windows.Graphics.Imaging;
using Windows.Media.FaceAnalysis;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.Graphics.Display;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;
using Windows.UI.Xaml.Shapes;

using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;

namespace Test
{

    public sealed partial class MainPage : Page
    {
        private readonly uint sourceImageHeightLimit = 1280;
        private BitmapImage maskImage;

        public MainPage()
        {
            this.InitializeComponent();
        }

        private async void MaskFile_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                FileOpenPicker photoPicker = new FileOpenPicker();
                photoPicker.ViewMode = PickerViewMode.Thumbnail;
                photoPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
                photoPicker.FileTypeFilter.Add(".jpg");
                photoPicker.FileTypeFilter.Add(".jpeg");
                photoPicker.FileTypeFilter.Add(".png");
                photoPicker.FileTypeFilter.Add(".bmp");

                StorageFile maskFile = await photoPicker.PickSingleFileAsync();
                if (maskFile == null)
                {
                    return;
                }

                maskImage = new BitmapImage();
                using (var s = await maskFile.OpenReadAsync())
                {
                    maskImage.SetSource(s);
                }

                Preview.Source = maskImage;
            }
            catch (Exception ex)
            {
                this.ClearVisualization();
                Debug.WriteLine(ex.ToString());
            }
        }

        private void SetupVisualization(WriteableBitmap displaySource, IList<DetectedFace> foundFaces)
        {
            ImageBrush brush = new ImageBrush();
            brush.ImageSource = displaySource;
            brush.Stretch = Stretch.Fill;
            this.PhotoCanvas.Background = brush;

            if (foundFaces != null)
            {
                double widthScale = displaySource.PixelWidth / this.PhotoCanvas.ActualWidth;
                double heightScale = displaySource.PixelHeight / this.PhotoCanvas.ActualHeight;

                foreach (DetectedFace face in foundFaces)
                {
                    Image image = new Image();
                    image.Source = maskImage;
                    image.Tag = face.FaceBox;
                    image.Width = (uint)(face.FaceBox.Width / widthScale);
                    image.Height = (uint)(face.FaceBox.Height / heightScale);
                    image.Margin = new Thickness((uint)(face.FaceBox.X / widthScale), (uint)(face.FaceBox.Y / heightScale), 0, 0);
                    this.PhotoCanvas.Children.Add(image);
                }
            }

            string message;
            if (foundFaces == null || foundFaces.Count == 0)
            {
                message = "Didn't find any human faces in the image";
            }
            else if (foundFaces.Count == 1)
            {
                message = "Found a human face in the image";
            }
            else
            {
                message = "Found " + foundFaces.Count + " human faces in the image";
            }

            Debug.WriteLine(message);
        }

        private void ClearVisualization()
        {
            this.PhotoCanvas.Background = null;
            this.PhotoCanvas.Children.Clear();
            Debug.WriteLine(string.Empty);
        }

        private BitmapTransform ComputeScalingTransformForSourceImage(BitmapDecoder sourceDecoder)
        {
            BitmapTransform transform = new BitmapTransform();

            if (sourceDecoder.PixelHeight > this.sourceImageHeightLimit)
            {
                float scalingFactor = (float)this.sourceImageHeightLimit / (float)sourceDecoder.PixelHeight;

                transform.ScaledWidth = (uint)Math.Floor(sourceDecoder.PixelWidth * scalingFactor);
                transform.ScaledHeight = (uint)Math.Floor(sourceDecoder.PixelHeight * scalingFactor);
            }

            return transform;
        }

        private async void OpenFile_Click(object sender, RoutedEventArgs e)
        {
            IList<DetectedFace> faces = null;
            SoftwareBitmap detectorInput = null;
            WriteableBitmap displaySource = null;

            try
            {
                FileOpenPicker photoPicker = new FileOpenPicker();
                photoPicker.ViewMode = PickerViewMode.Thumbnail;
                photoPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
                photoPicker.FileTypeFilter.Add(".jpg");
                photoPicker.FileTypeFilter.Add(".jpeg");
                photoPicker.FileTypeFilter.Add(".png");
                photoPicker.FileTypeFilter.Add(".bmp");

                StorageFile photoFile = await photoPicker.PickSingleFileAsync();
                if (photoFile == null)
                {
                    return;
                }

                this.ClearVisualization();

                Debug.WriteLine("Opening...");
                using (IRandomAccessStream fileStream = await photoFile.OpenAsync(Windows.Storage.FileAccessMode.Read))
                {
                    BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fileStream);
                    BitmapTransform transform = this.ComputeScalingTransformForSourceImage(decoder);

                    using (SoftwareBitmap originalBitmap = await decoder.GetSoftwareBitmapAsync(decoder.BitmapPixelFormat, BitmapAlphaMode.Ignore, transform, ExifOrientationMode.IgnoreExifOrientation, ColorManagementMode.DoNotColorManage))
                    {
                        const BitmapPixelFormat InputPixelFormat = BitmapPixelFormat.Gray8;
                        if (FaceDetector.IsBitmapPixelFormatSupported(InputPixelFormat))
                        {
                            using (detectorInput = SoftwareBitmap.Convert(originalBitmap, InputPixelFormat))
                            {
                                // Create a WritableBitmap for our visualization display; copy the original bitmap pixels to wb's buffer.
                                displaySource = new WriteableBitmap(originalBitmap.PixelWidth, originalBitmap.PixelHeight);
                                originalBitmap.CopyToBuffer(displaySource.PixelBuffer);

                                Debug.WriteLine("Detecting...");

                                FaceDetector detector = await FaceDetector.CreateAsync();
                                faces = await detector.DetectFacesAsync(detectorInput);

                                // Create our display using the available image and face results.
                                this.SetupVisualization(displaySource, faces);
                            }
                        }
                        else
                        {
                            Debug.WriteLine("PixelFormat '" + InputPixelFormat.ToString() + "' is not supported by FaceDetector");
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                this.ClearVisualization();
                Debug.WriteLine(ex.ToString());
            }
        }

        private void PhotoCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            try
            {
                if (this.PhotoCanvas.Background != null)
                {
                    WriteableBitmap displaySource = (this.PhotoCanvas.Background as ImageBrush).ImageSource as WriteableBitmap;

                    double widthScale = displaySource.PixelWidth / this.PhotoCanvas.ActualWidth;
                    double heightScale = displaySource.PixelHeight / this.PhotoCanvas.ActualHeight;

                    foreach (var item in PhotoCanvas.Children)
                    {
                        Rectangle box = item as Rectangle;
                        if (box == null)
                        {
                            continue;
                        }

                        // We saved the original size of the face box in the rectangles Tag field.
                        BitmapBounds faceBounds = (BitmapBounds)box.Tag;
                        box.Width = (uint)(faceBounds.Width / widthScale);
                        box.Height = (uint)(faceBounds.Height / heightScale);

                        box.Margin = new Thickness((uint)(faceBounds.X / widthScale), (uint)(faceBounds.Y / heightScale), 0, 0);
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
        }

        private async void SaveFile_Click(object sender, RoutedEventArgs e)
        {
            var bitmap = new RenderTargetBitmap();
            await bitmap.RenderAsync(PhotoCanvas);

            var pixelBuffer = await bitmap.GetPixelsAsync();
            var pixels = pixelBuffer.ToArray();
            var displayInformation = DisplayInformation.GetForCurrentView();

            var savePicker = new Windows.Storage.Pickers.FileSavePicker();
            savePicker.SuggestedStartLocation =
                Windows.Storage.Pickers.PickerLocationId.DocumentsLibrary;
            savePicker.FileTypeChoices.Add("JPEG", new List<string>() { ".jpg" });
            savePicker.SuggestedFileName = "save_file.jpg";

            Windows.Storage.StorageFile file = await savePicker.PickSaveFileAsync();
            if (file != null)
            {
                using (var stream = await file.OpenAsync(FileAccessMode.ReadWrite))
                {
                    var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);

                    // 8 bit color reduction 
                    encoder.SetPixelData(
                        BitmapPixelFormat.Bgra8,
                        BitmapAlphaMode.Premultiplied,
                        (uint)bitmap.PixelWidth,
                        (uint)bitmap.PixelHeight,
                        displayInformation.RawDpiX,
                        displayInformation.RawDpiY,
                        pixels);

                    await encoder.FlushAsync();
                }
            }
        }
    }
}

最後に

元々、UWPアプリの申請を試したくてこのアプリを作成しました。ほぼ同じソースコードでパッケージにして申請したところ、プライベートですが問題なく申請が通りました。