diff --git a/Documentation/DigitalAssetManagement.jpg b/Documentation/DigitalAssetManagement.jpg new file mode 100644 index 0000000..c27e799 Binary files /dev/null and b/Documentation/DigitalAssetManagement.jpg differ diff --git a/Documentation/DigitalAssetManagement.md b/Documentation/DigitalAssetManagement.md new file mode 100644 index 0000000..b77519d --- /dev/null +++ b/Documentation/DigitalAssetManagement.md @@ -0,0 +1,30 @@ +# Digital Asset Management +This sample illustrates how Computer Vision can add a layer of insights to a collection of images. + +![alt text](https://github.com/Microsoft/Cognitive-Samples-IntelligentKiosk/blob/master/Documentation/DigitalAssetManagement.jpg "Digital Asset Management") + +# Key Source Code + +* [DigitalAssetManagementPage](../Kiosk/Views/DigitalAssetManagement/DigitalAssetManagementPage.xaml.cs): Main page that drives the demo. It displays the images along with its associated filters. It also contains the UI to manage your cached insights extracted from your images. + +* [ImageInsights](../Kiosk/Views/DigitalAssetManagement/ImageInsights.cs): POCO object representing insights extracted from each of your images. + +* [ImageInsightsViewModel](../Kiosk/Views/DigitalAssetManagement/ImageInsightsViewModel.cs): A wrapper around the ImageInsights object to support data binding to the UI and filtering the image collection. + +* [ImageProcessor](../Kiosk/Views/DigitalAssetManagement/ImageProcessor.cs): Uses your Azure Cognitive Service to extract ImageInsights from your images. + +* [FilesViewModel](../Kiosk/Views/DigitalAssetManagement/FilesViewModel.cs): CRUD operations for ImageInsights. Stores them in JSON format within the applications local storage. + +## Running the demo + +The first time the solution is ran you will be prompted to enter your Azure Cognitive Service key. If you don't have one, you can create one [here](https://ms.portal.azure.com/#create/Microsoft.CognitiveServicesAllInOne). Your key is stored in the applications local settings. Your key can be changed in the future using the settings menu. + +Next, select either a local folder containing images, or an Azure blob collection containing images. If you are using a blob collection you will need to supply a [shared access signature URI](https://docs.microsoft.com/en-us/azure/storage/common/storage-sas-overview). This URI will allow temporary access to even private containers. + +Once a folder or storage collection is selected the images will be processed for insights. The image insights are cached in the applications local storage. Only the insights about the images are cached while the images themselves are not. The insights can be re opened, downloaded or deleted using the History menu. + +## How it works + +Each image from a local folder or a blob collection is processed through the Computer Vision API and/or the Face API, depending on which services you elect to use. The results are cached in the applications local storage using JSON file format. The images themselves are never cached. The JSON file contains the output from the API for each image, minus some extracted insights not used by this demo. + +Each time the JSON file is loaded it is used to create a list of filters over the images. These filters, along with the associated images are displayed in the UI. When a filter is selected, images matching that filter are added to the image display. If no filters are selected, all images are displayed. diff --git a/Documentation/ImageCollectionInsights.JPG b/Documentation/ImageCollectionInsights.JPG deleted file mode 100644 index 1417628..0000000 Binary files a/Documentation/ImageCollectionInsights.JPG and /dev/null differ diff --git a/Documentation/ImageCollectionInsights.md b/Documentation/ImageCollectionInsights.md deleted file mode 100644 index 1a900cd..0000000 --- a/Documentation/ImageCollectionInsights.md +++ /dev/null @@ -1,22 +0,0 @@ -# Image Collection Insights - -![alt text](https://github.com/Microsoft/Cognitive-Samples-IntelligentKiosk/blob/master/Documentation/ImageCollectionInsights.JPG "Image Collection Insights") - -This demo showcases an example of using Cognitive Services to add a layer of intelligence on top of a collection of -photos (e.g. filter by face, by emotion, by tag, etc). - -Couple notes: -* Once it processes a folder it saves the results in a json file in that folder so it doesn’t repeat the work again when looking at -that folder. If you want to force it to bypass the json file and re-compute, expand the toolbar menu and you will find a way to do it - -* Processing is limited to the first 50 files. If you want to bypass that and compute all files in a folder, expand the toolbar menu -and you will find a toggle to enable that - -# Key Source Code - -* [ImageCollectionInsights](../Kiosk/Views/ImageCollectionInsights/ImageCollectionInsights.xaml.cs): Main page that drives the demo. It -displays the images on a grid on the right hand side, and adds a set of filters on the left hand side that shows the insights from -the photos and lets you filter the content by faces, emotion or visual features. - -* [ImageProcessor](../Kiosk/Views/ImageCollectionInsights/ImageProcessor.cs): Class that processes each photo and creates the metadata -that is displayed by the UI. diff --git a/Kiosk/Controls/Converters.cs b/Kiosk/Controls/Converters.cs index f5a09ba..18ba348 100644 --- a/Kiosk/Controls/Converters.cs +++ b/Kiosk/Controls/Converters.cs @@ -33,6 +33,7 @@ using ServiceHelpers; using System; +using System.Collections; using Windows.UI.Xaml; using Windows.UI.Xaml.Data; @@ -254,7 +255,25 @@ public class CollectionCountToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - return System.Convert.ToInt32(value) > 0 ? Visibility.Visible : Visibility.Collapsed; + //get min value + int.TryParse(parameter != null ? (string)parameter : string.Empty, out int minValue); + + //get count + var count = 0; + if (value is int) + { + count = (int)value; + } + else + { + var collection = value as ICollection; + if (collection != null) + { + count = collection.Count; + } + } + + return count > minValue ? Visibility.Visible : Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, string language) @@ -267,7 +286,25 @@ public class ReverseCollectionCountToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - return System.Convert.ToInt32(value) > 0 ? Visibility.Collapsed : Visibility.Visible; + //get min value + int.TryParse(parameter != null ? (string)parameter : string.Empty, out int minValue); + + //get count + var count = 0; + if (value is int) + { + count = (int)value; + } + else + { + var collection = value as ICollection; + if (collection != null) + { + count = collection.Count; + } + } + + return count > minValue ? Visibility.Collapsed : Visibility.Visible; } public object ConvertBack(object value, Type targetType, object parameter, string language) @@ -370,4 +407,45 @@ public object ConvertBack(object value, Type targetType, object parameter, strin throw new NotImplementedException(); } } + + public class MathConverter : IValueConverter + { + public double Add { get; set; } + public double Multiply { get; set; } + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value != null) + { + var number = System.Convert.ToDouble(value); + return (number + Add) * Multiply; + } + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value != null) + { + var number = System.Convert.ToDouble(value); + return (number / Multiply) - Add; + } + return value; + } + } + + public class BooleanToIntConverter : IValueConverter + { + public int IfTrue { get; set; } = 1; + public int IfFalse { get; set; } = 0; + public object Convert(object value, Type targetType, object parameter, string language) + { + + return (bool)value ? IfTrue : IfFalse; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + return ((int)value) == IfTrue ? true : false; + } + } } \ No newline at end of file diff --git a/Kiosk/IntelligentKioskSample.csproj b/Kiosk/IntelligentKioskSample.csproj index 94767ad..738785d 100644 --- a/Kiosk/IntelligentKioskSample.csproj +++ b/Kiosk/IntelligentKioskSample.csproj @@ -352,6 +352,16 @@ DemoLauncherPage.xaml + + DigitalAssetManagementPage.xaml + + + + + + + StorageDialog.xaml + FaceApiExplorerPage.xaml @@ -383,15 +393,6 @@ HowOldKioskPage.xaml - - - - - - - - - InitialView.xaml @@ -708,6 +709,14 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -748,10 +757,6 @@ Designer MSBuild:Compile - - MSBuild:Compile - Designer - Designer MSBuild:Compile diff --git a/Kiosk/ServiceHelpers/CoreUtil.cs b/Kiosk/ServiceHelpers/CoreUtil.cs index 19ab902..b9ad67b 100644 --- a/Kiosk/ServiceHelpers/CoreUtil.cs +++ b/Kiosk/ServiceHelpers/CoreUtil.cs @@ -91,5 +91,17 @@ public static Rect ToRect(this BoundingRect rect) { return new Rect(rect.X, rect.Y, rect.W, rect.H); } + + public static Rect Inflate(this Rect rect, double inflatePercentage) + { + var width = rect.Width * inflatePercentage; + var height = rect.Height * inflatePercentage; + return new Rect(rect.X - ((width - rect.Width) / 2), rect.Y - ((height - rect.Height) / 2), width, height); + } + + public static Rect Scale(this Rect rect, double scale) + { + return new Rect(rect.X * scale, rect.Y * scale, rect.Width * scale, rect.Height * scale); + } } } diff --git a/Kiosk/Views/DigitalAssetManagement/DigitalAssetManagementPage.xaml b/Kiosk/Views/DigitalAssetManagement/DigitalAssetManagementPage.xaml new file mode 100644 index 0000000..e25a158 --- /dev/null +++ b/Kiosk/Views/DigitalAssetManagement/DigitalAssetManagementPage.xaml @@ -0,0 +1,654 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kiosk/Views/DigitalAssetManagement/DigitalAssetManagementPage.xaml.cs b/Kiosk/Views/DigitalAssetManagement/DigitalAssetManagementPage.xaml.cs new file mode 100644 index 0000000..1d80571 --- /dev/null +++ b/Kiosk/Views/DigitalAssetManagement/DigitalAssetManagementPage.xaml.cs @@ -0,0 +1,456 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Cognitive Services: http://www.microsoft.com/cognitive +// +// Microsoft Cognitive Services Github: +// https://github.com/Microsoft/Cognitive +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using Microsoft.Azure.CognitiveServices.Vision.CustomVision.Prediction; +using Microsoft.Azure.CognitiveServices.Vision.CustomVision.Training; +using Microsoft.Azure.CognitiveServices.Vision.CustomVision.Training.Models; +using ServiceHelpers; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using Windows.Storage; +using Windows.Storage.AccessCache; +using Windows.Storage.Pickers; +using Windows.UI.Popups; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Navigation; + +namespace IntelligentKioskSample.Views.DigitalAssetManagement +{ + [KioskExperience(Id = "DigitalAssetManagement", + DisplayName = "Digital Asset Management", + Description = "See how Computer Vision can add a layer of insights to image collections", + ImagePath = "ms-appx:/Assets/DemoGallery/Image Collection Insights.jpg", + ExperienceType = ExperienceType.Guided | ExperienceType.Business, + TechnologiesUsed = TechnologyType.Face | TechnologyType.Emotion | TechnologyType.Vision, + TechnologyArea = TechnologyAreaType.Vision, + DateAdded = "2019/10/30")] + public sealed partial class DigitalAssetManagementPage : Page, INotifyPropertyChanged + { + const int _maxImageCountPerProcessingCycle = 50; + + ImageProcessor _imageProcessor = new ImageProcessor(); + DigitalAssetData _currentData; + CustomVisionTrainingClient _customVisionTraining; + CustomVisionPredictionClient _customVisionPrediction; + + public event PropertyChangedEventHandler PropertyChanged; + + public ObservableCollection CustomVisionProjects { get; set; } = new ObservableCollection(); + public FilesViewModel FileManager { get; } = new FilesViewModel(); + public ImageFiltersViewModel ImageFilters { get; } = new ImageFiltersViewModel(); + + public DigitalAssetManagementPage() + { + this.InitializeComponent(); + } + + protected async override void OnNavigatedTo(NavigationEventArgs e) + { + if (string.IsNullOrEmpty(SettingsHelper.Instance.FaceApiKey) || + string.IsNullOrEmpty(SettingsHelper.Instance.VisionApiKey)) + { + await new MessageDialog("Missing Face or Vision API Key. Please enter a key in the Settings page.", "Missing API Key").ShowAsync(); + } + + FaceListManager.FaceListsUserDataFilter = SettingsHelper.Instance.WorkspaceKey + "_DigitalAssetManagement"; + await FaceListManager.Initialize(); + + //load files + await FileManager.LoadFilesAsync(); + + //setup Custom Vision + await InitCustomVision(); + + base.OnNavigatedTo(e); + } + + protected override async void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + await FaceListManager.ResetFaceLists(); + + base.OnNavigatingFrom(e); + } + + async Task InitCustomVision() + { + //setup custom vision clients + if (!string.IsNullOrEmpty(SettingsHelper.Instance.CustomVisionTrainingApiKey) && + !string.IsNullOrEmpty(SettingsHelper.Instance.CustomVisionPredictionApiKey)) + { + _customVisionTraining = new CustomVisionTrainingClient { Endpoint = SettingsHelper.Instance.CustomVisionTrainingApiKeyEndpoint, ApiKey = SettingsHelper.Instance.CustomVisionTrainingApiKey }; + _customVisionPrediction = new CustomVisionPredictionClient { Endpoint = SettingsHelper.Instance.CustomVisionPredictionApiKeyEndpoint, ApiKey = SettingsHelper.Instance.CustomVisionPredictionApiKey }; + } + + //get custom vision projects + CustomVisionProjects.Clear(); + if (_customVisionTraining != null) + { + var projects = await _customVisionTraining.GetProjectsAsync(); + CustomVisionProjects.AddRange(projects.OrderBy(i => i.Name).Select(i => new ProjectViewModel { Project = i })); + } + + //enable UI + CustomVisionApi.IsEnabled = _customVisionTraining != null; + } + + public DigitalAssetData CurrentData + { + get => _currentData; + set + { + _currentData = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentData))); + } + } + + private void StartOverClicked(object sender, RoutedEventArgs e) + { + StartOver(); + } + + void StartOver() + { + //reset back to loading screen + this.landingMessage.Visibility = Visibility.Visible; + this.filterTab.Visibility = Visibility.Collapsed; + this.ActiveFilters.Visibility = Visibility.Collapsed; + this.ImagesContainer.Visibility = Visibility.Collapsed; + ImageFilters.Clear(); + CurrentData = null; + progressRing.IsActive = false; + } + + async void LoadTypeClicked(object sender, ItemClickEventArgs e) + { + var tag = (e.ClickedItem as FrameworkElement).Tag; + switch (tag) + { + case "file": + await LoadFromFile(); + break; + case "storage": + await LoadFromStorage(); + break; + } + } + + async Task LoadFromFile() + { + try + { + FolderPicker folderPicker = new FolderPicker(); + folderPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary; + folderPicker.FileTypeFilter.Add("*"); + StorageFolder folder = await folderPicker.PickSingleFolderAsync(); + + if (folder != null) + { + StorageApplicationPermissions.FutureAccessList.Add(folder); + await LoadImages(new FileSource(new Uri(folder.Path)), _maxImageCountPerProcessingCycle, null); + } + } + catch (Exception ex) + { + await Util.GenericApiCallExceptionHandler(ex, "Error loading from the target folder."); + } + } + + async Task LoadFromStorage() + { + try + { + var dialog = new StorageDialog(); + if ((await dialog.ShowAsync()) == ContentDialogResult.Primary) + { + await LoadImages(new StorageSource(new Uri(dialog.SasUri)), _maxImageCountPerProcessingCycle, null); + } + } + catch (Exception ex) + { + await Util.GenericApiCallExceptionHandler(ex, "Error loading from the target storage."); + } + } + + async Task LoadImages(ImageProcessorSource source, int? fileLimit, int? startingFileIndex) + { + //prepare UI + this.progressRing.IsActive = true; + this.landingMessage.Visibility = Visibility.Collapsed; + this.filterTab.Visibility = Visibility.Visible; + this.ActiveFilters.Visibility = Visibility.Visible; + this.ImagesContainer.Visibility = Visibility.Visible; + ImageFilters.Clear(); + await FaceListManager.ResetFaceLists(); + + try + { + //convert to view model + var serviceTypes = (FaceApi.IsChecked.GetValueOrDefault() ? ImageProcessorServiceType.Face : 0) | + (ComputerVisionApi.IsChecked.GetValueOrDefault() ? ImageProcessorServiceType.ComputerVision : 0) | + (CustomVisionApi.IsChecked.GetValueOrDefault() ? ImageProcessorServiceType.CustomVision : 0); + var customVisionProjects = CustomVisionProjects.Where(i => i.IsSelected).Select(i => i.Project.Id).ToArray(); + var data = await _imageProcessor.ProcessImagesAsync(source, await GetServices(serviceTypes, customVisionProjects), fileLimit, startingFileIndex, async insight => + { + await ImageFilters.AddImage(insight); + }); + CurrentData = data; + + ImageFilters.AddImagesCompleted(); + + //save data + await FileManager.SaveFileAsync(CurrentData); + } + catch + { + StartOver(); + throw; + } + finally + { + //finished + this.progressRing.IsActive = false; + } + } + + async Task LoadImages(DigitalAssetData data) + { + //prepare UI + this.progressRing.IsActive = true; + this.landingMessage.Visibility = Visibility.Collapsed; + this.filterTab.Visibility = Visibility.Visible; + this.ActiveFilters.Visibility = Visibility.Visible; + this.ImagesContainer.Visibility = Visibility.Visible; + ImageFilters.Clear(); + CurrentData = null; + + try + { + foreach (var insight in data.Insights) + { + await ImageFilters.AddImage(insight); + } + CurrentData = data; + + ImageFilters.AddImagesCompleted(); + } + catch (Exception ex) + { + StartOver(); + await Util.GenericApiCallExceptionHandler(ex, "Error loading from history."); + } + finally + { + //finished + this.progressRing.IsActive = false; + } + } + + async Task LoadMoreImages(int? fileLimit) + { + //validate + var data = CurrentData; + if (data == null) + { + return; + } + + //prepare UI + this.progressRing.IsActive = true; + + try + { + //create source + var source = data.Info.Source == "StorageSource" ? (ImageProcessorSource)new StorageSource(data.Info.Path) : new FileSource(data.Info.Path); + + //convert to view model + var newData = await _imageProcessor.ProcessImagesAsync(source, await GetServices(data.Info.Services, data.Info.CustomVisionProjects), fileLimit, data.Info.LastFileIndex, async insight => + { + await ImageFilters.AddImage(insight); + }); + newData.Insights = data.Insights.Concat(newData.Insights).ToArray(); + CurrentData = newData; + + ImageFilters.AddImagesCompleted(); + + //save data + await FileManager.SaveFileAsync(CurrentData); + } + catch + { + StartOver(); + throw; + } + finally + { + //finished + this.progressRing.IsActive = false; + } + } + + async void History_ItemClick(object sender, ItemClickEventArgs e) + { + (sender as ListViewBase).SelectedItem = null; + + //load the data + var data = await FileManager.GetFileData((e.ClickedItem as FileViewModel).File); + if (data == null) + { + await Util.GenericApiCallExceptionHandler(new Exception("failed to load json file"), "Error loading from history."); + } + + HistoryFlyout.Hide(); + + //load the images + await LoadImages(data); + } + + async void Reprocess_Click(object sender, RoutedEventArgs e) + { + await LoadMoreImages(_maxImageCountPerProcessingCycle); + } + + async void Download_Click(object sender, RoutedEventArgs e) + { + await FileManager.DownloadFileAsync((sender as FrameworkElement).DataContext as FileViewModel); + } + + async void Delete_Click(object sender, RoutedEventArgs e) + { + await FileManager.DeleteFileAsync((sender as FrameworkElement).DataContext as FileViewModel); + } + + async Task GetServices(ImageProcessorServiceType serviceTypes, Guid[] customVisionProjects) + { + var result = new List(); + if (serviceTypes.HasFlag(ImageProcessorServiceType.Face)) + { + result.Add(new FaceProcessorService()); + } + if (serviceTypes.HasFlag(ImageProcessorServiceType.ComputerVision)) + { + result.Add(new ComputerVisionProcessorService()); + } + if (serviceTypes.HasFlag(ImageProcessorServiceType.CustomVision)) + { + result.Add(new CustomVisionProcessorService(_customVisionPrediction, await CustomVisionProcessorService.GetProjectIterations(_customVisionTraining, customVisionProjects))); + } + return result.ToArray(); + } + + private void FilterChanged(object sender, RoutedEventArgs e) + { + ImageFilters.ApplyFilters(); + } + + private void WordSearch(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) + { + if (sender.Tag == null) //flag to ignore updating the autosuggest list + { + System.Diagnostics.Debug.WriteLine(args.CheckCurrent()); + //find suggestion for last word + var lastWord = sender.Text.Trim().Split(' ').LastOrDefault() ?? string.Empty; + //pick top 5 words sorting words starting with on top + sender.ItemsSource = ImageFilters.WordFilters + .Where(i => ((string)i.Key).Contains(lastWord, StringComparison.OrdinalIgnoreCase)) //contains word + .OrderBy(i => ((string)i.Key).StartsWith(lastWord, StringComparison.OrdinalIgnoreCase) ? 1 : 2) //put terms starting with word on top + .Select(i => i.Key).Take(5); //top 5 + } + else + { + sender.Tag = null; //reset the flag + } + } + } + + private void WordSearchQuery(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + ImageFilters.ApplyWordsFilter(sender.Text); + } + + private void WordSearchChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) + { + //replace last word only + var words = sender.Text.Trim().Split(' '); + words[words.Length - 1] = args.SelectedItem as string; + sender.Text = string.Join(' ', words); + sender.Tag = true; //flag to ignore updating the autosuggest list + } + + private void ShowAllToggle(object sender, RoutedEventArgs e) + { + var filter = (sender as FrameworkElement)?.DataContext as FilterCollection; + if (filter != null) + { + filter.IsShowingAll = !filter.IsShowingAll; + } + } + + private void ImagesContainer_ItemClick(object sender, ItemClickEventArgs e) + { + DetailView.DataContext = e.ClickedItem; + FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement); + } + + private void NavigateCustomVisionSetup(Windows.UI.Xaml.Documents.Hyperlink sender, Windows.UI.Xaml.Documents.HyperlinkClickEventArgs args) + { + Frame.Navigate(typeof(CustomVisionSetup)); + } + + public class ProjectViewModel : INotifyPropertyChanged + { + bool _isSelected; + + public event PropertyChangedEventHandler PropertyChanged; + + public Project Project { get; set; } + public bool IsSelected + { + get => _isSelected; + set + { + _isSelected = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected))); + } + } + } + } +} diff --git a/Kiosk/Views/DigitalAssetManagement/FilesViewModel.cs b/Kiosk/Views/DigitalAssetManagement/FilesViewModel.cs new file mode 100644 index 0000000..5f7df6a --- /dev/null +++ b/Kiosk/Views/DigitalAssetManagement/FilesViewModel.cs @@ -0,0 +1,207 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Cognitive Services: http://www.microsoft.com/cognitive +// +// Microsoft Cognitive Services Github: +// https://github.com/Microsoft/Cognitive +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Windows.Storage; +using Windows.Storage.Pickers; + +namespace IntelligentKioskSample.Views.DigitalAssetManagement +{ + public class FilesViewModel + { + string _folderName = "DigitalAssetManagement"; + + public ObservableCollection Files { get; } = new ObservableCollection(); + + public async Task LoadFilesAsync() + { + //load file listing + var folder = await GetFolder(); + var files = (await folder.GetFilesAsync()).OrderByDescending(i => i.DateCreated); + Files.Clear(); + foreach (var file in files) + { + try + { + Files.Add(new FileViewModel { File = file, Info = await GetFileInfo(file) }); + } + catch { } + } + } + + public async Task SaveFileAsync(DigitalAssetData data) + { + //get existing file to replace, or new file + var file = Files.Where(i => i.Info.Path == data.Info.Path).Select(i => i.File).FirstOrDefault() ?? await (await GetFolder()).CreateFileAsync($"{Guid.NewGuid()}.json", CreationCollisionOption.ReplaceExisting); + + //save file + using (StreamWriter writer = new StreamWriter(await file.OpenStreamForWriteAsync())) + { + string jsonStr = JsonConvert.SerializeObject(data, Formatting.Indented); + await writer.WriteAsync(jsonStr); + } + + //reload files + await LoadFilesAsync(); + } + + public async Task GetFileData(StorageFile file) + { + try + { + using (var stream = await file.OpenStreamForReadAsync()) + { + using (var reader = new StreamReader(stream)) + { + using (var json = new JsonTextReader(reader)) + { + var serializer = new JsonSerializer(); + return serializer.Deserialize(json); + } + } + } + } + catch { } + return null; + } + + public async Task DeleteFileAsync(FileViewModel file) + { + await file.File.DeleteAsync(); + + //refresh file list + await LoadFilesAsync(); + } + + public async Task DownloadFileAsync(FileViewModel file) + { + //prompt for location to save + try + { + var save = new FileSavePicker(); + save.SuggestedStartLocation = PickerLocationId.Downloads; + save.SuggestedFileName = file.Info.Name; + save.FileTypeChoices.Add("json", new List { ".json" }); + var newFile = await save.PickSaveFileAsync(); + if (newFile != null) + { + await file.File.CopyAndReplaceAsync(newFile); + } + } + catch (Exception ex) + { + await Util.GenericApiCallExceptionHandler(ex, "Error downloading file."); + } + } + + async Task GetFileInfo(StorageFile file) + { + try + { + using (var stream = await file.OpenStreamForReadAsync()) + { + using (var reader = new StreamReader(stream)) + { + using (var json = new JsonTextReader(reader)) + { + var serializer = new JsonSerializer(); + while (json.Read()) + { + if (json.TokenType == JsonToken.PropertyName && (json.Value as string) == "Info") + { + json.Read(); + if (json.TokenType == JsonToken.StartObject) + { + return serializer.Deserialize(json); + } + } + } + } + } + } + } + catch { } + return null; + } + + async Task GetFolder() + { + if ((await ApplicationData.Current.LocalFolder.TryGetItemAsync(_folderName)) != null) + { + return await ApplicationData.Current.LocalFolder.GetFolderAsync(_folderName); + } + else + { + return await ApplicationData.Current.LocalFolder.CreateFolderAsync(_folderName); + } + } + } + + public class FileViewModel + { + public StorageFile File { get; set; } + public DigitalAssetInfo Info { get; set; } + } + + public class DigitalAssetData + { + public DigitalAssetInfo Info { get; set; } + public ImageInsights[] Insights { get; set; } + } + + public class DigitalAssetInfo + { + public Uri Path { get; set; } + public string Name { get; set; } + public ImageProcessorServiceType Services { get; set; } + public Guid[] CustomVisionProjects { get; set; } + public int? FileLimit { get; set; } + public int LastFileIndex { get; set; } + public bool ReachedEndOfFiles { get; set; } + public string Source { get; set; } + } + + [Flags] + public enum ImageProcessorServiceType + { + Face = 1, + ComputerVision = 2, + CustomVision = 4 + } +} diff --git a/Kiosk/Views/ImageCollectionInsights/VisionInsights.cs b/Kiosk/Views/DigitalAssetManagement/ImageInsights.cs similarity index 51% rename from Kiosk/Views/ImageCollectionInsights/VisionInsights.cs rename to Kiosk/Views/DigitalAssetManagement/ImageInsights.cs index 692fbc3..6e8966b 100644 --- a/Kiosk/Views/ImageCollectionInsights/VisionInsights.cs +++ b/Kiosk/Views/DigitalAssetManagement/ImageInsights.cs @@ -31,11 +31,52 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -namespace IntelligentKioskSample.Views.ImageCollectionInsights +using Microsoft.Azure.CognitiveServices.Vision.ComputerVision.Models; +using System; +using Face = Microsoft.Azure.CognitiveServices.Vision.Face.Models; + +namespace IntelligentKioskSample.Views.DigitalAssetManagement { + public class ImageInsights + { + public Uri ImageUri { get; set; } + public FaceInsights[] FaceInsights { get; set; } + public VisionInsights VisionInsights { get; set; } + public CustomVisionInsights[] CustomVisionInsights { get; set; } + } + public class VisionInsights { public string Caption { get; set; } public string[] Tags { get; set; } + public string[] Objects { get; set; } + public string[] Landmarks { get; set; } + public string[] Celebrities { get; set; } + public string[] Brands { get; set; } + public string[] Words { get; set; } + public AdultInfo Adult { get; set; } + public ColorInfo Color { get; set; } + public ImageType ImageType { get; set; } + public ImageMetadata Metadata { get; set; } + } + + public class FaceInsights + { + public Guid UniqueFaceId { get; set; } + public Face.FaceRectangle FaceRectangle { get; set; } + public Face.FaceAttributes FaceAttributes { get; set; } + } + + public class CustomVisionInsights + { + public string Name { get; set; } + public CustomVisionPrediction[] Predictions { get; set; } + public bool IsObjectDetection { get; set; } + } + + public class CustomVisionPrediction + { + public string Name { get; set; } + public double Probability { get; set; } } } diff --git a/Kiosk/Views/DigitalAssetManagement/ImageInsightsViewModel.cs b/Kiosk/Views/DigitalAssetManagement/ImageInsightsViewModel.cs new file mode 100644 index 0000000..91bea53 --- /dev/null +++ b/Kiosk/Views/DigitalAssetManagement/ImageInsightsViewModel.cs @@ -0,0 +1,861 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Cognitive Services: http://www.microsoft.com/cognitive +// +// Microsoft Cognitive Services Github: +// https://github.com/Microsoft/Cognitive +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using Microsoft.Azure.CognitiveServices.Vision.ComputerVision.Models; +using Microsoft.Azure.CognitiveServices.Vision.Face.Models; +using ServiceHelpers; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.Storage; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Imaging; +using Face = Microsoft.Azure.CognitiveServices.Vision.Face; + +namespace IntelligentKioskSample.Views.DigitalAssetManagement +{ + public class ImageFiltersViewModel + { + List _allFilters; + List _allResults = new List(); + + public ObservableCollection FilteredResults { get; set; } = new ObservableCollection(); + public FilterCollection ActiveFilters { get; set; } = new FilterCollection() { Name = "Filters" }; + public FilterCollection TagFilters { get; set; } = new FilterCollection() { Name = "Tags" }; + public FilterCollection FaceFilters { get; set; } = new FilterCollection() { Name = "Unique faces" }; + public FilterCollection EmotionFilters { get; set; } = new FilterCollection() { Name = "Emotion" }; + public FilterCollection ObjectFilters { get; set; } = new FilterCollection() { Name = "Detected objects" }; + public FilterCollection LandmarkFilters { get; set; } = new FilterCollection() { Name = "Landmarks" }; + public FilterCollection CelebrityFilters { get; set; } = new FilterCollection() { Name = "Celebrities" }; + public FilterCollection BrandFilters { get; set; } = new FilterCollection() { Name = "Brands" }; + public FilterCollection WordFilters { get; set; } = new FilterCollection() { Name = "Extracted text" }; + public FilterCollection WordFiltersSelected { get; set; } = new FilterCollection() { Name = "Selected text" }; + public FilterCollection ModerationFilters { get; set; } = new FilterCollection() { Name = "Content moderation" }; + public FilterCollection ColorFilters { get; set; } = new FilterCollection() { Name = "Color" }; + public FilterCollection OrientationFilters { get; set; } = new FilterCollection() { Name = "Orientation" }; + public FilterCollection ImageTypeFilters { get; set; } = new FilterCollection() { Name = "Image type" }; + public FilterCollection SizeFilters { get; set; } = new FilterCollection() { Name = "Size" }; + public FilterCollection AgeFilters { get; set; } = new FilterCollection() { Name = "Age" }; + public FilterCollection GenderFilters { get; set; } = new FilterCollection() { Name = "Gender" }; + public FilterCollection PeopleFilters { get; set; } = new FilterCollection() { Name = "Number of people" }; + public FilterCollection FaceAttributesFilters { get; set; } = new FilterCollection() { Name = "Face attributes" }; + public FilterCollection FaceQualityFilters { get; set; } = new FilterCollection() { Name = "Face image quality" }; + public FilterCollection CustomVisionTagFilters { get; set; } = new FilterCollection() { Name = "Custom Vision tags" }; + public FilterCollection CustomVisionObjectFilters { get; set; } = new FilterCollection() { Name = "Custom Vision objects" }; + + public ImageFiltersViewModel() + { + //set fields + _allFilters = new List() { TagFilters, FaceFilters, EmotionFilters, ObjectFilters, LandmarkFilters, CelebrityFilters, BrandFilters, WordFilters, ModerationFilters, ColorFilters, OrientationFilters, ImageTypeFilters, SizeFilters, AgeFilters, GenderFilters, PeopleFilters, FaceAttributesFilters, FaceQualityFilters, CustomVisionTagFilters, CustomVisionObjectFilters }; + } + + public void Clear() + { + FilteredResults.Clear(); + _allResults.Clear(); + ActiveFilters.Clear(); + foreach (var filter in _allFilters) + { + filter.Clear(); + } + } + + public void ApplyFilters() + { + var activeFilters = _allFilters.SelectMany(i => i.Where(e => e.IsChecked)); + FilteredResults.AddRemoveRange(activeFilters.SelectMany(i => i.Parents).Distinct()); + if (FilteredResults.Count == 0 && _allFilters.SelectMany(i => i.Where(e => e.IsChecked)).Count() == 0) + { + FilteredResults.AddRemoveRange(_allResults); + } + ActiveFilters.AddRemoveRange(activeFilters); + } + + public void ApplyWordsFilter(string text) + { + //select filter words + var words = text.Trim().Split(' '); + var hasChanged = false; + foreach (var filter in WordFilters.Where(i => i.IsChecked && !words.Contains((string)i.Key, StringComparer.OrdinalIgnoreCase))) + { + filter.IsChecked = false; + hasChanged = true; + } + foreach (var filter in WordFilters.Where(i => words.Contains((string)i.Key, StringComparer.OrdinalIgnoreCase))) + { + filter.IsChecked = true; + hasChanged = true; + } + + if (hasChanged) + { + //update filter + ApplyFilters(); + + //update selected filters + WordFiltersSelected.AddRemoveRange(WordFilters.Where(i => i.IsChecked)); + } + } + + public void AddImagesCompleted() + { + foreach (var filter in _allFilters) + { + //sort + var reordered = filter.OrderByDescending(i => i.Parents.Count).ThenBy(i => i.Key).AsQueryable(); + + //apply gender related filters + if (!SettingsHelper.Instance.ShowAgeAndGender && filter.Count != 0 && filter.First().Key is string) + { + reordered = reordered.Where(i => !Util.ContainsGenderRelatedKeyword(i.Key as string)); + } + + var items = reordered.ToList(); + filter.Clear(); + filter.AddRange(items); + } + } + + public async Task AddImage(ImageInsights insights) + { + // Load image from file + BitmapImage bitmapImage = new BitmapImage(); + if (insights.ImageUri.IsFile) + { + await bitmapImage.SetSourceAsync((await (await StorageFile.GetFileFromPathAsync(insights.ImageUri.AbsoluteUri)).OpenStreamForReadAsync()).AsRandomAccessStream()); + } + else + { + bitmapImage.UriSource = insights.ImageUri; + } + + //load smaller image - for performace + bitmapImage.DecodePixelHeight = 270; + + // Create the view models + ImageInsightsViewModel insightsViewModel = new ImageInsightsViewModel() { Insights = insights, ImageSource = bitmapImage }; + + //tags + foreach (var entity in insights.VisionInsights?.Tags ?? Array.Empty()) + { + AddFilter(TagFilters, entity, entity, insightsViewModel); + } + + //faces + foreach (var entity in insights.FaceInsights ?? Array.Empty()) + { + var key = entity.UniqueFaceId == Guid.Empty ? Guid.NewGuid() : entity.UniqueFaceId; + var imageScale = bitmapImage.PixelHeight < bitmapImage.DecodePixelHeight ? 1d : (double)bitmapImage.DecodePixelHeight / (double)bitmapImage.PixelHeight; + var filter = AddFilter(FaceFilters, entity, key, insightsViewModel, bitmapImage, entity.FaceRectangle.ToRect().Scale(imageScale).Inflate(2)); + + //rescale face rect if image has been rescaled + if (filter.Count == 1 && bitmapImage.PixelHeight == 0) + { + bitmapImage.ImageOpened += (sender, e) => + { + var bitmap = sender as BitmapImage; + if (bitmap.DecodePixelHeight != 0 && bitmap.DecodePixelHeight < bitmap.PixelHeight) + { + var imageFilter = filter as ImageFilterViewModel; + imageFilter.ImageCrop = entity.FaceRectangle.ToRect().Scale((double)bitmap.DecodePixelHeight / (double)bitmap.PixelHeight).Inflate(2); + } + }; + } + } + + //emotions + insightsViewModel.Emotions = insights.FaceInsights?.Select(i => Util.EmotionToRankedList(i.FaceAttributes.Emotion).First().Key).Distinct().ToArray() ?? Array.Empty(); + foreach (var entity in insightsViewModel.Emotions) + { + AddFilter(EmotionFilters, entity, entity, insightsViewModel); + } + + //objects + foreach (var entity in insights.VisionInsights?.Objects ?? Array.Empty()) + { + AddFilter(ObjectFilters, entity, entity, insightsViewModel); + } + + //landmarks + foreach (var entity in insights.VisionInsights?.Landmarks ?? Array.Empty()) + { + AddFilter(LandmarkFilters, entity, entity, insightsViewModel); + } + + //celebrities + foreach (var entity in insights.VisionInsights?.Celebrities ?? Array.Empty()) + { + AddFilter(CelebrityFilters, entity, entity, insightsViewModel); + } + + //brands + foreach (var entity in insights.VisionInsights?.Brands ?? Array.Empty()) + { + AddFilter(BrandFilters, entity, entity, insightsViewModel); + } + + //moderation + insightsViewModel.Moderation = GetAdultFlags(insights.VisionInsights?.Adult).ToArray(); + foreach (var entity in insightsViewModel.Moderation) + { + AddFilter(ModerationFilters, entity, entity, insightsViewModel); + insightsViewModel.BlurImage = true; //set blur flag + } + + //words + foreach (var entity in insights.VisionInsights?.Words ?? Array.Empty()) + { + AddFilter(WordFilters, entity, entity, insightsViewModel); + } + + //color + insightsViewModel.Color = GetColorFlags(insights.VisionInsights?.Color).ToArray(); + foreach (var entity in insightsViewModel.Color) + { + AddFilter(ColorFilters, entity, entity, insightsViewModel); + } + + //orientation + insightsViewModel.Orientation = GetOrientation(insights.VisionInsights?.Metadata); + if (insightsViewModel.Orientation != null) + { + AddFilter(OrientationFilters, insightsViewModel.Orientation, insightsViewModel.Orientation, insightsViewModel); + } + + //image type + insightsViewModel.ImageType = GetImageTypeFlags(insights.VisionInsights?.ImageType).ToArray(); + foreach (var entity in insightsViewModel.ImageType) + { + AddFilter(ImageTypeFilters, entity, entity, insightsViewModel); + } + + //size + insightsViewModel.Size = GetSize(insights.VisionInsights?.Metadata); + if (insightsViewModel.Size != null) + { + AddFilter(SizeFilters, insightsViewModel.Size, insightsViewModel.Size, insightsViewModel); + } + + //People + insightsViewModel.People = GetPeopleFlags(insights.VisionInsights?.Objects, insights.FaceInsights).ToArray(); + foreach (var entity in insightsViewModel.People) + { + AddFilter(PeopleFilters, entity, entity, insightsViewModel); + } + + //face attributes + insightsViewModel.FaceAttributes = GetFaceAttributesFlags(insights.FaceInsights).ToArray(); + foreach (var entity in insightsViewModel.FaceAttributes) + { + AddFilter(FaceAttributesFilters, entity, entity, insightsViewModel); + } + + //face quality + insightsViewModel.FaceQualtity = GetFaceQualityFlags(insights.FaceInsights).ToArray(); + foreach (var entity in insightsViewModel.FaceQualtity) + { + AddFilter(FaceQualityFilters, entity, entity, insightsViewModel); + } + + //Custom Vision tags + insightsViewModel.CustomVisionTags = GetCustomVisionTags(insights.CustomVisionInsights).ToArray(); + foreach (var entity in insightsViewModel.CustomVisionTags) + { + AddFilter(CustomVisionTagFilters, entity, entity, insightsViewModel); + } + + //Custom Vision objects + insightsViewModel.CustomVisionObjects = GetCustomVisionObjects(insights.CustomVisionInsights).ToArray(); + foreach (var entity in insightsViewModel.CustomVisionObjects) + { + AddFilter(CustomVisionObjectFilters, entity, entity, insightsViewModel); + } + + if (SettingsHelper.Instance.ShowAgeAndGender) //only if age and gender is allowed + { + //Age + insightsViewModel.Age = GetAgeFlags(insights.FaceInsights).ToArray(); + foreach (var entity in insightsViewModel.Age) + { + AddFilter(AgeFilters, entity, entity, insightsViewModel); + } + + //Gender + insightsViewModel.Gender = GetGenderFlags(insights.FaceInsights).ToArray(); + foreach (var entity in insightsViewModel.Gender) + { + AddFilter(GenderFilters, entity, entity, insightsViewModel); + } + } + + //add viewmodel to collection + _allResults.Add(insightsViewModel); + FilteredResults.Add(insightsViewModel); + } + + TextFilterViewModel AddFilter(ICollection filters, object entity, object key, ImageInsightsViewModel parent, ImageSource imageSource = null, Rect? imageCrop = null) + { + var filter = filters.FirstOrDefault(i => i.Key.Equals(key)); + if (filter == null) + { + //construct filter + if (imageSource == null) + { + filter = new TextFilterViewModel() { Entity = entity, Key = key }; + } + else + { + filter = new ImageFilterViewModel() { Entity = entity, Key = key, ImageSource = imageSource, ImageCrop = imageCrop ?? new Rect() }; + } + + filters.Add(filter); + } + if (!filter.Parents.Contains(parent)) + { + filter.AddParent(parent); + } + return filter; + } + + IEnumerable GetAdultFlags(AdultInfo info) + { + var result = new List(); + if (info != null) + { + if (info.IsAdultContent) + { + result.Add("Adult"); + } + if (info.IsRacyContent) + { + result.Add("Racy"); + } + if (info.IsGoryContent) + { + result.Add("Gore"); + } + } + return result; + } + + IEnumerable GetColorFlags(ColorInfo color) + { + var result = new List(); + if (color != null) + { + if (color.IsBWImg) + { + result.Add("Black & White"); + return result; + } + + result.Add(color.DominantColorForeground); + result.Add(color.DominantColorBackground); + } + return result.Distinct(); + } + + string GetOrientation(ImageMetadata metadata) + { + //validate + if (metadata == null || metadata.Height == 0 || metadata.Width == 0) + { + return null; + } + + var aspectRatio = (double)metadata.Height / (double)metadata.Width; + return aspectRatio > 1 ? "Vertical" : "Horizontal"; + } + + IEnumerable GetImageTypeFlags(ImageType imageType) + { + var result = new List(); + if (imageType != null) + { + if (imageType.ClipArtType > 0) + { + result.Add("Clip Art"); + } + if (imageType.LineDrawingType > 0) + { + result.Add("Line Drawing"); + } + } + return result; + } + + string GetSize(ImageMetadata metadata) + { + //validate + if (metadata == null || metadata.Height == 0 || metadata.Width == 0) + { + return null; + } + + //get image size + if (metadata.Height * metadata.Width >= 800000) + { + return "Large"; + } + else if (metadata.Height * metadata.Width >= 120000) + { + return "Medium"; + } + else if (metadata.Height * metadata.Width > 65536) + { + return "Small"; + } + return "Icon"; + } + + IEnumerable GetPeopleFlags(string[] objects, FaceInsights[] faces) + { + var result = new List(); + + //from person objects + var objectCount = -1; + if (objects != null) + { + objectCount = objects.Where(i => i == "person").Count(); + } + + //from faces + var faceCount = -1; + if (faces != null) + { + faceCount = faces.Length; + } + + //pick the highest + var peopleCount = objectCount > faceCount ? objectCount : faceCount; + + //create the results + if (peopleCount == 0) + { + result.Add("contains no people"); + } + else if (peopleCount > 0) + { + result.Add("contains a person"); + } + if (peopleCount == 1) + { + result.Add("1 person"); + } + else if (peopleCount == 2) + { + result.Add("2 people"); + } + else if (peopleCount == 3) + { + result.Add("3 people"); + } + else if (peopleCount >= 4) + { + result.Add("4 or more people"); + } + return result; + } + + IEnumerable GetAgeFlags(FaceInsights[] faces) + { + var result = new List(); + if (faces != null) + { + foreach (var face in faces) + { + if (face.FaceAttributes.Age < 4) + { + result.Add("Infants"); + } + else if (face.FaceAttributes.Age < 13) + { + result.Add("Children"); + } + else if (face.FaceAttributes.Age < 20) + { + result.Add("Teenagers"); + } + else if (face.FaceAttributes.Age < 30) + { + result.Add("20s"); + } + else if (face.FaceAttributes.Age < 40) + { + result.Add("30s"); + } + else if (face.FaceAttributes.Age < 50) + { + result.Add("40s"); + } + else if (face.FaceAttributes.Age < 60) + { + result.Add("50s"); + } + else if (face.FaceAttributes.Age < 70) + { + result.Add("60s"); + } + else + { + result.Add("70s and older"); + } + } + } + return result.Distinct(); + } + + IEnumerable GetGenderFlags(FaceInsights[] faces) + { + return faces?.Select(i => i.FaceAttributes.Gender.GetValueOrDefault(Face.Models.Gender.Genderless).ToString()).Distinct() ?? Array.Empty(); + } + + IEnumerable GetFaceAttributesFlags(FaceInsights[] faces) + { + var result = new List(); + var threshhold = .49; + if (faces != null) + { + foreach (var face in faces) + { + //accessories + foreach (var accessory in face.FaceAttributes.Accessories?.Where(i => i.Confidence >= threshhold).Select(i => i.Type.ToString()) ?? Array.Empty()) + { + result.Add(accessory); + } + //Beard + if ((face.FaceAttributes.FacialHair?.Beard ?? 0) >= threshhold) + { + result.Add("Beard"); + } + //Moustache + if ((face.FaceAttributes.FacialHair?.Moustache ?? 0) >= threshhold) + { + result.Add("Moustache"); + } + //Sideburns + if ((face.FaceAttributes.FacialHair?.Sideburns ?? 0) >= threshhold) + { + result.Add("Sideburns"); + } + //Bald + if ((face.FaceAttributes.Hair?.Bald ?? 0) >= threshhold) + { + result.Add("Bald"); + } + //HairColor + var hairColor = (face.FaceAttributes.Hair?.HairColor ?? Array.Empty()).Where(i => i.Color != HairColorType.Unknown && i.Color != HairColorType.Other).OrderByDescending(i => i.Confidence).Select(i => i.Color.ToString()).FirstOrDefault(); + if (hairColor != null) + { + result.Add(hairColor + " Hair"); + } + //Hair invisible + if (face.FaceAttributes.Hair?.Invisible ?? false) + { + result.Add("Hair isn't visible"); + } + //Eye Makup + if (face.FaceAttributes.Makeup?.EyeMakeup ?? false) + { + result.Add("Eye Makup"); + } + //Lip Makup + if (face.FaceAttributes.Makeup?.LipMakeup ?? false) + { + result.Add("Lip Makup"); + } + //glasses + var glasses = face.FaceAttributes.Glasses.GetValueOrDefault(Face.Models.GlassesType.NoGlasses); + if (glasses != GlassesType.NoGlasses) + { + switch (glasses) + { + case Face.Models.GlassesType.ReadingGlasses: + result.Add("Reading Glasses"); + break; + case Face.Models.GlassesType.Sunglasses: + result.Add("Sunglasses"); + break; + case Face.Models.GlassesType.SwimmingGoggles: + result.Add("Swimming Goggles"); + break; + default: + result.Add(glasses.ToString()); + break; + } + } + //head pose + if (face.FaceAttributes.HeadPose.Pitch.Between(-10, 10) && face.FaceAttributes.HeadPose.Roll.Between(-10, 10) && face.FaceAttributes.HeadPose.Yaw.Between(-10, 10)) + { + result.Add("Facing Camera"); + } + if (!face.FaceAttributes.HeadPose.Yaw.Between(-37, 37)) + { + result.Add("Profile"); + } + //Occlusion + if (face.FaceAttributes.Occlusion?.EyeOccluded ?? false) + { + result.Add("Eye isn't visible"); + } + if (face.FaceAttributes.Occlusion?.ForeheadOccluded ?? false) + { + result.Add("Forehead isn't visible"); + } + if (face.FaceAttributes.Occlusion?.MouthOccluded ?? false) + { + result.Add("Mouth isn't visible"); + } + } + } + return result.Distinct(); + } + + IEnumerable GetFaceQualityFlags(FaceInsights[] faces) + { + var result = new List(); + if (faces != null) + { + foreach (var face in faces) + { + //Blur + var blur = face.FaceAttributes.Blur?.BlurLevel; + if (blur != null) + { + result.Add(blur.ToString() + " Blur"); + } + //Exposure + var exposure = face.FaceAttributes.Exposure?.ExposureLevel; + if (exposure != null) + { + switch (exposure) + { + case ExposureLevel.UnderExposure: + result.Add("Under Exposure"); + break; + case ExposureLevel.GoodExposure: + result.Add("Good Exposure"); + break; + case ExposureLevel.OverExposure: + result.Add("Over Exposure"); + break; + } + } + //Noise + var noise = face.FaceAttributes.Noise?.NoiseLevel; + if (noise != null) + { + result.Add(noise.ToString() + " Noise Level"); + } + } + } + return result.Distinct(); + } + + IEnumerable GetCustomVisionTags(CustomVisionInsights[] customVision) + { + return customVision?.Where(i => !i.IsObjectDetection).SelectMany(i => i.Predictions.Where(e => e.Probability >= .6).Select(e => e.Name)) ?? Enumerable.Empty(); + } + + IEnumerable GetCustomVisionObjects(CustomVisionInsights[] customVision) + { + return customVision?.Where(i => i.IsObjectDetection).SelectMany(i => i.Predictions.Where(e => e.Probability >= .6).Select(e => e.Name)) ?? Enumerable.Empty(); + } + } + + public class ImageInsightsViewModel + { + public ImageInsights Insights { get; set; } + public ImageSource ImageSource { get; set; } + public bool BlurImage { get; set; } + public string[] Emotions { get; set; } + public string[] Moderation { get; set; } + public string[] Color { get; set; } + public string Orientation { get; set; } + public string[] ImageType { get; set; } + public string Size { get; set; } + public string[] People { get; set; } + public string[] FaceAttributes { get; set; } + public string[] FaceQualtity { get; set; } + public string[] CustomVisionTags { get; set; } + public string[] CustomVisionObjects { get; set; } + public string[] Age { get; set; } + public string[] Gender { get; set; } + } + + public class TextFilterViewModel : INotifyPropertyChanged + { + bool _isChecked; + List _parents = new List(); + + public event PropertyChangedEventHandler PropertyChanged; + + public object Entity { get; set; } + public object Key { get; set; } + public IReadOnlyList Parents => _parents; + public int Count { get; private set; } + + public bool IsChecked + { + get => _isChecked; + set + { + _isChecked = value; + OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsChecked))); + } + } + + public void AddParent(ImageInsightsViewModel parent) + { + _parents.Add(parent); + Count++; + OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count))); + } + + protected void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + } + + public class ImageFilterViewModel : TextFilterViewModel + { + Rect _imageCrop; + + public ImageSource ImageSource { get; set; } + public Rect ImageCrop + { + get => _imageCrop; + set + { + _imageCrop = value; + OnPropertyChanged(new PropertyChangedEventArgs(nameof(ImageCrop))); + } + } + } + + public class FilterCollection : ObservableCollection + { + bool _isShowingAll; + bool _showAllEnabled; + string _name; + + public int ShowAllCount { get; set; } = 20; + + public string Name + { + get => _name; + set { _name = value; OnPropertyChanged(new PropertyChangedEventArgs(nameof(Name))); } + } + + public bool IsShowingAll + { + get => _isShowingAll; + set { _isShowingAll = value; OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsShowingAll))); } + } + + public bool ShowAllEnabled + { + get => _showAllEnabled; + protected set { _showAllEnabled = value; OnPropertyChanged(new PropertyChangedEventArgs(nameof(ShowAllEnabled))); } + } + + protected override void InsertItem(int index, TextFilterViewModel item) + { + base.InsertItem(index, item); + + if (Count > ShowAllCount) + { + ShowAllEnabled = true; + } + } + + protected override void ClearItems() + { + base.ClearItems(); + + ShowAllEnabled = false; + IsShowingAll = false; + } + + protected override void RemoveItem(int index) + { + base.RemoveItem(index); + + if (Count <= ShowAllCount) + { + ShowAllEnabled = false; + } + } + } + + + public static class Extensions + { + public static void AddRange(this IList list, IEnumerable items) + { + foreach (var item in items) + { + list.Add(item); + } + } + + public static bool Between(this double value, double lowest, double highest) + { + return value >= lowest && value <= highest; + } + + public static void AddRemoveRange(this IList list, IEnumerable items) + { + //get items to remove + var toRemove = list.Except(items).ToArray(); + + //get items to add + var toAdd = items.Except(list).ToArray(); + + //remove items + foreach (var item in toRemove) + { + list.Remove(item); + } + + //add items + list.AddRange(toAdd); + } + } +} diff --git a/Kiosk/Views/DigitalAssetManagement/ImageProcessor.cs b/Kiosk/Views/DigitalAssetManagement/ImageProcessor.cs new file mode 100644 index 0000000..aa708f1 --- /dev/null +++ b/Kiosk/Views/DigitalAssetManagement/ImageProcessor.cs @@ -0,0 +1,494 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Cognitive Services: http://www.microsoft.com/cognitive +// +// Microsoft Cognitive Services Github: +// https://github.com/Microsoft/Cognitive +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using Microsoft.Azure.CognitiveServices.Vision.ComputerVision.Models; +using Microsoft.Azure.CognitiveServices.Vision.CustomVision.Prediction; +using Microsoft.Azure.CognitiveServices.Vision.CustomVision.Prediction.Models; +using Microsoft.Azure.CognitiveServices.Vision.CustomVision.Training; +using Microsoft.Azure.CognitiveServices.Vision.Face.Models; +using Microsoft.Azure.Storage.Blob; +using Microsoft.Rest; +using ServiceHelpers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Windows.Storage; +using Windows.Storage.Search; + +namespace IntelligentKioskSample.Views.DigitalAssetManagement +{ + public class ImageProcessor + { + public async Task ProcessImagesAsync(ImageProcessorSource source, IImageProcessorService[] services, int? fileLimit, int? startingFileIndex, Func callback) + { + //validate + if (services == null || services.Length == 0) + { + throw new ApplicationException("No Azure services provided for image processing pipeline. Need at least 1."); + } + + //get images + var insights = new List(); + var (filePaths, reachedEndOfFiles) = await source.GetFilePaths(fileLimit, startingFileIndex); + + //process each image - in batches + var tasks = new List>(); + var lastFile = filePaths.Last(); + foreach (var filePath in filePaths) + { + tasks.Add(ProcessImageAsync(filePath, services)); + if (tasks.Count == 8 || filePath == lastFile) + { + var results = await Task.WhenAll(tasks); + foreach (var task in tasks) + { + var insight = task.Result; + insights.Add(insight); + await callback(insight); + } + tasks.Clear(); + } + } + + var serviceTypes = services.Select(i => i.GetProcessorServiceType).Aggregate((i, e) => i | e); + var customVisionProjects = services.OfType().SelectMany(i => i.ProjectIterations.Select(e => e.Project)).ToArray(); + return new DigitalAssetData() + { + Info = new DigitalAssetInfo + { + Path = source.Path, + FileLimit = fileLimit, + Services = serviceTypes, + CustomVisionProjects = customVisionProjects, + Name = source.GetName(), + LastFileIndex = filePaths.Count() + (startingFileIndex ?? 0), + ReachedEndOfFiles = reachedEndOfFiles, + Source = source.GetType().Name + }, + Insights = insights.ToArray() + }; + } + + async Task ProcessImageAsync(Uri filePath, IImageProcessorService[] services) + { + ImageAnalyzer analyzer = null; + if (filePath.IsFile) + { + analyzer = new ImageAnalyzer((await StorageFile.GetFileFromPathAsync(filePath.LocalPath)).OpenStreamForReadAsync); + } + else + { + analyzer = new ImageAnalyzer(filePath.AbsoluteUri); + } + analyzer.ShowDialogOnFaceApiErrors = true; + + //run image processors + var tasks = services.Select(i => (i.ProcessImage(analyzer).ToArray())).ToArray(); + await Task.WhenAll(tasks.SelectMany(i => i)); + + //run post image processor + await Task.WhenAll(services.SelectMany(i => (i.PostProcessImage(analyzer)))); + + //assign the results + var result = new ImageInsights { ImageUri = filePath }; + for (int index = 0; index < services.Length; index++) + { + services[index].AssignResult(tasks[index], analyzer, result); + } + + return result; + } + } + + public interface IImageProcessorService + { + ImageProcessorServiceType GetProcessorServiceType { get; } + IEnumerable ProcessImage(ImageAnalyzer analyzer); + IEnumerable PostProcessImage(ImageAnalyzer analyzer); + void AssignResult(IEnumerable completeTask, ImageAnalyzer analyzer, ImageInsights result); + } + + public class FaceProcessorService : IImageProcessorService + { + static readonly FaceAttributeType[] _faceFeatures = new[] + { + FaceAttributeType.Accessories, + FaceAttributeType.Age, + FaceAttributeType.Blur, + FaceAttributeType.Emotion, + FaceAttributeType.Exposure, + FaceAttributeType.FacialHair, + FaceAttributeType.Gender, + FaceAttributeType.Glasses, + FaceAttributeType.Hair, + FaceAttributeType.HeadPose, + FaceAttributeType.Makeup, + FaceAttributeType.Noise, + FaceAttributeType.Occlusion, + FaceAttributeType.Smile + }; + + public ImageProcessorServiceType GetProcessorServiceType { get => ImageProcessorServiceType.Face; } + public IEnumerable ProcessImage(ImageAnalyzer analyzer) + { + yield return analyzer.DetectFacesAsync(true, false, _faceFeatures); + } + + public IEnumerable PostProcessImage(ImageAnalyzer analyzer) + { + yield return analyzer.FindSimilarPersistedFacesAsync(); + } + + public void AssignResult(IEnumerable completeTask, ImageAnalyzer analyzer, ImageInsights result) + { + // assign face api results + List faceInsightsList = new List(); + foreach (var face in analyzer.DetectedFaces ?? Array.Empty()) + { + FaceInsights faceInsights = new FaceInsights { FaceRectangle = face.FaceRectangle, FaceAttributes = face.FaceAttributes }; + + var similarFaceMatch = analyzer.SimilarFaceMatches?.FirstOrDefault(s => s.Face.FaceId == face.FaceId); + if (similarFaceMatch != null) + { + faceInsights.UniqueFaceId = similarFaceMatch.SimilarPersistedFace.PersistedFaceId.GetValueOrDefault(); + } + + faceInsightsList.Add(faceInsights); + } + result.FaceInsights = faceInsightsList.ToArray(); + } + } + + public class ComputerVisionProcessorService : IImageProcessorService + { + static readonly VisualFeatureTypes?[] _visionFeatures = new VisualFeatureTypes?[] + { + VisualFeatureTypes.Tags, + VisualFeatureTypes.Description, + VisualFeatureTypes.Objects, + VisualFeatureTypes.Brands, + VisualFeatureTypes.Categories, + VisualFeatureTypes.Adult, + VisualFeatureTypes.ImageType, + VisualFeatureTypes.Color + }; + + public ImageProcessorServiceType GetProcessorServiceType { get => ImageProcessorServiceType.ComputerVision; } + public IEnumerable ProcessImage(ImageAnalyzer analyzer) + { + yield return analyzer.AnalyzeImageAsync(null, visualFeatures: _visionFeatures); + yield return analyzer.RecognizeTextAsync(); + } + + public IEnumerable PostProcessImage(ImageAnalyzer analyzer) + { + yield break; + } + + public void AssignResult(IEnumerable completeTask, ImageAnalyzer analyzer, ImageInsights result) + { + // assign computer vision results + result.VisionInsights = new VisionInsights + { + Caption = analyzer.AnalysisResult?.Description?.Captions.FirstOrDefault()?.Text, + Tags = analyzer.AnalysisResult?.Tags != null ? analyzer.AnalysisResult.Tags.Select(t => t.Name).ToArray() : new string[0], + Objects = analyzer.AnalysisResult?.Objects != null ? analyzer.AnalysisResult.Objects.Select(t => t.ObjectProperty).ToArray() : new string[0], + Celebrities = analyzer.AnalysisResult?.Categories != null ? analyzer.AnalysisResult.Categories.Where(i => i.Detail?.Celebrities != null && i.Detail.Celebrities.Count != 0).SelectMany(i => i.Detail.Celebrities).Select(i => i.Name).ToArray() : new string[0], + Landmarks = analyzer.AnalysisResult?.Categories != null ? analyzer.AnalysisResult.Categories.Where(i => i.Detail?.Landmarks != null && i.Detail.Landmarks.Count != 0).SelectMany(i => i.Detail.Landmarks).Select(i => i.Name).ToArray() : new string[0], + Brands = analyzer.AnalysisResult?.Brands != null ? analyzer.AnalysisResult.Brands.Select(t => t.Name).ToArray() : new string[0], + Adult = analyzer.AnalysisResult?.Adult, + Color = analyzer.AnalysisResult?.Color, + ImageType = analyzer.AnalysisResult?.ImageType, + Metadata = analyzer.AnalysisResult?.Metadata, + Words = analyzer.TextOperationResult?.Lines != null ? analyzer.TextOperationResult.Lines.SelectMany(i => i.Words).Select(i => i.Text).ToArray() : new string[0], + }; + } + } + + public class CustomVisionProcessorService : IImageProcessorService + { + SemaphoreSlim _semaphore = new SemaphoreSlim(1); + ICustomVisionPredictionClient _predictionClient; + public ProjectIteration[] ProjectIterations { get; } + + public CustomVisionProcessorService(ICustomVisionPredictionClient predictionClient, ProjectIteration[] projectIterations) + { + //set fields + _predictionClient = predictionClient; + ProjectIterations = projectIterations; + } + + public static async Task GetProjectIterations(ICustomVisionTrainingClient trainingClient, Guid[] projects) + { + var result = new List(); + foreach (var project in projects) + { + var iterations = await trainingClient.GetIterationsAsync(project); + var projectEntity = await trainingClient.GetProjectAsync(project); + var domain = await trainingClient.GetDomainAsync(projectEntity.Settings.DomainId); + var iteration = iterations.Where(i => i.Status == "Completed").OrderByDescending(i => i.TrainedAt.Value).FirstOrDefault(); + if (iteration != null) + { + result.Add(new ProjectIteration() { Project = project, Iteration = iteration.Id, ProjectName = projectEntity.Name, IsObjectDetection = domain.Type == "ObjectDetection" }); + } + } + return result.ToArray(); + } + + public ImageProcessorServiceType GetProcessorServiceType { get => ImageProcessorServiceType.CustomVision; } + public IEnumerable ProcessImage(ImageAnalyzer analyzer) + { + foreach (var projectIteration in ProjectIterations) + { + yield return PredictImage(analyzer, projectIteration, ProjectIterations.Length); + } + } + + async Task> PredictImage(ImageAnalyzer analyzer, ProjectIteration projectIteration, int projectCount) + { + if (analyzer.ImageUrl != null) + { + return (await AutoRetry(async () => await _predictionClient.PredictImageUrlAsync(projectIteration.Project, new Microsoft.Azure.CognitiveServices.Vision.CustomVision.Prediction.Models.ImageUrl(analyzer.ImageUrl), projectIteration.Iteration)), projectIteration); + } + else + { + //bug fix: if multiple project needed to be processed for a local file, create requests for them one at a time. + //(for some reason when the CustomVision client creates multiple requests using the same file stream it locks up creating the request - a better fix would be good as this one degrades speed) + var allowAsync = projectCount == 1; + + return (await RunSequentially(async () => await AutoRetry(async () => await _predictionClient.PredictImageAsync(projectIteration.Project, await analyzer.GetImageStreamCallback(), projectIteration.Iteration)), allowAsync), projectIteration); + } + } + + async Task RunSequentially(Func> action, bool allowAsync) + { + if (!allowAsync) + { + await _semaphore.WaitAsync(); + } + try + { + return await action(); + } + finally + { + if (!allowAsync) + { + _semaphore.Release(); + } + } + } + + async Task AutoRetry(Func> action) + { + int retriesLeft = 6; + int delay = 500; + + TResponse response = default(TResponse); + + while (true) + { + try + { + response = await action(); + break; + } + catch (HttpOperationException exception) when (exception.Response.StatusCode == (System.Net.HttpStatusCode)429 && retriesLeft > 0) + { + ErrorTrackingHelper.TrackException(exception, "Custom Vision API throttling error"); + + await Task.Delay(delay); + retriesLeft--; + delay *= 2; + continue; + } + } + + return response; + } + + public IEnumerable PostProcessImage(ImageAnalyzer analyzer) + { + yield break; + } + + public void AssignResult(IEnumerable completeTask, ImageAnalyzer analyzer, ImageInsights result) + { + result.CustomVisionInsights = completeTask.OfType>>().Select(i => new CustomVisionInsights + { + Name = i.Result.Item2.ProjectName, + IsObjectDetection = i.Result.Item2.IsObjectDetection, + Predictions = i.Result.Item1.Predictions.Select(e => new CustomVisionPrediction + { + Name = e.TagName, + Probability = e.Probability + }).ToArray() + }).ToArray(); + } + + public class ProjectIteration + { + public Guid Project { get; set; } + public Guid Iteration { get; set; } + public string ProjectName { get; set; } + public bool IsObjectDetection { get; set; } + } + } + + public abstract class ImageProcessorSource + { + protected string[] ValidExtentions = { ".png", ".jpg", ".bmp", ".jpeg", ".gif" }; + public Uri Path { get; } + + public ImageProcessorSource(Uri path) + { + //set fields + Path = path; + } + + public abstract Task<(IEnumerable, bool)> GetFilePaths(int? fileLimit, int? startingIndex); + public abstract string GetName(); + } + + public class StorageSource : ImageProcessorSource + { + public StorageSource(Uri path) : base(path) { } + + public override async Task<(IEnumerable, bool)> GetFilePaths(int? fileLimit, int? startingIndex) + { + //calculate max results + var maxResults = fileLimit; + if (fileLimit != null && startingIndex != null) + { + maxResults = fileLimit + startingIndex; + } + + var container = new CloudBlobContainer(Path); + var results = new List(); + var reachedFileLimit = false; + var skipped = 0; + var files = await container.ListBlobsSegmentedAsync(null, false, BlobListingDetails.None, maxResults, null, null, null); + BlobContinuationToken continuationToken = null; + do + { + foreach (var file in files.Results) + { + //skip if not the right extention + var extentionIndex = file.Uri.LocalPath.LastIndexOf('.'); + if (extentionIndex >= 0 && ValidExtentions.Contains(file.Uri.LocalPath.Substring(extentionIndex), StringComparer.OrdinalIgnoreCase)) + { + //skip + if (startingIndex != null && skipped != startingIndex) + { + skipped++; + continue; + } + + //create file URI + var root = Path.AbsoluteUri.Remove(Path.AbsoluteUri.Length - Path.PathAndQuery.Length); + var query = Path.Query; + var path = file.Uri.LocalPath; + var fileUri = new Uri(root + path + query); + results.Add(fileUri); + + //at file limit + if (fileLimit != null && results.Count >= fileLimit.Value) + { + reachedFileLimit = true; + break; + } + } + } + + //get more results + continuationToken = files.ContinuationToken; + if (files.ContinuationToken != null && !reachedFileLimit) + { + files = await container.ListBlobsSegmentedAsync(null, false, BlobListingDetails.Metadata, maxResults, files.ContinuationToken, null, null); + } + else + { + break; + } + } while (continuationToken != null); + + //determine if we reached the end of all files + var reachedEndOfFiles = !reachedFileLimit; + if (reachedEndOfFiles && files.ContinuationToken != null) + { + reachedEndOfFiles = false; + } + + return (results.ToArray(), reachedEndOfFiles); + } + + public override string GetName() + { + return Uri.UnescapeDataString(Path.LocalPath.Replace(@"/", string.Empty)); + } + } + + public class FileSource : ImageProcessorSource + { + public FileSource(Uri path) : base(path) { } + + public override async Task<(IEnumerable, bool)> GetFilePaths(int? fileLimit, int? startingIndex) + { + //calulate new file limit + if (fileLimit != null && startingIndex != null) + { + fileLimit = fileLimit + startingIndex; + } + + var folder = await StorageFolder.GetFolderFromPathAsync(Path.LocalPath); + var query = folder.CreateFileQueryWithOptions(new QueryOptions(CommonFileQuery.DefaultQuery, ValidExtentions)); + var files = fileLimit != null ? await query.GetFilesAsync(0, (uint)fileLimit.Value) : await query.GetFilesAsync(); + var filePaths = files.Select(i => new Uri(i.Path)); + if (startingIndex != null) + { + filePaths = filePaths.Skip(startingIndex.Value); + } + var result = filePaths.ToArray(); + var reachedEndOfFiles = fileLimit == null || result.Length < fileLimit; + return (result, reachedEndOfFiles); + } + + public override string GetName() + { + return Uri.UnescapeDataString(Path.Segments.Last()); + } + } +} diff --git a/Kiosk/Views/DigitalAssetManagement/StorageDialog.xaml b/Kiosk/Views/DigitalAssetManagement/StorageDialog.xaml new file mode 100644 index 0000000..67a871c --- /dev/null +++ b/Kiosk/Views/DigitalAssetManagement/StorageDialog.xaml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/Kiosk/Views/ImageCollectionInsights/ImageInsights.cs b/Kiosk/Views/DigitalAssetManagement/StorageDialog.xaml.cs similarity index 83% rename from Kiosk/Views/ImageCollectionInsights/ImageInsights.cs rename to Kiosk/Views/DigitalAssetManagement/StorageDialog.xaml.cs index 206d716..4a20578 100644 --- a/Kiosk/Views/ImageCollectionInsights/ImageInsights.cs +++ b/Kiosk/Views/DigitalAssetManagement/StorageDialog.xaml.cs @@ -31,12 +31,17 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -namespace IntelligentKioskSample.Views.ImageCollectionInsights +using Windows.UI.Xaml.Controls; + +namespace IntelligentKioskSample.Views.DigitalAssetManagement { - public class ImageInsights + public sealed partial class StorageDialog : ContentDialog { - public string ImageId { get; set; } - public FaceInsights[] FaceInsights { get; set; } - public VisionInsights VisionInsights { get; set; } + public string SasUri { get; set; } + + public StorageDialog() + { + this.InitializeComponent(); + } } } diff --git a/Kiosk/Views/ImageCollectionInsights/EmotionFilterViewModel.cs b/Kiosk/Views/ImageCollectionInsights/EmotionFilterViewModel.cs deleted file mode 100644 index f189f9b..0000000 --- a/Kiosk/Views/ImageCollectionInsights/EmotionFilterViewModel.cs +++ /dev/null @@ -1,61 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. -// -// Microsoft Cognitive Services: http://www.microsoft.com/cognitive -// -// Microsoft Cognitive Services Github: -// https://github.com/Microsoft/Cognitive -// -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License: -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System.ComponentModel; - -namespace IntelligentKioskSample.Views.ImageCollectionInsights -{ - public class EmotionFilterViewModel : INotifyPropertyChanged - { - public bool IsChecked { get; set; } - public string Emotion { get; set; } - - private int count; - public int Count - { - get { return this.count; } - set - { - this.count = value; - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count")); - } - } - - public event PropertyChangedEventHandler PropertyChanged; - - public EmotionFilterViewModel(string emotion) - { - this.Emotion = emotion; - } - } -} diff --git a/Kiosk/Views/ImageCollectionInsights/FaceFilterViewModel.cs b/Kiosk/Views/ImageCollectionInsights/FaceFilterViewModel.cs deleted file mode 100644 index c321829..0000000 --- a/Kiosk/Views/ImageCollectionInsights/FaceFilterViewModel.cs +++ /dev/null @@ -1,65 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. -// -// Microsoft Cognitive Services: http://www.microsoft.com/cognitive -// -// Microsoft Cognitive Services Github: -// https://github.com/Microsoft/Cognitive -// -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License: -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.ComponentModel; -using Windows.UI.Xaml.Media; - -namespace IntelligentKioskSample.Views.ImageCollectionInsights -{ - public class FaceFilterViewModel : INotifyPropertyChanged - { - public bool IsChecked { get; set; } - public Guid FaceId { get; set; } - public ImageSource ImageSource { get; set; } - - private int count; - public int Count - { - get { return this.count; } - set - { - this.count = value; - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count")); - } - } - - public event PropertyChangedEventHandler PropertyChanged; - - public FaceFilterViewModel(Guid faceId, ImageSource croppedFace) - { - this.FaceId = faceId; - this.ImageSource = croppedFace; - } - } -} diff --git a/Kiosk/Views/ImageCollectionInsights/FaceInsights.cs b/Kiosk/Views/ImageCollectionInsights/FaceInsights.cs deleted file mode 100644 index 90ad6a1..0000000 --- a/Kiosk/Views/ImageCollectionInsights/FaceInsights.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. -// -// Microsoft Cognitive Services: http://www.microsoft.com/cognitive -// -// Microsoft Cognitive Services Github: -// https://github.com/Microsoft/Cognitive -// -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License: -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using Microsoft.Azure.CognitiveServices.Vision.Face.Models; -using System; - -namespace IntelligentKioskSample.Views.ImageCollectionInsights -{ - public class FaceInsights - { - public Guid UniqueFaceId { get; set; } - public FaceRectangle FaceRectangle { get; set; } - public string TopEmotion { get; set; } - public string Gender { get; set; } - public double Age { get; set; } - } -} diff --git a/Kiosk/Views/ImageCollectionInsights/ImageCollectionInsights.xaml b/Kiosk/Views/ImageCollectionInsights/ImageCollectionInsights.xaml deleted file mode 100644 index c25c0f7..0000000 --- a/Kiosk/Views/ImageCollectionInsights/ImageCollectionInsights.xaml +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Kiosk/Views/ImageCollectionInsights/ImageCollectionInsights.xaml.cs b/Kiosk/Views/ImageCollectionInsights/ImageCollectionInsights.xaml.cs deleted file mode 100644 index d46b87e..0000000 --- a/Kiosk/Views/ImageCollectionInsights/ImageCollectionInsights.xaml.cs +++ /dev/null @@ -1,371 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. -// -// Microsoft Cognitive Services: http://www.microsoft.com/cognitive -// -// Microsoft Cognitive Services Github: -// https://github.com/Microsoft/Cognitive -// -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License: -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using Microsoft.Azure.CognitiveServices.Vision.Face.Models; -using Newtonsoft.Json; -using ServiceHelpers; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using System.Threading.Tasks; -using Windows.Foundation; -using Windows.Storage; -using Windows.Storage.Pickers; -using Windows.Storage.Search; -using Windows.UI.Popups; -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; - -namespace IntelligentKioskSample.Views.ImageCollectionInsights -{ - [KioskExperience(Id = "ImageCollectionInsights", - DisplayName = "Image Collection Insights", - Description = "See how Computer Vision can add a layer of insights to image collections", - ImagePath = "ms-appx:/Assets/DemoGallery/Image Collection Insights.jpg", - ExperienceType = ExperienceType.Guided | ExperienceType.Business, - TechnologiesUsed = TechnologyType.Face | TechnologyType.Emotion | TechnologyType.Vision, - TechnologyArea = TechnologyAreaType.Vision, - DateAdded = "2017/09/11")] - public sealed partial class ImageCollectionInsights : Page - { - private StorageFolder currentRootFolder; - - public List AllResults { get; set; } = new List(); - public ObservableCollection FilteredResults { get; set; } = new ObservableCollection(); - public ObservableCollection TagFilters { get; set; } = new ObservableCollection(); - public ObservableCollection FaceFilters { get; set; } = new ObservableCollection(); - public ObservableCollection EmotionFilters { get; set; } = new ObservableCollection(); - - public ImageCollectionInsights() - { - this.InitializeComponent(); - } - - protected async override void OnNavigatedTo(NavigationEventArgs e) - { - if (string.IsNullOrEmpty(SettingsHelper.Instance.FaceApiKey) || - string.IsNullOrEmpty(SettingsHelper.Instance.VisionApiKey)) - { - await new MessageDialog("Missing Face or Vision API Key. Please enter a key in the Settings page.", "Missing API Key").ShowAsync(); - } - - base.OnNavigatedTo(e); - } - - private async void ProcessImagesClicked(object sender, RoutedEventArgs e) - { - try - { - FolderPicker folderPicker = new FolderPicker(); - folderPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary; - folderPicker.FileTypeFilter.Add("*"); - StorageFolder folder = await folderPicker.PickSingleFolderAsync(); - - if (folder != null) - { - await ProcessImagesAsync(folder); - } - - this.currentRootFolder = folder; - } - catch (Exception ex) - { - await Util.GenericApiCallExceptionHandler(ex, "Error picking the target folder."); - } - } - - private async void ReProcessImagesClicked(object sender, RoutedEventArgs e) - { - await this.ProcessImagesAsync(this.currentRootFolder, forceProcessing: true); - } - - private async Task ProcessImagesAsync(StorageFolder rootFolder, bool forceProcessing = false) - { - this.progressRing.IsActive = true; - - this.landingMessage.Visibility = Visibility.Collapsed; - this.filterTab.Visibility = Visibility.Visible; - this.reprocessImagesButton.IsEnabled = true; - - this.FilteredResults.Clear(); - this.AllResults.Clear(); - this.TagFilters.Clear(); - this.EmotionFilters.Clear(); - this.FaceFilters.Clear(); - - List insightsList = new List(); - - if (!forceProcessing) - { - // see if we have pre-computed results and if so load it from the json file - try - { - StorageFile insightsResultFile = (await rootFolder.TryGetItemAsync("ImageInsights.json")) as StorageFile; - if (insightsResultFile != null) - { - using (StreamReader reader = new StreamReader(await insightsResultFile.OpenStreamForReadAsync())) - { - insightsList = JsonConvert.DeserializeObject>(await reader.ReadToEndAsync()); - foreach (var insights in insightsList) - { - await AddImageInsightsToViewModel(rootFolder, insights); - } - } - } - } - catch - { - // We will just compute everything again in case of errors - } - } - - if (!insightsList.Any()) - { - // start with fresh face lists - await FaceListManager.ResetFaceLists(); - - // enumerate through the images and extract the insights - QueryOptions fileQueryOptions = new QueryOptions(CommonFileQuery.DefaultQuery, new[] { ".png", ".jpg", ".bmp", ".jpeg", ".gif" }); - StorageFileQueryResult queryResult = rootFolder.CreateFileQueryWithOptions(fileQueryOptions); - var queryFileList = this.limitProcessingToggleButton.IsChecked.Value ? await queryResult.GetFilesAsync(0, 50) : await queryResult.GetFilesAsync(); - - foreach (var item in queryFileList) - { - // Resize (if needed) in order to reduce network latency. Then store the result in a temporary file. - StorageFile resizedFile = await ApplicationData.Current.TemporaryFolder.CreateFileAsync("ImageCollectionInsights.jpg", CreationCollisionOption.GenerateUniqueName); - var resizeTransform = await Util.ResizePhoto(await item.OpenStreamForReadAsync(), 720, resizedFile); - - // Send the file for processing - ImageInsights insights = await ImageProcessor.ProcessImageAsync(resizedFile.OpenStreamForReadAsync, item.Name); - - // Delete resized file - await resizedFile.DeleteAsync(); - - // Adjust all FaceInsights coordinates based on the transform function between the original and resized photos - foreach (var faceInsight in insights.FaceInsights) - { - faceInsight.FaceRectangle.Left = (int) (faceInsight.FaceRectangle.Left * resizeTransform.Item1); - faceInsight.FaceRectangle.Top = (int)(faceInsight.FaceRectangle.Top * resizeTransform.Item2); - faceInsight.FaceRectangle.Width = (int)(faceInsight.FaceRectangle.Width * resizeTransform.Item1); - faceInsight.FaceRectangle.Height = (int)(faceInsight.FaceRectangle.Height * resizeTransform.Item2); - } - - insightsList.Add(insights); - await AddImageInsightsToViewModel(rootFolder, insights); - } - - // save to json - StorageFile jsonFile = await rootFolder.CreateFileAsync("ImageInsights.json", CreationCollisionOption.ReplaceExisting); - using (StreamWriter writer = new StreamWriter(await jsonFile.OpenStreamForWriteAsync())) - { - string jsonStr = JsonConvert.SerializeObject(insightsList, Formatting.Indented); - await writer.WriteAsync(jsonStr); - } - } - - List tagsGroupedByCountAndSorted = new List(); - foreach (var group in this.TagFilters.GroupBy(t => t.Count).OrderByDescending(g => g.Key)) - { - tagsGroupedByCountAndSorted.AddRange(group.OrderBy(t => t.Tag)); - } - - if (!SettingsHelper.Instance.ShowAgeAndGender) - { - tagsGroupedByCountAndSorted = tagsGroupedByCountAndSorted.Where(t => !Util.ContainsGenderRelatedKeyword(t.Tag)).ToList(); - } - - this.TagFilters.Clear(); - this.TagFilters.AddRange(tagsGroupedByCountAndSorted); - - var sortedEmotions = this.EmotionFilters.OrderByDescending(e => e.Count).ToArray(); - this.EmotionFilters.Clear(); - this.EmotionFilters.AddRange(sortedEmotions); - - var sortedFaces = this.FaceFilters.OrderByDescending(f => f.Count).ToArray(); - this.FaceFilters.Clear(); - this.FaceFilters.AddRange(sortedFaces); - - this.progressRing.IsActive = false; - } - - private async Task AddImageInsightsToViewModel(StorageFolder rootFolder, ImageInsights insights) - { - // Load image from file - BitmapImage bitmapImage = new BitmapImage(); - await bitmapImage.SetSourceAsync((await (await rootFolder.GetFileAsync(insights.ImageId)).OpenStreamForReadAsync()).AsRandomAccessStream()); - bitmapImage.DecodePixelHeight = 360; - - // Create the view models - ImageInsightsViewModel insightsViewModel = new ImageInsightsViewModel(insights, bitmapImage); - this.AllResults.Add(insightsViewModel); - this.FilteredResults.Add(insightsViewModel); - - foreach (var tag in insights.VisionInsights.Tags) - { - TagFilterViewModel tvm = this.TagFilters.FirstOrDefault(t => t.Tag == tag); - if (tvm == null) - { - tvm = new TagFilterViewModel(tag); - this.TagFilters.Add(tvm); - } - tvm.Count++; - } - - foreach (var faceInsights in insights.FaceInsights) - { - FaceFilterViewModel fvm = this.FaceFilters.FirstOrDefault(f => f.FaceId == faceInsights.UniqueFaceId); - if (fvm == null) - { - StorageFile file = (await rootFolder.GetFileAsync(insights.ImageId)); - ImageSource croppedFaced = await Util.GetCroppedBitmapAsync( - file.OpenStreamForReadAsync, - new Rect( - faceInsights.FaceRectangle.Left, - faceInsights.FaceRectangle.Top, - faceInsights.FaceRectangle.Width, - faceInsights.FaceRectangle.Height - )); - - fvm = new FaceFilterViewModel(faceInsights.UniqueFaceId, croppedFaced); - this.FaceFilters.Add(fvm); - } - fvm.Count++; - } - - var distinctEmotions = insights.FaceInsights.Select(f => f.TopEmotion).Distinct(); - foreach (var emotion in distinctEmotions) - { - EmotionFilterViewModel evm = this.EmotionFilters.FirstOrDefault(f => f.Emotion == emotion); - if (evm == null) - { - evm = new EmotionFilterViewModel(emotion); - this.EmotionFilters.Add(evm); - } - evm.Count++; - } - } - - private void ApplyFilters() - { - this.FilteredResults.Clear(); - - var checkedTags = this.TagFilters.Where(t => t.IsChecked); - var checkedFaces = this.FaceFilters.Where(f => f.IsChecked); - var checkedEmotions = this.EmotionFilters.Where(e => e.IsChecked); - if (checkedTags.Any() || checkedFaces.Any() || checkedEmotions.Any()) - { - var fromTags = this.AllResults.Where(r => HasTag(checkedTags, r.Insights.VisionInsights.Tags)); - var fromFaces = this.AllResults.Where(r => HasFace(checkedFaces, r.Insights.FaceInsights)); - var fromEmotion = this.AllResults.Where(r => HasEmotion(checkedEmotions, r.Insights.FaceInsights)); - - this.FilteredResults.AddRange((fromTags.Concat(fromFaces).Concat(fromEmotion)).Distinct()); - } - else - { - this.FilteredResults.AddRange(this.AllResults); - } - } - - private bool HasFace(IEnumerable checkedFaces, FaceInsights[] faceInsights) - { - foreach (var item in checkedFaces) - { - if (faceInsights.Any(f => f.UniqueFaceId == item.FaceId)) - { - return true; - } - } - - return false; - } - - private bool HasEmotion(IEnumerable checkedEmotions, FaceInsights[] faceInsights) - { - foreach (var item in checkedEmotions) - { - if (faceInsights.Any(f => f.TopEmotion == item.Emotion)) - { - return true; - } - } - - return false; - } - - private bool HasTag(IEnumerable checkedTags, string[] tags) - { - foreach (var item in checkedTags) - { - if (tags.Any(t => t == item.Tag)) - { - return true; - } - } - - return false; - } - - private void TagFilterChanged(object sender, RoutedEventArgs e) - { - this.ApplyFilters(); - } - - private void FaceFilterChanged(object sender, RoutedEventArgs e) - { - this.ApplyFilters(); - } - - private void EmotionFilterChanged(object sender, RoutedEventArgs e) - { - this.ApplyFilters(); - } - } - - public static class Extensions - { - public static void AddRange(this IList list, IEnumerable items) - { - foreach (var item in items) - { - list.Add(item); - } - } - - } -} diff --git a/Kiosk/Views/ImageCollectionInsights/ImageInsightsViewModel.cs b/Kiosk/Views/ImageCollectionInsights/ImageInsightsViewModel.cs deleted file mode 100644 index e92afac..0000000 --- a/Kiosk/Views/ImageCollectionInsights/ImageInsightsViewModel.cs +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. -// -// Microsoft Cognitive Services: http://www.microsoft.com/cognitive -// -// Microsoft Cognitive Services Github: -// https://github.com/Microsoft/Cognitive -// -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License: -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using Windows.UI.Xaml.Media; - -namespace IntelligentKioskSample.Views.ImageCollectionInsights -{ - public class ImageInsightsViewModel - { - public ImageInsights Insights { get; set; } - public ImageSource ImageSource { get; set; } - - public ImageInsightsViewModel(ImageInsights insights, ImageSource imageSource) - { - this.Insights = insights; - this.ImageSource = imageSource; - } - } -} diff --git a/Kiosk/Views/ImageCollectionInsights/ImageProcessor.cs b/Kiosk/Views/ImageCollectionInsights/ImageProcessor.cs deleted file mode 100644 index 2ad4fea..0000000 --- a/Kiosk/Views/ImageCollectionInsights/ImageProcessor.cs +++ /dev/null @@ -1,98 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. -// -// Microsoft Cognitive Services: http://www.microsoft.com/cognitive -// -// Microsoft Cognitive Services Github: -// https://github.com/Microsoft/Cognitive -// -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License: -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using Microsoft.Azure.CognitiveServices.Vision.ComputerVision.Models; -using ServiceHelpers; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace IntelligentKioskSample.Views.ImageCollectionInsights -{ - public class ImageProcessor - { - private static readonly List DefaultVisualFeatureTypes = new List - { - VisualFeatureTypes.Tags, - VisualFeatureTypes.Description - }; - - public static async Task ProcessImageAsync(Func> imageStream, string imageId) - { - ImageAnalyzer analyzer = new ImageAnalyzer(imageStream); - analyzer.ShowDialogOnFaceApiErrors = true; - - // trigger vision, face and emotion requests - await Task.WhenAll(analyzer.AnalyzeImageAsync(null, visualFeatures: DefaultVisualFeatureTypes), analyzer.DetectFacesAsync(detectFaceAttributes: true)); - - // trigger face match against previously seen faces - await analyzer.FindSimilarPersistedFacesAsync(); - - ImageInsights result = new ImageInsights { ImageId = imageId }; - - // assign computer vision results - result.VisionInsights = new VisionInsights - { - Caption = analyzer.AnalysisResult.Description?.Captions.FirstOrDefault()?.Text, - Tags = analyzer.AnalysisResult.Tags != null ? analyzer.AnalysisResult.Tags.Select(t => t.Name).ToArray() : new string[0] - }; - - // assign face api and emotion api results - List faceInsightsList = new List(); - foreach (var face in analyzer.DetectedFaces) - { - FaceInsights faceInsights = new FaceInsights - { - FaceRectangle = face.FaceRectangle, - Age = face.FaceAttributes.Age.GetValueOrDefault(), - Gender = face.FaceAttributes.Gender?.ToString() ?? string.Empty, - TopEmotion = Util.EmotionToRankedList(face.FaceAttributes.Emotion).First().Key - }; - - SimilarFaceMatch similarFaceMatch = analyzer.SimilarFaceMatches.FirstOrDefault(s => s.Face.FaceId == face.FaceId); - if (similarFaceMatch != null) - { - faceInsights.UniqueFaceId = similarFaceMatch.SimilarPersistedFace.PersistedFaceId.GetValueOrDefault(); - } - - faceInsightsList.Add(faceInsights); - } - - result.FaceInsights = faceInsightsList.ToArray(); - - return result; - } - } -} diff --git a/Kiosk/Views/ImageCollectionInsights/TagFilterViewModel.cs b/Kiosk/Views/ImageCollectionInsights/TagFilterViewModel.cs deleted file mode 100644 index 03bf14c..0000000 --- a/Kiosk/Views/ImageCollectionInsights/TagFilterViewModel.cs +++ /dev/null @@ -1,61 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. -// -// Microsoft Cognitive Services: http://www.microsoft.com/cognitive -// -// Microsoft Cognitive Services Github: -// https://github.com/Microsoft/Cognitive -// -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License: -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System.ComponentModel; - -namespace IntelligentKioskSample.Views.ImageCollectionInsights -{ - public class TagFilterViewModel : INotifyPropertyChanged - { - public bool IsChecked { get; set; } - public string Tag { get; set; } - - private int count; - public int Count - { - get { return this.count; } - set - { - this.count = value; - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count")); - } - } - - public event PropertyChangedEventHandler PropertyChanged; - - public TagFilterViewModel(string tag) - { - this.Tag = tag; - } - } -} diff --git a/README.md b/README.md index 0c93455..1453040 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,12 @@ The Intelligent Kiosk Sample is a collection of demos showcasing workflows and e | [Caption Bot](Documentation/CaptionBot.md)     | Get a description of the content of a webcam image. | Computer Vision API; Windows 10 Face Tracking; | | [Custom Vision Explorer](Documentation/CustomVisionExplorer.md)     | Shows how to use the Custom Vision Service to create a custom image classifier or object detector and score images against it. | Custom Vision API, Bing Image Search API; Bing AutoSuggestion API | | [Custom Vision Setup](Documentation/CustomVisionSetup.md)     | Shows how to train a machine learning model using the Custom Vision Service. | Custom Vision API, Bing Image Search API; Bing AutoSuggestion API | +| [Digital Asset Management](Documentation/DigitalAssetManagement.md) | Showcases how Computer Vision can add a layer of insights to a collection of images | Face API, Emotion API, Computer Vision API, Custom Vision API | | [Face API Explorer](Documentation/FaceAPIExplorer.md) | A playground for the Face APIs used for extracting face-related attributes, such as head pose, gender, age, emotion, facial hair, and glasses, as well as face identification. | Windows 10 Face Tracking; Face API; Bing Image Search API; Bing AutoSuggestion API | | [Face Identification Setup](Documentation/FaceIdentificationSetup.md) | Shows how to train the machine learning model behind the Face APIs to recognize specific people. | Face identification; Bing Search API; Bing AutoSuggestion API | | [Form Recognizer](Documentation/FormRecognizer.md) | Explore using machine learning models to extract key/value pairs and tables from scanned forms. This can be used to accelerate business processes by automatically converting scanned forms to usable data. | Form Recognizer API | | [Greeting Kiosk](Documentation/GreetingKiosk.md)         | A simple hands-free and real-time workflow for recognizing when a known person approaches the camera | Windows 10 Face Tracking; Realtime sampling; Face identification | | [How Old](Documentation/HowOld.md)         | An autonomous workflow for capturing photos when people approach a web camera and pose for a photo. | Windows 10 Face Tracking; Age and gender prediction; Face identification | -| [Image Collection Insights](Documentation/ImageCollectionInsights.md)         | An example of using Cognitive Services to add a layer of intelligence on top of a collection of photos | Face API, Emotion API, Computer Vision API | | [Insurance Claim Automation](Documentation/InsuranceClaimAutomation.md) | An example of Robotic Process Automation (RPA), leveraging Custom Vision and Form Recognizer to illustrate automating the validation of insurance claims. | Custom Vision API, Form Recognizer API, Bing Image Search API, Bing AutoSuggestion API | | [Mall Kiosk](Documentation/MallKiosk.md) | An example of a Mall kiosk that makes product recommendations based on the people in front of the camera and analyzes their reaction to it | Windows 10 Face Tracking; Age and gender prediction; Realtime sampling of Emotion; Face identification; Windows 10 Speech-To-Text; Text Sentiment Analysis | | [Realtime Crowd Insights](Documentation/RealtimeCrowdInsights.md) | A realtime workflow for processing frames from a web camera to derive realtime crowd insights such as demographics, emotion and unique face counting | Windows 10 Face Tracking; Realtime sampling; Age, gender and emotion prediction; Face identification; Unique face counting |