Posted at

Cognitive ServicesのFace APIを使って、怒っている時にメールが送れないOutlookのアドインを作ってみる。


はじめに

「ついカッとなってやった、今は反省している。(´・ω・`)」

怒っている時にやったことって、往々にして後から後悔しますよね...(´・ω・`)

例えば、怒っている時にメールを送っても、ろくなことがないですよね。

ということで、

怒っている時にメールを送ることができないOutlookのアドインをざっくり作ってみました。


ざっくりした仕組み

Microsoftが提供している Cognitive ServicesFace API に、顔写真の感情認識ができる機能があります。

これを使って、Outookでメールを送信する前に感情を測定し、怒っていたらメールを送らせない。

という仕組みにしてみました。


できたもの

確認画面

メールを送信しようとすると、怒っているかの確認画面を表示するようにしました。

「怒っているか確認する!」 ボタンを押すとPCのフロントカメラで顔写真を撮影し、感情を測定します。


試してみる

※顔にはモザイク的なものをかけてあります。

1.png

無表情な感じだと「中立(Neutral)」のスコアがほぼ100%ですね。

2.png

ちょっと睨めつけてみたのですが、あんまりスコアが変わらず(´・ω・`)

3.png

全力の笑顔にしてみたら「幸福(Happiest)」が100%になった!(∩´∀`)∩

...

何回も試したのですが、怒りのスコアが上がらない(´;ω;`)ブワッ

もともとデフォルトで顔が笑っているとか、にやけているって言われるのですが、

MicrosoftのAIさん的にもそうらしいです(´・ω・`)

演技力の問題な気もしますが…(´・ω・`)


作ってみる

ということで、ざっくり作ってみます。

ソースは抜粋したものを載せているので、

実際に作ったものの一式は GitHub に置いておきました。


GitHub

GitHubはこちら


Microsoft Azure(Cognitive Services)への登録とFace APIの準備

割愛しますが「AI + Machine Learning」の「Face API」から新規作成できます。

「F0 Free」を選べば無料で使えます(∩´∀`)∩


VSTOアドインのプロジェクトを作成

VSTOアドインとして作成するので、

Visual Studioから「Outlook 2013 and 2016 VSTO Add-in」の新規プロジェクトを作成します。


表示するウインドウのプロジェクトを作成

VSTO アドインは、デフォルトではWindows Formsですが、

表示するウインドウには、WPFを使いたかったので「WPF User Control Library」のプロジェクトを追加しました。

以降、NamespaceがCameraPreviewになっているものは、こちらのプロジェクトです。


Face APIを使って顔写真の感情を分析できるようにする


Nuget Packageの追加

Microsoft.Azure.CognitiveServices.Vision.Face というNuGetパッケージを使用すると、

簡単にFace APIを使うことができるので、追加します。

※プレリリースを含める にチェックを入れないと一覧に表示されません。


GetEmotion.cs

以下を渡すと、年齢と性別と感情を返してくれるクラスを作ります。

これ単体で使いまわせるので、他に何か作る時も楽な気がします。


  • 顔写真ファイルのパス

  • Face APIのキー

  • エンドポイントのURL

using Microsoft.Azure.CognitiveServices.Vision.Face;

using Microsoft.Azure.CognitiveServices.Vision.Face.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace CameraPreview
{
public class GetEmotion
{
private readonly FaceAttributeType[] _faceAttributes = { FaceAttributeType.Age, FaceAttributeType.Gender, FaceAttributeType.Emotion };

public async Task<List<Attributes>> FromFilePathAsync(string filePath, string subscriptionKey, string endpoint)
{
var faceClient = new FaceClient(new ApiKeyServiceClientCredentials(subscriptionKey)) { Endpoint = endpoint };
try
{
return await DetectLocalAsync(faceClient, filePath);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return null;
}
}

private async Task<List<Attributes>> DetectLocalAsync(IFaceClient faceClient, string imagePath)
{
if (!File.Exists(imagePath))
{
return null;
}

try
{
using (Stream imageStream = File.OpenRead(imagePath))
{
var faceList = await faceClient.Face.DetectWithStreamAsync(imageStream, true, false, _faceAttributes);
return GetFaceAttributes(faceList);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return null;
}
}

private List<Attributes> GetFaceAttributes(IEnumerable<DetectedFace> faceList)
{
var detectedFaces = faceList.ToList();

if (!detectedFaces.Any())
{
return null;
}

return detectedFaces.Select(face => new Attributes
{
Age = face.FaceAttributes.Age,
Gender = face.FaceAttributes.Gender.ToString(),
Anger = face.FaceAttributes.Emotion.Anger,
Contempt = face.FaceAttributes.Emotion.Contempt,
Disgust = face.FaceAttributes.Emotion.Disgust,
Fear = face.FaceAttributes.Emotion.Fear,
Happiness = face.FaceAttributes.Emotion.Happiness,
Neutral = face.FaceAttributes.Emotion.Neutral,
Sadness = face.FaceAttributes.Emotion.Sadness,
Surprise = face.FaceAttributes.Emotion.Surprise
}
).ToList();
}
}
}


カメラで顔写真を撮影して、感情を測定できるようにする


PreviewWindow.xaml

表示する画面です。

<Window x:Class="CameraPreview.PreviewWindow"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CameraPreview"
mc:Ignorable="d"
Title="怒っているか確認してからメールしよう!" Height="700" Width="550" ResizeMode="NoResize" ShowInTaskbar="False" WindowStartupLocation="CenterOwner" >

<StackPanel Background="Black">
<Image x:Name="PreviewImage" Height="370" Width="530" Margin="2,2,2,6"/>
<Button Content="怒っているか確認する!" Margin="5" Click="CheckButton_OnClick" Height="30" HorizontalAlignment="Right" Width="310" IsDefault="true"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" >
<Button x:Name="SendButton" Content="メール送信" Margin="5" Click="SendButton_OnClick" Height="30" Width="100" IsEnabled="false"/>
<Button Content="キャンセル" Margin="5" Click="CancelButton_OnClick" Height="30" Width="200" IsCancel="true"/>
</StackPanel>
<TextBox x:Name="TextBox" Height="200" IsReadOnly="True"/>
</StackPanel>
</Window>


PreviewWindow.xaml.cs

べたっとコードビハインドにまとめて書いちゃいました。

Windows.Media.Captureなど、UWPのAPIを使用すると楽なので、それを使用しています。

フロントカメラで写真を撮影し、テンポラリフォルダに保存して、

その写真から感情を測定して、画面に表示しています。

また、怒り(Anger)のスコアが0.6以下の場合に、送信ボタンを押せるようにしています。

using System;

using System.Globalization;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Media.Imaging;
using Windows.Devices.Enumeration;
using Windows.Media.Capture;
using Windows.Media.MediaProperties;
using Windows.Storage.Streams;

namespace CameraPreview
{
public partial class PreviewWindow : Window
{
public PreviewWindow()
{
InitializeComponent();
}

public async void FaceCapture()
{
var setting = new MediaCaptureInitializationSettings
{
StreamingCaptureMode = StreamingCaptureMode.Video,
PhotoCaptureSource = PhotoCaptureSource.Auto
};

var devices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
setting.VideoDeviceId = devices.FirstOrDefault(item => item.EnclosureLocation != null && item.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front)?.Id ?? throw new InvalidOperationException();

using (var mediaCapture = new MediaCapture())
{
await mediaCapture.InitializeAsync(setting);
mediaCapture.VideoDeviceController.Brightness.TrySetAuto(true);
mediaCapture.VideoDeviceController.Contrast.TrySetAuto(true);

var pngProperties = ImageEncodingProperties.CreatePng();
pngProperties.Width = (uint)PreviewImage.ActualWidth;
pngProperties.Height = (uint)PreviewImage.ActualHeight;

using (var randomAccessStream = new InMemoryRandomAccessStream())
{
await mediaCapture.CapturePhotoToStreamAsync(pngProperties, randomAccessStream);
randomAccessStream.Seek(0);

var bitmapImage = new BitmapImage();
using (var stream = randomAccessStream.AsStream())
{
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = stream;
bitmapImage.EndInit();
}

PreviewImage.Source = bitmapImage;

var filePath = Path.GetTempPath() + "AngryMailTemp.png";
SaveBitmapSourceToFile(bitmapImage, filePath);

GEmotionAsync();
}
}
}

private async void GEmotionAsync()
{
var filePath = Path.GetTempPath() + "AngryMailTemp.png";
var getEmotion = new GetEmotion();
var attributes = await getEmotion.FromFilePathAsync(filePath, "Your Subscription Key", "https://japaneast.api.cognitive.microsoft.com/");

if (attributes == null)
{
TextBox.Text = @"顔写真がイマイチです(´・ω・`)";
File.Delete(filePath);
return;
}

foreach (var attribute in attributes)
{
TextBox.Text = "年齢: " + attribute.Age?.ToString(CultureInfo.InvariantCulture) + Environment.NewLine +
"性別: " + attribute.Gender + Environment.NewLine +
Environment.NewLine +
"怒り: " + attribute.Anger.ToString(CultureInfo.InvariantCulture) + Environment.NewLine +
"軽蔑: " + attribute.Contempt.ToString(CultureInfo.InvariantCulture) + Environment.NewLine +
"嫌悪: " + attribute.Disgust.ToString(CultureInfo.InvariantCulture) + Environment.NewLine +
"恐怖: " + attribute.Fear.ToString(CultureInfo.InvariantCulture) + Environment.NewLine +
"幸福: " + attribute.Happiness.ToString(CultureInfo.InvariantCulture) + Environment.NewLine +
"中立: " + attribute.Neutral.ToString(CultureInfo.InvariantCulture) + Environment.NewLine +
"悲哀: " + attribute.Sadness.ToString(CultureInfo.InvariantCulture) + Environment.NewLine +
"驚き: " + attribute.Surprise.ToString(CultureInfo.InvariantCulture);

if (attribute.Anger <= 0.6)
{
SendButton.IsEnabled = true;
}
}

File.Delete(filePath);
}

private static void SaveBitmapSourceToFile(BitmapSource bitmapSource, string filePath)
{
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
BitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
encoder.Save(fileStream);
}
}

private void CheckButton_OnClick(object sender, RoutedEventArgs e)
{
FaceCapture();
}

private void CancelButton_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
}

private void SendButton_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = true;
}
}
}


VSTOアドインを作る

アドイン側はシンプルです。メールの送信時に↑で作った画面を表示するようにしています。


ThisAddIn.cs

using CameraPreview;

using System.Windows.Interop;
using Outlook = Microsoft.Office.Interop.Outlook;

namespace AngryMail
{
public partial class ThisAddIn
{
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
Application.ItemSend += Application_ItemSend;
}

private void Application_ItemSend(object item, ref bool cancel)
{
//MailItemにキャストできないものは会議招待などメールではないものなので、何もしない。
if (!(item is Outlook._MailItem)) return;

var previewWindow = new PreviewWindow();
var activeWindow = Globals.ThisAddIn.Application.ActiveWindow();
var outlookHandle = new NativeMethods(activeWindow).Handle;
var windowInteropHelper = new WindowInteropHelper(previewWindow) { Owner = outlookHandle };
var dialogResult = previewWindow.ShowDialog();

if (dialogResult == true)
{
//Send Mail.
}
else
{
cancel = true;
}
}

private void InternalStartup() => Startup += ThisAddIn_Startup;
}
}


おわりに

怒った顔の演技は難しいですね...(´・ω・`)

Cognitive Servicesならお手軽にAIを使えるので、

ちょっとした思い付きでも、楽しいものや便利なものが作れそうですね。