diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
index 67acbf4..b15dafe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -251,3 +251,5 @@ paket-files/
# JetBrains Rider
.idea/
*.sln.iml
+/WapProjTemplate1
+/DesktopBridgeDeployment
diff --git a/DatabaseLibrary/App.config b/DatabaseLibrary/App.config
new file mode 100644
index 0000000..2fb423e
--- /dev/null
+++ b/DatabaseLibrary/App.config
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DatabaseLibrary/Data/IDataProvider.cs b/DatabaseLibrary/Data/IDataProvider.cs
new file mode 100644
index 0000000..0678478
--- /dev/null
+++ b/DatabaseLibrary/Data/IDataProvider.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DatabaseLibrary.Data
+{
+ public interface IDataProvider
+ {
+ void AddFile(PictureFile file, string groupId, int processingState);
+
+ PictureFile GetFile(string path);
+
+ void RemoveFile(int fileId); // Must also remove any persons associated with the file
+
+ void AddPerson(PicturePerson person);
+
+ void RemovePerson(int personId, int fileId);
+
+ int GetFileCountForPersonId(Guid personId);
+
+ Person GetPerson(Guid personId);
+
+ void AddPerson(Guid personId, string name, string userData);
+
+ void RemovePersonsForGroup(string groupId);
+
+ List GetFilesForPersonId(Guid personId);
+ }
+}
diff --git a/DatabaseLibrary/Data/IsolatedStorageDatabase.cs b/DatabaseLibrary/Data/IsolatedStorageDatabase.cs
new file mode 100644
index 0000000..36e8edd
--- /dev/null
+++ b/DatabaseLibrary/Data/IsolatedStorageDatabase.cs
@@ -0,0 +1,119 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.IsolatedStorage;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DatabaseLibrary.Data
+{
+ public class IsolatedStorageDatabase
+ {
+ private static readonly string _isolatedStorageDatabaseFileName = "Database.json";
+
+ private static IsolatedStorageDatabase _db;
+
+ [JsonProperty(PropertyName = "P")]
+ public virtual List People { get; set; }
+ [JsonProperty(PropertyName = "PF")]
+ public virtual List PictureFiles { get; set; }
+ [JsonProperty(PropertyName = "PL")]
+ public virtual List PictureFileGroupLookups { get; set; }
+ [JsonProperty(PropertyName = "PP")]
+ public virtual List PicturePersons { get; set; }
+
+ internal void SaveChanges()
+ {
+ saveDatabaseToIsolatedStorage(_db);
+ }
+
+ public IsolatedStorageDatabase()
+ {
+ }
+
+ public static IsolatedStorageDatabase GetInstance()
+ {
+ if (_db == null)
+ {
+ _db = getDatabaseFromIsolatedStorage();
+ }
+
+ if (_db == null)
+ {
+ var d = new IsolatedStorageDatabase();
+ d.People = new List();
+ d.PictureFiles = new List();
+ d.PictureFileGroupLookups = new List();
+ d.PicturePersons = new List();
+
+ _db = d;
+ saveDatabaseToIsolatedStorage(d);
+ }
+
+ return _db;
+ }
+
+ public static void saveDatabaseToIsolatedStorage(IsolatedStorageDatabase database)
+ {
+ using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null))
+ {
+ using (var oStream = new IsolatedStorageFileStream(_isolatedStorageDatabaseFileName, FileMode.Create, isoStore))
+ {
+ using (var writer = new StreamWriter(oStream))
+ {
+ writer.Write(JsonConvert.SerializeObject(database));
+ }
+ }
+ }
+ }
+
+ private static IsolatedStorageDatabase getDatabaseFromIsolatedStorage()
+ {
+ IsolatedStorageDatabase database = null;
+
+ try
+ {
+ using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null))
+ {
+ using (var iStreamForEndpoint = new IsolatedStorageFileStream(_isolatedStorageDatabaseFileName, FileMode.Open, isoStore))
+ {
+ using (var readerForEndpoint = new StreamReader(iStreamForEndpoint))
+ {
+ var json = readerForEndpoint.ReadToEnd();
+ database = JsonConvert.DeserializeObject(json, new JsonSerializerSettings { Error = deserialiseErrorEventHandler });
+ }
+ }
+ }
+ }
+ catch (FileNotFoundException)
+ {
+ database = null;
+ }
+
+ return database;
+ }
+
+ private static void deserialiseErrorEventHandler(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs e)
+ {
+
+ }
+
+ public string GetDatabaseLocation()
+ {
+ using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null))
+ {
+ return isoStore.GetType().GetField("m_RootDir", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(isoStore).ToString();
+ }
+
+ }
+
+ // v2 use Isolated Storage like Table STorage ;)
+ //private void AddRow(string tableName, string alphaNumRowKey, object row)
+ //{
+ //}
+ }
+}
diff --git a/DatabaseLibrary/Data/Person.cs b/DatabaseLibrary/Data/Person.cs
new file mode 100644
index 0000000..d624729
--- /dev/null
+++ b/DatabaseLibrary/Data/Person.cs
@@ -0,0 +1,26 @@
+namespace DatabaseLibrary.Data
+{
+ using Newtonsoft.Json;
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Data.Entity.Spatial;
+
+ [Table("Person")]
+ public partial class Person
+ {
+ [JsonProperty(PropertyName = "I")]
+ public Guid PersonId { get; set; }
+
+ [Required]
+ [StringLength(300)]
+ [JsonProperty(PropertyName = "N")]
+ public string Name { get; set; }
+
+ [JsonProperty(PropertyName = "D")]
+ public string UserData { get; set; }
+
+ // public virtual ICollection PicturePersons { get; set; }
+ }
+}
diff --git a/DatabaseLibrary/Data/PhotosDatabase.cs b/DatabaseLibrary/Data/PhotosDatabase.cs
new file mode 100644
index 0000000..b67bd64
--- /dev/null
+++ b/DatabaseLibrary/Data/PhotosDatabase.cs
@@ -0,0 +1,33 @@
+namespace DatabaseLibrary.Data
+{
+ using System;
+ using System.Data.Entity;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Linq;
+
+ public partial class PhotosDatabase : DbContext
+ {
+ public PhotosDatabase(string nameOrConnectionString)
+ : base(nameOrConnectionString)
+ {
+ }
+
+ public virtual DbSet People { get; set; }
+ public virtual DbSet PictureFiles { get; set; }
+ public virtual DbSet PictureFileGroupLookups { get; set; }
+ public virtual DbSet PicturePersons { get; set; }
+
+ protected override void OnModelCreating(DbModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .Property(e => e.LargePersonGroupId)
+ .IsUnicode(false);
+
+ modelBuilder.Entity()
+ .HasKey(l => new { l.PictureFileId, l.LargePersonGroupId });
+
+ modelBuilder.Entity()
+ .HasKey(l => l.Id);
+ }
+ }
+}
diff --git a/DatabaseLibrary/Data/PhotosDatabase2.cs b/DatabaseLibrary/Data/PhotosDatabase2.cs
new file mode 100644
index 0000000..4f83ac9
--- /dev/null
+++ b/DatabaseLibrary/Data/PhotosDatabase2.cs
@@ -0,0 +1,90 @@
+namespace DatabaseLibrary.Data
+{
+ using System;
+ using System.Data.Entity;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Linq;
+ using System.IO.IsolatedStorage;
+ using System.IO;
+ using Newtonsoft.Json;
+
+ public partial class PhotosDatabase2 : DbContext
+ {
+ private static readonly string _isolatedStorageDatabaseFileName = "Database2.txt";
+
+ public PhotosDatabase2() : base()
+ {
+ }
+
+ public override int SaveChanges()
+ {
+ saveDatabaseToIsolatedStorage(this);
+ return 1;
+ }
+
+ public static void saveDatabaseToIsolatedStorage(PhotosDatabase2 database)
+ {
+ using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null))
+ {
+ using (var oStream = new IsolatedStorageFileStream(_isolatedStorageDatabaseFileName, FileMode.Create, isoStore))
+ {
+ using (var writer = new StreamWriter(oStream))
+ {
+ var settings = new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate };
+ writer.Write(JsonConvert.SerializeObject(database, settings));
+ }
+ }
+ }
+ }
+
+ private static PhotosDatabase2 getDatabaseFromIsolatedStorage()
+ {
+ PhotosDatabase2 database = null;
+
+ using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null))
+ {
+ try
+ {
+ using (var iStreamForEndpoint = new IsolatedStorageFileStream(_isolatedStorageDatabaseFileName, FileMode.Open, isoStore))
+ {
+ using (var readerForEndpoint = new StreamReader(iStreamForEndpoint))
+ {
+ var json = readerForEndpoint.ReadToEnd();
+ try
+ {
+ database = JsonConvert.DeserializeObject(json);
+ }
+ catch (Exception e)
+ {
+
+ }
+ }
+ }
+ }
+ catch (FileNotFoundException)
+ {
+ database = null;
+ }
+ }
+ return database;
+ }
+
+ public virtual DbSet People { get; set; }
+ public virtual DbSet PictureFiles { get; set; }
+ public virtual DbSet PictureFileGroupLookups { get; set; }
+ public virtual DbSet PicturePersons { get; set; }
+
+ protected override void OnModelCreating(DbModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .Property(e => e.LargePersonGroupId)
+ .IsUnicode(false);
+
+ modelBuilder.Entity()
+ .HasKey(l => new { l.PictureFileId, l.LargePersonGroupId });
+
+ modelBuilder.Entity()
+ .HasKey(l => l.Id);
+ }
+ }
+}
diff --git a/DatabaseLibrary/Data/PictureFile.cs b/DatabaseLibrary/Data/PictureFile.cs
new file mode 100644
index 0000000..838923a
--- /dev/null
+++ b/DatabaseLibrary/Data/PictureFile.cs
@@ -0,0 +1,36 @@
+namespace DatabaseLibrary.Data
+{
+ using Newtonsoft.Json;
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+
+ [Table("PictureFile")]
+ public partial class PictureFile
+ {
+ [Key]
+ [JsonProperty(PropertyName = "I")]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; set; }
+
+ [Required]
+ [JsonProperty(PropertyName = "F")]
+ public string FilePath { get; set; }
+
+ [JsonProperty(PropertyName = "A")]
+ public DateTime DateAdded { get; set; }
+
+ [JsonProperty(PropertyName = "T")]
+ public DateTime? DateTaken { get; set; }
+
+ [JsonProperty(PropertyName = "C")]
+ public bool IsConfirmed { get; set; }
+
+ //[JsonIgnore]
+ //public virtual ICollection PicturePersons { get; set; }
+
+ // public virtual ICollection PictureFileGroupLookups { get; set; }
+
+ }
+}
diff --git a/DatabaseLibrary/Data/PictureFileGroupLookup.cs b/DatabaseLibrary/Data/PictureFileGroupLookup.cs
new file mode 100644
index 0000000..d045649
--- /dev/null
+++ b/DatabaseLibrary/Data/PictureFileGroupLookup.cs
@@ -0,0 +1,31 @@
+namespace DatabaseLibrary.Data
+{
+ using Newtonsoft.Json;
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Data.Entity.Spatial;
+
+ [Table("PictureFileGroupLookup")]
+ public partial class PictureFileGroupLookup
+ {
+ [Key]
+ [Column(Order = 0)]
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ [JsonProperty(PropertyName = "I")]
+ public int PictureFileId { get; set; }
+
+ [Key]
+ [Column(Order = 1)]
+ [StringLength(50)]
+ [JsonProperty(PropertyName = "G")]
+ public string LargePersonGroupId { get; set; }
+
+ [JsonProperty(PropertyName = "S")]
+ public int ProcessingState { get; set; }
+
+ [JsonIgnore]
+ public virtual PictureFile PictureFile { get; set; }
+ }
+}
diff --git a/DatabaseLibrary/Data/PicturePerson.cs b/DatabaseLibrary/Data/PicturePerson.cs
new file mode 100644
index 0000000..60668e3
--- /dev/null
+++ b/DatabaseLibrary/Data/PicturePerson.cs
@@ -0,0 +1,44 @@
+namespace DatabaseLibrary.Data
+{
+ using Newtonsoft.Json;
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Data.Entity.Spatial;
+
+ [Table("PicturePerson")]
+ public partial class PicturePerson
+ {
+ [Key]
+ [JsonProperty(PropertyName = "I")]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; set; }
+
+ [JsonProperty(PropertyName = "F")]
+ public int PictureFileId { get; set; }
+
+ [Required]
+ [StringLength(50)]
+ [JsonProperty(PropertyName = "G")]
+ public string LargePersonGroupId { get; set; }
+
+ [JsonProperty(PropertyName = "P")]
+ public Guid? PersonId { get; set; }
+
+ [JsonProperty(PropertyName = "A")]
+ public DateTime DateAdded { get; set; }
+
+ [JsonProperty(PropertyName = "J")]
+ public string FaceJSON { get; set; }
+
+ [JsonProperty(PropertyName = "C")]
+ public bool IsConfirmed { get; set; }
+
+ [JsonIgnore]
+ public virtual PictureFile PictureFile { get; set; }
+
+ [JsonIgnore]
+ public virtual Person Person { get; set; }
+ }
+}
diff --git a/DatabaseLibrary/DataProviderManager.cs b/DatabaseLibrary/DataProviderManager.cs
new file mode 100644
index 0000000..215165e
--- /dev/null
+++ b/DatabaseLibrary/DataProviderManager.cs
@@ -0,0 +1,163 @@
+using DatabaseLibrary;
+using DatabaseLibrary.Data;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.IsolatedStorage;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DatabaseLibrary
+{
+ public class DataProviderManager
+ {
+ private readonly string _isolatedStorageDatabaseTypeFileName = "DatabaseType.txt";
+ private readonly string _isolatedStorageConnectionStringFileName = "ConnectionString.txt";
+ private readonly string _defaultConnectionString = @"data source=.\sqlexpress;initial catalog=PhotosDatabase;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework";
+
+ private static IDataProvider _currentSingleton = null;
+ private static DataSourceType? _dataSourceType;
+ private static string _connectionString;
+
+ private DataSourceType GetDatabaseTypeFromIsolatedStorage()
+ {
+ DataSourceType databaseType;
+
+ using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null))
+ {
+ try
+ {
+ using (var iStreamForEndpoint = new IsolatedStorageFileStream(_isolatedStorageDatabaseTypeFileName, FileMode.Open, isoStore))
+ {
+ using (var readerForEndpoint = new StreamReader(iStreamForEndpoint))
+ {
+ databaseType = (DataSourceType)Enum.Parse(typeof(DataSourceType),readerForEndpoint.ReadLine());
+ }
+ }
+ }
+ catch (FileNotFoundException)
+ {
+ databaseType = DataSourceType.LocalIsolatedStorage;
+ }
+ }
+ return databaseType;
+ }
+
+ private void SaveDatabaseTypeToIsolatedStorage(DataSourceType databaseType)
+ {
+ using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null))
+ {
+ using (var oStream = new IsolatedStorageFileStream(_isolatedStorageDatabaseTypeFileName, FileMode.Create, isoStore))
+ {
+ using (var writer = new StreamWriter(oStream))
+ {
+ writer.WriteLine(databaseType);
+ }
+ }
+ }
+ }
+
+ private void SaveConnectionStringToIsolatedStorage(string connectionString)
+ {
+ using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null))
+ {
+ using (var oStream = new IsolatedStorageFileStream(_isolatedStorageConnectionStringFileName, FileMode.Create, isoStore))
+ {
+ using (var writer = new StreamWriter(oStream))
+ {
+ writer.WriteLine(connectionString);
+ }
+ }
+ }
+ }
+
+ private string GetConnectionStringToIsolatedStorage()
+ {
+ string connectionString = null;
+
+ using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null))
+ {
+ try
+ {
+ using (var iStreamForEndpoint = new IsolatedStorageFileStream(_isolatedStorageConnectionStringFileName, FileMode.Open, isoStore))
+ {
+ using (var readerForEndpoint = new StreamReader(iStreamForEndpoint))
+ {
+ connectionString = readerForEndpoint.ReadLine();
+ }
+ }
+ }
+ catch (FileNotFoundException)
+ {
+ connectionString = null;
+ }
+ }
+ if (string.IsNullOrEmpty(connectionString))
+ {
+ connectionString = _defaultConnectionString;
+ }
+ return connectionString;
+ }
+
+ public DataSourceType DataSourceType
+ {
+ get
+ {
+ if (_dataSourceType == null)
+ {
+ _dataSourceType = GetDatabaseTypeFromIsolatedStorage();
+ }
+ return _dataSourceType.Value;
+ }
+ set
+ {
+ _dataSourceType = value;
+ SaveDatabaseTypeToIsolatedStorage(value);
+ }
+ }
+
+ public string ConnectionString
+ {
+ get
+ {
+ if (_connectionString == null)
+ {
+ _connectionString = GetConnectionStringToIsolatedStorage();
+ }
+ return _connectionString;
+ }
+ set
+ {
+ _connectionString = value;
+ SaveConnectionStringToIsolatedStorage(value);
+ }
+ }
+
+ public static IDataProvider Current
+ {
+ get
+ {
+ if (_currentSingleton == null)
+ {
+ switch(_dataSourceType)
+ {
+ case DataSourceType.SqlConnectionString:
+ _currentSingleton = new SqlDataProvider("name=PhotosDatabase");
+ break;
+ case DataSourceType.LocalIsolatedStorage:
+ _currentSingleton = new IsolatedStorageDataProvider();
+ break;
+ }
+ }
+
+ return _currentSingleton;
+ }
+ }
+
+ public void ReinitialiseDatabase()
+ {
+ _currentSingleton = null;
+ }
+ }
+}
diff --git a/DatabaseLibrary/DataSourceType.cs b/DatabaseLibrary/DataSourceType.cs
new file mode 100644
index 0000000..db9902f
--- /dev/null
+++ b/DatabaseLibrary/DataSourceType.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DatabaseLibrary
+{
+ public enum DataSourceType
+ {
+ [Description("Using an SQL connection string")]
+ SqlConnectionString = 1,
+ [Description("Using local Isolated Storage")]
+ LocalIsolatedStorage = 2
+ }
+}
diff --git a/DatabaseLibrary/DatabaseLibrary.csproj b/DatabaseLibrary/DatabaseLibrary.csproj
new file mode 100644
index 0000000..6f57512
--- /dev/null
+++ b/DatabaseLibrary/DatabaseLibrary.csproj
@@ -0,0 +1,71 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}
+ Library
+ Properties
+ DatabaseLibrary
+ DatabaseLibrary
+ v4.5.2
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\Photo-App\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll
+
+
+ ..\Photo-App\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll
+
+
+ ..\Photo-App\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DatabaseLibrary/IsolatedStorageDataProvider.cs b/DatabaseLibrary/IsolatedStorageDataProvider.cs
new file mode 100644
index 0000000..f649337
--- /dev/null
+++ b/DatabaseLibrary/IsolatedStorageDataProvider.cs
@@ -0,0 +1,106 @@
+using DatabaseLibrary.Data;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.IsolatedStorage;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DatabaseLibrary
+{
+ public class IsolatedStorageDataProvider : IDataProvider
+ {
+ private static IsolatedStorageDatabase _db;
+
+ public IsolatedStorageDataProvider()
+ {
+ _db = IsolatedStorageDatabase.GetInstance();
+ }
+
+ public void AddFile(PictureFile file, string groupId, int processingState)
+ {
+ var lastFile = _db.PictureFiles.LastOrDefault();
+ if (lastFile != null)
+ {
+ file.Id = lastFile.Id + 1; // Yuk, manual indexing rush
+ }
+ else
+ {
+ file.Id = 1; // index starting from 1
+ }
+
+ _db.PictureFiles.Add(file);
+ _db.SaveChanges();
+
+ _db.PictureFileGroupLookups.Add(new PictureFileGroupLookup { LargePersonGroupId = groupId, PictureFileId = file.Id, ProcessingState = processingState });
+ _db.SaveChanges();
+ }
+
+ public PictureFile GetFile(string path)
+ {
+ return _db.PictureFiles.Where(a => a.FilePath == path).SingleOrDefault();
+ }
+
+ public void AddPerson(PicturePerson person)
+ {
+ _db.PicturePersons.Add(person);
+ _db.SaveChanges();
+ }
+
+ public void RemoveFile(int fileId)
+ {
+ var dbFile = _db.PictureFiles.SingleOrDefault(a=>a.Id == fileId);
+ _db.PictureFiles.Remove(dbFile);
+ _db.SaveChanges();
+ }
+
+ public void RemovePerson(int personId, int fileId)
+ {
+ var dbPerson = _db.PicturePersons.SingleOrDefault(a => a.Id == fileId);
+ _db.PicturePersons.Remove(dbPerson);
+ _db.SaveChanges();
+ }
+
+ public int GetFileCountForPersonId(Guid personId)
+ {
+ var peops = _db.PicturePersons.Where(a => a.PersonId == personId);
+ return peops.Count();
+ }
+
+ public List GetFilesForPersonId(Guid personId)
+ {
+ var peops = _db.PicturePersons.Where(a => a.PersonId == personId).ToList();
+ foreach(var person in peops)
+ {
+ person.PictureFile = _db.PictureFiles.Find(a => a.Id == person.PictureFileId);
+ person.Person = _db.People.Find(a => a.PersonId == person.PersonId);
+ }
+
+ return peops;
+ }
+
+ public Person GetPerson(Guid personId)
+ {
+ return _db.People.Where(a => a.PersonId == personId).SingleOrDefault();
+ }
+
+ public void AddPerson(Guid personId, string name, string userData)
+ {
+ _db.People.Add(new Person { Name = name, PersonId = personId, UserData = userData });
+ _db.SaveChanges();
+ }
+
+ public void RemovePersonsForGroup(string groupId)
+ {
+ var persons = _db.PicturePersons.Where(a => a.LargePersonGroupId == groupId);
+ foreach(var p in persons)
+ {
+ _db.PicturePersons.Remove(p);
+ }
+ _db.SaveChanges();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/DatabaseLibrary/Properties/AssemblyInfo.cs b/DatabaseLibrary/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..c27f930
--- /dev/null
+++ b/DatabaseLibrary/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("DatabaseLibrary")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("DatabaseLibrary")]
+[assembly: AssemblyCopyright("Copyright © 2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("d7d7a8ca-8e2e-4ff3-9eb4-78de8a514332")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/DatabaseLibrary/SqlDataProvider.cs b/DatabaseLibrary/SqlDataProvider.cs
new file mode 100644
index 0000000..e73e60a
--- /dev/null
+++ b/DatabaseLibrary/SqlDataProvider.cs
@@ -0,0 +1,89 @@
+using DatabaseLibrary.Data;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DatabaseLibrary
+{
+ public class SqlDataProvider : IDataProvider
+ {
+ PhotosDatabase _db;
+
+ public SqlDataProvider(string connectionString)
+ {
+ _db = new PhotosDatabase(connectionString);
+ }
+
+ public void AddFile(PictureFile file, string groupId, int processingState)
+ {
+ var lookup = new PictureFileGroupLookup
+ {
+ LargePersonGroupId = groupId,
+ ProcessingState = processingState,
+ };
+
+ lookup.PictureFile = file;
+ _db.PictureFileGroupLookups.Add(lookup);
+ _db.SaveChanges();
+
+
+ //db.PictureFiles.Add(file);
+ //db.SaveChanges();
+ //db.PictureFileGroupLookups.Add(new PictureFileGroupLookup { LargePersonGroupId = groupId, PictureFileId = })
+ }
+
+ public PictureFile GetFile(string path)
+ {
+ return _db.PictureFiles.Where(a => a.FilePath == path).SingleOrDefault();
+ }
+
+ public void AddPerson(PicturePerson person)
+ {
+ _db.PicturePersons.Add(person);
+ _db.SaveChanges();
+ }
+
+ public void RemoveFile(int fileId)
+ {
+ var dbFile = _db.PictureFiles.Find(fileId);
+ _db.PictureFiles.Remove(dbFile);
+ _db.SaveChanges();
+ }
+
+ public void RemovePerson(int personId, int fileId)
+ {
+ var dbPerson = _db.PicturePersons.Find(fileId);
+ _db.PicturePersons.Remove(dbPerson);
+ _db.SaveChanges();
+ }
+
+ public int GetFileCountForPersonId(Guid personId)
+ {
+ return _db.PicturePersons.Where(a => a.PersonId == personId).Count();
+ }
+
+ public List GetFilesForPersonId(Guid personId)
+ {
+ return _db.PicturePersons.Where(a => a.PersonId == personId).ToList();
+ }
+
+ public Person GetPerson(Guid personId)
+ {
+ return _db.People.Where(a => a.PersonId == personId).SingleOrDefault();
+ }
+
+ public void AddPerson(Guid personId, string name, string userData)
+ {
+ _db.People.Add(new Person { Name = name, PersonId = personId, UserData = userData });
+ _db.SaveChanges();
+ }
+
+ public void RemovePersonsForGroup(string groupId)
+ {
+ _db.Database.ExecuteSqlCommand($"DELETE FROM PicturePerson WHERE LargePersonGroupId = '{groupId}'");
+ }
+ }
+}
diff --git a/DatabaseLibrary/packages.config b/DatabaseLibrary/packages.config
new file mode 100644
index 0000000..4b6f309
--- /dev/null
+++ b/DatabaseLibrary/packages.config
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Photo-App/App.config b/Photo-App/App.config
new file mode 100644
index 0000000..1ff1dc8
--- /dev/null
+++ b/Photo-App/App.config
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Photo-App/App.xaml b/Photo-App/App.xaml
new file mode 100644
index 0000000..f343856
--- /dev/null
+++ b/Photo-App/App.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/Photo-App/App.xaml.cs b/Photo-App/App.xaml.cs
new file mode 100644
index 0000000..f2de48a
--- /dev/null
+++ b/Photo-App/App.xaml.cs
@@ -0,0 +1,28 @@
+using Photo_Detect_Catalogue_Search_WPF_App.Helpers;
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Threading;
+
+namespace Photo_Detect_Catalogue_Search_WPF_App
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ private App()
+ {
+ this.DispatcherUnhandledException += App_DispatcherUnhandledException;
+ }
+
+ private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
+ {
+ FileTraceWriter.LogError(e.Exception, $"UnhandledException, {e.Exception.Message}");
+ }
+ }
+}
diff --git a/Photo-App/Assets/Microsoft-logo_rgb_c-gray.png b/Photo-App/Assets/Microsoft-logo_rgb_c-gray.png
new file mode 100644
index 0000000..7846308
Binary files /dev/null and b/Photo-App/Assets/Microsoft-logo_rgb_c-gray.png differ
diff --git a/Photo-App/Assets/default.jpg b/Photo-App/Assets/default.jpg
new file mode 100644
index 0000000..00e1df8
Binary files /dev/null and b/Photo-App/Assets/default.jpg differ
diff --git a/Photo-App/Controls/FaceIdentificationPage.xaml b/Photo-App/Controls/FaceIdentificationPage.xaml
new file mode 100644
index 0000000..93de7a7
--- /dev/null
+++ b/Photo-App/Controls/FaceIdentificationPage.xaml
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Photo-App/Controls/FaceIdentificationPage.xaml.cs b/Photo-App/Controls/FaceIdentificationPage.xaml.cs
new file mode 100644
index 0000000..394c4a6
--- /dev/null
+++ b/Photo-App/Controls/FaceIdentificationPage.xaml.cs
@@ -0,0 +1,732 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+// This is rewritten from the same named page in the other sample project.
+// This page loads the datasets of images for each person into the web service.
+// You should have at least 3-5 images of each person to begin.
+// Create a parent folder for the group, which has subfolders inside,
+// for each person in the group/family. The parent folder name should be the
+// group name "Jones Family", the sub folders should be the people names,
+// like "Jones-John" and "Jones-Jane", then the file names should be the
+// person name with 1,2,3, etc after. See the demo data folder, exampled below:
+//
+// ../Laker/Laker-Peter/Laker-Peter1.jpg
+// ../Laker/Laker-Peter/Laker-Peter2.jpg
+// ../Laker/Laker-Peter/Laker-Peter3.jpg
+// ../Laker/Laker-Sally/Laker-Sally1.jpg
+// ../Laker/Laker-Sally/Laker-Sally2.jpg
+
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Controls
+{
+ using System;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.ComponentModel;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Linq;
+ using System.Text;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Windows;
+ using System.Windows.Controls;
+ using ClientContract = Microsoft.ProjectOxford.Face.Contract;
+ using System.Windows.Media;
+ using Microsoft.ProjectOxford.Face.Contract;
+ using Microsoft.ProjectOxford.Face;
+ using Photo_Detect_Catalogue_Search_WPF_App.Helpers;
+ using Newtonsoft.Json.Serialization;
+
+ ///
+ /// Interaction logic for FaceDetection.xaml
+ ///
+ public partial class FaceIdentificationPage : Page, INotifyPropertyChanged
+ {
+ #region Fields
+
+ ///
+ /// Description dependency property
+ ///
+ public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register("Description", typeof(string), typeof(FaceIdentificationPage));
+
+ ///
+ /// Temporary group id for create person database.
+ ///
+ private static string sampleGroupId = Guid.NewGuid().ToString();
+
+ ///
+ /// Faces to identify
+ ///
+ private ObservableCollection _faces = new ObservableCollection();
+
+ ///
+ /// Person database
+ ///
+ private ObservableCollection _persons = new ObservableCollection();
+
+ ///
+ /// User picked image file
+ ///
+ private ImageSource _selectedFile;
+
+ ///
+ /// max concurrent process number for client query.
+ ///
+ private int _maxConcurrentProcesses;
+
+ private MainWindowLogTraceWriter _mainWindowLogTraceWriter;
+
+ #endregion Fields
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class
+ ///
+ public FaceIdentificationPage()
+ {
+ InitializeComponent();
+ _maxConcurrentProcesses = 4;
+
+ _mainWindowLogTraceWriter = new MainWindowLogTraceWriter();
+ }
+
+ #endregion Constructors
+
+ #region Events
+
+ ///
+ /// Implement INotifyPropertyChanged interface
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ #endregion Events
+
+ #region Properties
+
+ ///
+ /// Gets or sets description
+ ///
+ public string Description
+ {
+ get
+ {
+ return (string)GetValue(DescriptionProperty);
+ }
+
+ set
+ {
+ SetValue(DescriptionProperty, value);
+ }
+ }
+
+ ///
+ /// Gets or sets group id.
+ ///
+ public string GroupId
+ {
+ get
+ {
+ return sampleGroupId;
+ }
+
+ set
+ {
+ sampleGroupId = value;
+ }
+ }
+
+ ///
+ /// Gets constant maximum image size for rendering detection result
+ ///
+ public int MaxImageSize
+ {
+ get
+ {
+ return 300;
+ }
+ }
+
+ ///
+ /// Gets person database
+ ///
+ public ObservableCollection Persons
+ {
+ get
+ {
+ return _persons;
+ }
+ }
+
+ ///
+ /// Gets or sets user picked image file
+ ///
+ public ImageSource SelectedFile
+ {
+ get
+ {
+ return _selectedFile;
+ }
+
+ set
+ {
+ _selectedFile = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("SelectedFile"));
+ }
+ }
+ }
+
+ ///
+ /// Gets faces to identify
+ ///
+ public ObservableCollection TargetFaces
+ {
+ get
+ {
+ return _faces;
+ }
+ }
+
+ #endregion Properties
+
+ #region Methods
+
+ ///
+ /// Pick the root person database folder, to minimum the data preparation logic, the folder should be under following construction
+ /// Each person's image should be put into one folder named as the person's name
+ /// All person's image folder should be put directly under the root person database folder
+ ///
+ /// Event sender
+ /// Event argument
+ private async void FolderPicker_Click(object sender, RoutedEventArgs e)
+ {
+ bool groupExists = false;
+
+ MainWindow mainWindow = Window.GetWindow(this) as MainWindow;
+ string subscriptionKey = mainWindow._scenariosControl.SubscriptionKey;
+ string endpoint= mainWindow._scenariosControl.SubscriptionEndpoint;
+
+ var faceServiceClient = new FaceServiceClient(subscriptionKey,endpoint);
+
+ // Test whether the group already exists
+ try
+ {
+ MainWindow.Log("Request: Group {0} will be used to build a person database. Checking whether the group exists.", this.GroupId);
+
+ await faceServiceClient.GetLargePersonGroupAsync(this.GroupId);
+ groupExists = true;
+ MainWindow.Log("Response: Group {0} exists.", this.GroupId);
+ }
+ catch (FaceAPIException ex)
+ {
+ if (ex.ErrorCode != "LargePersonGroupNotFound")
+ {
+ MainWindow.Log("Response: {0}. {1}", ex.ErrorCode, ex.ErrorMessage);
+ return;
+ }
+ else
+ {
+ MainWindow.Log("Response: Group {0} did not exist previously.", this.GroupId);
+ }
+ }
+
+ if (groupExists)
+ {
+ var cleanGroup = System.Windows.MessageBox.Show(string.Format("Requires a clean up for group \"{0}\" before setting up a new person database. Click OK to proceed, group \"{0}\" will be cleared.", this.GroupId), "Warning", MessageBoxButton.OKCancel);
+ if (cleanGroup == MessageBoxResult.OK)
+ {
+ await faceServiceClient.DeleteLargePersonGroupAsync(this.GroupId);
+ this.GroupId = Guid.NewGuid().ToString();
+ }
+ else
+ {
+ return;
+ }
+ }
+
+ // Show folder picker
+ System.Windows.Forms.FolderBrowserDialog dlg = new System.Windows.Forms.FolderBrowserDialog();
+ var result = dlg.ShowDialog();
+
+ // Set the suggestion count is intent to minimum the data preparation step only,
+ // it's not corresponding to service side constraint
+ const int SuggestionCount = 15;
+
+ if (result == System.Windows.Forms.DialogResult.OK)
+ {
+ // User picked a root person database folder
+ // Clear person database
+ Persons.Clear();
+ TargetFaces.Clear();
+ SelectedFile = null;
+ IdentifyButton.IsEnabled = false;
+
+ // Call create large person group REST API
+ // Create large person group API call will failed if group with the same name already exists
+ MainWindow.Log("Request: Creating group \"{0}\"", this.GroupId);
+ try
+ {
+ await faceServiceClient.CreateLargePersonGroupAsync(this.GroupId, this.GroupId, dlg.SelectedPath);
+ MainWindow.Log("Response: Success. Group \"{0}\" created", this.GroupId);
+ }
+ catch (FaceAPIException ex)
+ {
+ MainWindow.Log("Response: {0}. {1}", ex.ErrorCode, ex.ErrorMessage);
+ return;
+ }
+
+ int processCount = 0;
+ bool forceContinue = false;
+
+ MainWindow.Log("Request: Preparing faces for identification, detecting faces in chosen folder.");
+
+ // Enumerate top level directories, each directory contains one person's images
+ int invalidImageCount = 0;
+ foreach (var dir in System.IO.Directory.EnumerateDirectories(dlg.SelectedPath))
+ {
+ var tasks = new List();
+ var tag = System.IO.Path.GetFileName(dir);
+ Person p = new Person();
+ p.PersonName = tag;
+
+ var faces = new ObservableCollection();
+ p.Faces = faces;
+
+ // Call create person REST API, the new create person id will be returned
+ MainWindow.Log("Request: Creating person \"{0}\"", p.PersonName);
+
+ p.PersonId = (await RetryHelper.OperationWithBasicRetryAsync(async () => await
+ faceServiceClient.CreatePersonInLargePersonGroupAsync(this.GroupId, p.PersonName, dir),
+ new[] { "RateLimitExceeded" },
+ traceWriter:_mainWindowLogTraceWriter
+ )).PersonId.ToString();
+
+ MainWindow.Log("Response: Success. Person \"{0}\" (PersonID:{1}) created", p.PersonName, p.PersonId);
+
+ string img;
+ // Enumerate images under the person folder, call detection
+ var imageList =
+ new ConcurrentBag(
+ Directory.EnumerateFiles(dir, "*.*", SearchOption.AllDirectories)
+ .Where(s => s.ToLower().EndsWith(".jpg") || s.ToLower().EndsWith(".png") || s.ToLower().EndsWith(".bmp") || s.ToLower().EndsWith(".gif")));
+
+ while (imageList.TryTake(out img))
+ {
+ tasks.Add(Task.Factory.StartNew(
+ async (obj) =>
+ {
+ var imgPath = obj as string;
+
+ using (var fStream = File.OpenRead(imgPath))
+ {
+ try
+ {
+ // Update person faces on server side
+ var persistFace = await faceServiceClient.AddPersonFaceInLargePersonGroupAsync(this.GroupId, Guid.Parse(p.PersonId), fStream, imgPath);
+ return new Tuple(imgPath, persistFace);
+ }
+ catch (FaceAPIException ex)
+ {
+ // if operation conflict, retry.
+ if (ex.ErrorCode.Equals("ConcurrentOperationConflict"))
+ {
+ MainWindow.Log("Concurrent operation conflict. Retrying.");
+ imageList.Add(imgPath);
+ return null;
+ }
+ // if operation cause rate limit exceed, retry.
+ else if (ex.ErrorCode.Equals("RateLimitExceeded"))
+ {
+ MainWindow.Log("Rate limit exceeded. Retrying.");
+ imageList.Add(imgPath);
+ return null;
+ }
+ else if (ex.ErrorMessage.Contains("more than 1 face in the image."))
+ {
+ Interlocked.Increment(ref invalidImageCount);
+ }
+ // Here we simply ignore all detection failure in this sample
+ // You may handle these exceptions by check the Error.Error.Code and Error.Message property for ClientException object
+ return new Tuple(imgPath, null);
+ }
+ }
+ },
+ img).Unwrap().ContinueWith((detectTask) =>
+ {
+ // Update detected faces for rendering
+ var detectionResult = detectTask?.Result;
+ if (detectionResult == null || detectionResult.Item2 == null)
+ {
+ return;
+ }
+
+ this.Dispatcher.Invoke(
+ new Action, string, ClientContract.AddPersistedFaceResult>(UIHelper.UpdateFace),
+ faces,
+ detectionResult.Item1,
+ detectionResult.Item2);
+ }));
+ if (processCount >= SuggestionCount && !forceContinue)
+ {
+ var continueProcess = System.Windows.Forms.MessageBox.Show("The images loaded have reached the recommended count, may take long time if proceed. Would you like to continue to load images?", "Warning", System.Windows.Forms.MessageBoxButtons.YesNo);
+ if (continueProcess == System.Windows.Forms.DialogResult.Yes)
+ {
+ forceContinue = true;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ if (tasks.Count >= _maxConcurrentProcesses || imageList.IsEmpty)
+ {
+ await Task.WhenAll(tasks);
+ tasks.Clear();
+ }
+ }
+
+ Persons.Add(p);
+ }
+ if (invalidImageCount > 0)
+ {
+ MainWindow.Log("Warning: more or less than one face is detected in {0} images, can not add to face list.", invalidImageCount);
+ }
+ MainWindow.Log("Response: Success. Total {0} faces are detected.", Persons.Sum(p => p.Faces.Count));
+
+ try
+ {
+ // Start train large person group
+ MainWindow.Log("Request: Training group \"{0}\"", this.GroupId);
+
+ await RetryHelper.VoidOperationWithBasicRetryAsync(() =>
+ faceServiceClient.TrainLargePersonGroupAsync(this.GroupId),
+ new[] { "RateLimitExceeded" },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ //await faceServiceClient.TrainLargePersonGroupAsync(this.GroupId);
+
+ // Wait until train completed
+ while (true)
+ {
+ await Task.Delay(1000);
+ var status = await faceServiceClient.GetLargePersonGroupTrainingStatusAsync(this.GroupId);
+ MainWindow.Log("Response: {0}. Group \"{1}\" training process is {2}", "Success", this.GroupId, status.Status);
+ if (status.Status != Status.Running)
+ {
+ break;
+ }
+ }
+ IdentifyButton.IsEnabled = true;
+ }
+ catch (FaceAPIException ex)
+ {
+ MainWindow.Log("Response: {0}. {1}", ex.ErrorCode, ex.ErrorMessage);
+ }
+ }
+ GC.Collect();
+ }
+
+ ///
+ /// Pick image, detect and identify all faces detected
+ ///
+ /// Event sender
+ /// Event arguments
+ private async void Identify_Click(object sender, RoutedEventArgs e)
+ {
+ // Show file picker
+ Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
+ dlg.DefaultExt = ".jpg";
+ dlg.Filter = "Image files(*.jpg, *.png, *.bmp, *.gif) | *.jpg; *.png; *.bmp; *.gif";
+ var result = dlg.ShowDialog();
+
+ if (result.HasValue && result.Value)
+ {
+ // User picked one image
+ // Clear previous detection and identification results
+ TargetFaces.Clear();
+ var pickedImagePath = dlg.FileName;
+ var renderingImage = UIHelper.LoadImageAppliedOrientation(pickedImagePath);
+ var imageInfo = UIHelper.GetImageInfoForRendering(renderingImage);
+ SelectedFile = renderingImage;
+
+ var sw = Stopwatch.StartNew();
+
+ MainWindow mainWindow = Window.GetWindow(this) as MainWindow;
+ string subscriptionKey = mainWindow._scenariosControl.SubscriptionKey;
+ string subscriptionEndpoint = mainWindow._scenariosControl.SubscriptionEndpoint;
+ var faceServiceClient = new FaceServiceClient(subscriptionKey, subscriptionEndpoint);
+
+ // Call detection REST API
+ using (var fStream = File.OpenRead(pickedImagePath))
+ {
+ try
+ {
+ var faces = await RetryHelper.OperationWithBasicRetryAsync(async () => await
+ faceServiceClient.DetectAsync(fStream),
+ new[] { "RateLimitExceeded" },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ //var faces = await faceServiceClient.DetectAsync(fStream);
+
+ // Convert detection result into UI binding object for rendering
+ foreach (var face in UIHelper.CalculateFaceRectangleForRendering(faces, MaxImageSize, imageInfo))
+ {
+ TargetFaces.Add(face);
+ }
+
+ MainWindow.Log("Request: Identifying {0} face(s) in group \"{1}\"", faces.Length, this.GroupId);
+
+ // Identify each face
+ // Call identify REST API, the result contains identified person information
+
+ var identifyResult = await RetryHelper.OperationWithBasicRetryAsync(async () => await
+ faceServiceClient.IdentifyAsync(faces.Select(ff => ff.FaceId).ToArray(), largePersonGroupId: this.GroupId),
+ new[] { "RateLimitExceeded" },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ //var identifyResult = await faceServiceClient.IdentifyAsync(faces.Select(ff => ff.FaceId).ToArray(), largePersonGroupId: this.GroupId);
+
+ for (int idx = 0; idx < faces.Length; idx++)
+ {
+ // Update identification result for rendering
+ var face = TargetFaces[idx];
+ var res = identifyResult[idx];
+ if (res.Candidates.Length > 0 && Persons.Any(p => p.PersonId == res.Candidates[0].PersonId.ToString()))
+ {
+ face.PersonName = Persons.Where(p => p.PersonId == res.Candidates[0].PersonId.ToString()).First().PersonName;
+ }
+ else
+ {
+ face.PersonName = "Unknown";
+ }
+ }
+
+ var outString = new StringBuilder();
+ foreach (var face in TargetFaces)
+ {
+ outString.AppendFormat("Face {0} is identified as {1}. ", face.FaceId, face.PersonName);
+ }
+
+ MainWindow.Log("Response: Success. {0}", outString);
+ }
+ catch (FaceAPIException ex)
+ {
+ MainWindow.Log("Response: {0}. {1}", ex.ErrorCode, ex.ErrorMessage);
+ }
+ }
+ }
+ GC.Collect();
+ }
+
+ #endregion Methods
+
+ #region Nested Types
+
+ ///
+ /// Identification result for UI binding
+ ///
+ public class IdentificationResult : INotifyPropertyChanged
+ {
+ #region Fields
+
+ ///
+ /// Face to identify
+ ///
+ private Face _faceToIdentify;
+
+ ///
+ /// Identified person's name
+ ///
+ private string _name;
+
+ #endregion Fields
+
+ #region Events
+
+ ///
+ /// Implement INotifyPropertyChanged interface
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ #endregion Events
+
+ #region Properties
+
+ ///
+ /// Gets or sets face to identify
+ ///
+ public Face FaceToIdentify
+ {
+ get
+ {
+ return _faceToIdentify;
+ }
+
+ set
+ {
+ _faceToIdentify = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("FaceToIdentify"));
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets identified person's name
+ ///
+ public string Name
+ {
+ get
+ {
+ return _name;
+ }
+
+ set
+ {
+ _name = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("Name"));
+ }
+ }
+ }
+
+ #endregion Properties
+ }
+
+ ///
+ /// Person structure for UI binding
+ ///
+ public class Person : INotifyPropertyChanged
+ {
+ #region Fields
+
+ ///
+ /// Person's faces from database
+ ///
+ private ObservableCollection _faces = new ObservableCollection();
+
+ ///
+ /// Person's id
+ ///
+ private string _personId;
+
+ ///
+ /// Person's name
+ ///
+ private string _personName;
+
+ #endregion Fields
+
+ #region Events
+
+ ///
+ /// Implement INotifyPropertyChanged interface
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ #endregion Events
+
+ #region Properties
+
+ ///
+ /// Gets or sets person's faces from database
+ ///
+ public ObservableCollection Faces
+ {
+ get
+ {
+ return _faces;
+ }
+
+ set
+ {
+ _faces = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("Faces"));
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets person's id
+ ///
+ public string PersonId
+ {
+ get
+ {
+ return _personId;
+ }
+
+ set
+ {
+ _personId = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("PersonId"));
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets person's name
+ ///
+ public string PersonName
+ {
+ get
+ {
+ return _personName;
+ }
+
+ set
+ {
+ _personName = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("PersonName"));
+ }
+ }
+ }
+
+ #endregion Properties
+ }
+
+ #endregion Nested Types
+ }
+}
\ No newline at end of file
diff --git a/Photo-App/Controls/ManageGroupsControl.xaml b/Photo-App/Controls/ManageGroupsControl.xaml
new file mode 100644
index 0000000..b97313c
--- /dev/null
+++ b/Photo-App/Controls/ManageGroupsControl.xaml
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Photo-App/Controls/ManageGroupsControl.xaml.cs b/Photo-App/Controls/ManageGroupsControl.xaml.cs
new file mode 100644
index 0000000..2c9e6c7
--- /dev/null
+++ b/Photo-App/Controls/ManageGroupsControl.xaml.cs
@@ -0,0 +1,552 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Controls
+{
+ using DatabaseLibrary;
+ using DatabaseLibrary.Data;
+ using Microsoft.ProjectOxford.Face;
+ using Microsoft.ProjectOxford.Face.Contract;
+ using Photo_Detect_Catalogue_Search_WPF_App.Helpers;
+ using Photo_Detect_Catalogue_Search_WPF_App.Models;
+ using System;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.ComponentModel;
+ using System.Globalization;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using System.Windows;
+ using System.Windows.Controls;
+ using System.Windows.Data;
+ using System.Windows.Input;
+
+ ///
+ /// Interaction logic for ManageGroupsControl.xaml
+ ///
+ public partial class ManageGroupsControl : UserControl, INotifyPropertyChanged
+ {
+ ///
+ /// The number of faces to pull
+ ///
+ private const int _numberOfFacesToPull = 5;
+
+ ///
+ /// The face service client
+ ///
+ private FaceServiceClient _faceServiceClient;
+
+ ///
+ /// The main window
+ ///
+ private MainWindow _mainWindow;
+
+ ///
+ /// The modal lock
+ ///
+ private bool _showLock;
+
+ ///
+ /// The database provider layer
+ ///
+ private IDataProvider _db = DataProviderManager.Current;
+
+ ///
+ /// max concurrent process number for client query.
+ ///
+ private int _maxConcurrentProcesses;
+
+ ///
+ /// The face groups
+ ///
+ private ObservableCollection _faceGroups = new ObservableCollection();
+
+ ///
+ /// The selected faces
+ ///
+ private ObservableCollection _selectedFaces
+ = new ObservableCollection();
+
+ ///
+ /// The selected group
+ ///
+ private LargePersonGroupExtended _selectedGroup;
+
+ ///
+ /// The main window log trace writer
+ ///
+ private MainWindowLogTraceWriter _mainWindowLogTraceWriter;
+
+
+
+ ///
+ /// Gets or sets a value indicating whether [show lock].
+ ///
+ ///
+ /// true if [show lock]; otherwise, false.
+ ///
+ public bool ShowLock
+ {
+ get
+ {
+ return _showLock;
+ }
+ set
+ {
+ _showLock = value;
+ PropertyChanged(this, new PropertyChangedEventArgs("ShowLock"));
+ }
+ }
+
+ public bool HasNoGroups
+ {
+ get
+ {
+ return FaceGroups == null || FaceGroups.Count == 0;
+ }
+ }
+
+ ///
+ /// Gets the face groups.
+ ///
+ ///
+ /// The face groups.
+ ///
+ public ObservableCollection FaceGroups
+ {
+ get
+ {
+ return _faceGroups;
+ }
+ }
+
+ ///
+ /// Gets or sets the selected group.
+ ///
+ ///
+ /// The selected group.
+ ///
+ public LargePersonGroupExtended SelectedGroup
+ {
+ get
+ {
+ return _selectedGroup;
+ }
+
+ set
+ {
+ _selectedGroup = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("SelectedGroup"));
+ //Task.Factory.StartNew(() =>
+ //{
+ GetPeopleForSelectedGroup().ConfigureAwait(false);
+ //});
+ }
+ }
+ }
+
+ ///
+ /// Checks the people in database.
+ ///
+ private void CheckPeopleInDatabase()
+ {
+ foreach(var person in SelectedGroup.GroupPersons)
+ {
+ var dbPerson = _db.GetPerson(person.Person.PersonId);
+ if (dbPerson == null)
+ {
+ _db.AddPerson(person.Person.PersonId, person.Person.Name, person.Person.UserData);
+ }
+ }
+ }
+
+ ///
+ /// Gets the people for selected group.
+ ///
+ ///
+ public async Task GetPeopleForSelectedGroup()
+ {
+ if (SelectedGroup == null)
+ {
+ return;
+ }
+
+ ShowLock = true;
+
+ MainWindow.Log("Loading group persons...");
+ SelectedGroup.GroupPersons.Clear();
+
+ Microsoft.ProjectOxford.Face.Contract.Person[] peops = null;
+
+ while(true)
+ {
+ try
+ {
+ // ListPersonsInLargePersonGroupAsync also has skip/take overrides
+ peops = await _faceServiceClient.ListPersonsInLargePersonGroupAsync(SelectedGroup.Group.LargePersonGroupId);
+ break;
+ }
+ catch (Exception e)
+ {
+ MainWindow.Log($"API rate limit exceeded, retrying");
+ await Task.Delay(1000);
+ }
+ }
+
+ if (peops == null)
+ {
+ MainWindow.Log($"failed to get Persons in group");
+ return;
+ }
+
+ foreach (var p in peops)
+ {
+ try
+ {
+ var person = new PersonExtended { Person = p };
+ person.PersonFilesDbCount = _db.GetFileCountForPersonId(p.PersonId);
+ this.Dispatcher.Invoke(() =>
+ {
+ SelectedGroup.GroupPersons.Add(person);
+ });
+
+ // Initially loading just one, to save on API calls
+ var guidList = new ConcurrentBag(person.Person.PersistedFaceIds.Take(1));
+
+ await GetFacesFromServerAsync(person, guidList);
+ }
+ catch (Exception exc)
+ {
+ MainWindow.Log($"Error: GetPeople: Failed to load {p.Name}");
+ }
+
+ }
+
+ MainWindow.Log("Finished loading group persons");
+ CheckPeopleInDatabase();
+
+ ShowLock = false;
+ }
+
+ ///
+ /// Gets trained faces from the API.
+ ///
+ /// The person.
+ /// The unique identifier list.
+ ///
+ private async Task GetFacesFromServerAsync(PersonExtended person, ConcurrentBag guidList)
+ {
+ var tasks = new List();
+ Guid guid;
+ while (guidList.TryTake(out guid))
+ {
+ tasks.Add(Task.Factory.StartNew((object inParams) =>
+ {
+ var prm = (Tuple)inParams;
+ try
+ {
+ var face = _faceServiceClient.GetPersonFaceInLargePersonGroupAsync(SelectedGroup.Group.LargePersonGroupId, prm.Item1.Person.PersonId, prm.Item2).Result;
+
+ this.Dispatcher.Invoke(
+ new Action, string, PersistedFace>(UIHelper.UpdateFace),
+ prm.Item1.Faces,
+ face.UserData,
+ face);
+ }
+ catch (FaceAPIException e)
+ {
+ // if operation conflict, retry.
+ if (e.ErrorCode.Equals("ConcurrentOperationConflict"))
+ {
+ guidList.Add(guid);
+ }
+ }
+ catch (Exception ex)
+ {
+ if (ex.InnerException != null && ex.InnerException.Message.Contains("not found"))
+ {
+ return;
+ }
+
+ guidList.Add(guid);
+ this.Dispatcher.Invoke(() =>
+ {
+ MainWindow.Log($"Service request limit exceeded (20/min) - Re-trying in 2 seconds");
+ Task.Delay(2000).Wait();
+ });
+ }
+ }, new Tuple(person, guid)));
+
+ if (tasks.Count >= _maxConcurrentProcesses || guidList.IsEmpty)
+ {
+ await Task.WhenAll(tasks);
+ tasks.Clear();
+ }
+ }
+
+ await Task.WhenAll(tasks);
+ tasks.Clear();
+
+ return;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ManageGroupsControl()
+ {
+ InitializeComponent();
+ _mainWindowLogTraceWriter = new MainWindowLogTraceWriter();
+ _maxConcurrentProcesses = 4;
+ Loaded += ManageGroupsControl_Loaded;
+ }
+
+ ///
+ /// Handles the Loaded event of the ManageGroupsControl control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void ManageGroupsControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ _mainWindow = Window.GetWindow(this) as MainWindow;
+ string subscriptionKey = _mainWindow._scenariosControl.SubscriptionKey;
+ string endpoint = _mainWindow._scenariosControl.SubscriptionEndpoint;
+
+ _faceServiceClient = new FaceServiceClient(subscriptionKey, endpoint);
+ await LoadGroups();
+ }
+ catch (Exception exc)
+ {
+ MainWindow.Log($"ManageGroupsControl_Loaded: {exc}");
+ MessageBox.Show($"Error loading Group Manager: {exc.Message}");
+ }
+ }
+
+ ///
+ /// Loads the groups.
+ ///
+ ///
+ private async Task LoadGroups()
+ {
+ FaceGroups.Clear();
+
+ var groups = await RetryHelper.OperationWithBasicRetryAsync(async () => await
+ _faceServiceClient.ListLargePersonGroupsAsync(),
+ new[] { "RateLimitExceeded" },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ foreach (var grp in groups)
+ {
+ FaceGroups.Add(new LargePersonGroupExtended { Group = grp });
+ }
+
+ PropertyChanged(this, new PropertyChangedEventArgs("HasNoGroups"));
+ MainWindow.Log("Found {0} groups.", groups.Length);
+ }
+
+ ///
+ /// Occurs when a property value changes.
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// Handles the Click event of the btnFolderScan control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void btnFolderScan_Click(object sender, RoutedEventArgs e)
+ {
+ var person = SelectedGroup.SelectedPerson;
+ var ctrl = new ScanFolderControl(_selectedGroup, _mainWindow, this);
+
+ grdMain.Children.Add(ctrl);
+
+ //var win = new PopupWindow(ctrl, $"Scan folders for matches with {_selectedGroup.Group.Name}");
+ //win.Show();
+ }
+
+ ///
+ /// Handles the Click event of the btnAddGroup control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void btnAddGroup_Click(object sender, RoutedEventArgs e)
+ {
+ var groupId = Guid.NewGuid().ToString();
+
+ //await faceServiceClient.CreateLargePersonGroupAsync(groupId, groupId);
+ await RetryHelper.VoidOperationWithBasicRetryAsync(() =>
+ _faceServiceClient.CreateLargePersonGroupAsync(groupId, groupId),
+ new[] { "RateLimitExceeded" },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ await LoadGroups();
+ SelectedGroup = FaceGroups.Where(a => a.Group.LargePersonGroupId == groupId).SingleOrDefault();
+ }
+
+ ///
+ /// Handles the Click event of the btnUpdateGroup control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void btnUpdateGroup_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ //await faceServiceClient.UpdateLargePersonGroupAsync(SelectedGroup.Group.LargePersonGroupId, SelectedGroup.Group.Name, SelectedGroup.Group.UserData);
+ await RetryHelper.VoidOperationWithBasicRetryAsync(() =>
+ _faceServiceClient.UpdateLargePersonGroupAsync(SelectedGroup.Group.LargePersonGroupId, SelectedGroup.Group.Name, SelectedGroup.Group.UserData),
+ new[] { "RateLimitExceeded" },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ MainWindow.Log($"Changes to the selected group were saved successfully");
+ }
+ catch (Exception ex)
+ {
+ MainWindow.Log($"Error updating group: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Handles the Click event of the btnDeleteGroup control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void btnDeleteGroup_Click(object sender, RoutedEventArgs e)
+ {
+ MessageBoxResult messageBoxResult = System.Windows.MessageBox.Show("Are you sure you want to delete this group and database matches?", "Delete Confirmation", System.Windows.MessageBoxButton.YesNo);
+ if (messageBoxResult == MessageBoxResult.Yes)
+ {
+ try
+ {
+ await RetryHelper.VoidOperationWithBasicRetryAsync(() =>
+ _faceServiceClient.DeleteLargePersonGroupAsync(SelectedGroup.Group.LargePersonGroupId),
+ new[] { "RateLimitExceeded" },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ _db.RemovePersonsForGroup(SelectedGroup.Group.LargePersonGroupId);
+
+ FaceGroups.Remove(SelectedGroup);
+ SelectedGroup = null;
+ MainWindow.Log($"Selected group deleted successfully");
+ }
+ catch (Exception ex)
+ {
+ MainWindow.Log($"Error deleting group: {ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Handles the Click event of the btnShowFiles control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void btnShowFiles_Click(object sender, RoutedEventArgs e)
+ {
+ var person = SelectedGroup.SelectedPerson;
+ if (person == null)
+ {
+ MessageBox.Show("You must first select a person from the group");
+ return; // To do. Should be CanExecute
+ }
+
+ var ctrl = new ShowPersonMatchedFilesControl(person);
+
+ grdMain.Children.Add(ctrl);
+
+ //var win = new PopupWindow(ctrl, $"Matched files for {person.Person.Name}");
+ //win.Show();
+ }
+
+ private async void btnShowMore_Click(object sender, RoutedEventArgs e)
+ {
+ var person = SelectedGroup.SelectedPerson;
+ if (person == null)
+ {
+ MessageBox.Show("You must first select a person from the group");
+ return; // To do. Should be CanExecute
+ }
+
+ var alreadyTaken = person.Faces.Count;
+ var maxAvailable = person.Person.PersistedFaceIds.Count();
+ if (alreadyTaken < maxAvailable)
+ {
+ var guidList = new ConcurrentBag(person.Person.PersistedFaceIds.Skip(alreadyTaken).Take(_numberOfFacesToPull));
+ await GetFacesFromServerAsync(person, guidList);
+ }
+ else
+ {
+ MainWindow.Log($"No more images stored in the service for {person.Person.Name}");
+ }
+ }
+
+ ///
+ /// Handles the MouseDown event of the Grid control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ foreach(var p in SelectedGroup.GroupPersons)
+ {
+ p.IsSelected = false;
+ }
+ var grid = sender as Grid;
+ var person = grid.DataContext as PersonExtended;
+ SelectedGroup.SelectedPerson = person;
+ person.IsSelected = true;
+ }
+
+ ///
+ /// Handles the Click event of the btnImportGroup control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void btnImportGroup_Click(object sender, RoutedEventArgs e)
+ {
+ _mainWindow.Dispatcher.Invoke(() =>
+ {
+ var listBox = (ListBox)_mainWindow._scenariosControl.FindName("_scenarioListBox");
+ listBox.SelectedIndex = 0;
+ });
+ }
+ }
+
+}
+
diff --git a/Photo-App/Controls/PopupWindow.xaml b/Photo-App/Controls/PopupWindow.xaml
new file mode 100644
index 0000000..c15ee1d
--- /dev/null
+++ b/Photo-App/Controls/PopupWindow.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/Photo-App/Controls/PopupWindow.xaml.cs b/Photo-App/Controls/PopupWindow.xaml.cs
new file mode 100644
index 0000000..d2c7806
--- /dev/null
+++ b/Photo-App/Controls/PopupWindow.xaml.cs
@@ -0,0 +1,51 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Controls
+{
+ using System.Windows;
+
+ ///
+ /// Interaction logic for PopupWindow.xaml
+ ///
+ public partial class PopupWindow : Window
+ {
+ public PopupWindow(FrameworkElement popupControl, string title)
+ {
+ InitializeComponent();
+ grdContent.Children.Add(popupControl);
+ Title = title;
+ }
+ }
+}
diff --git a/Photo-App/Controls/ScanFolderControl.xaml b/Photo-App/Controls/ScanFolderControl.xaml
new file mode 100644
index 0000000..d2e4cca
--- /dev/null
+++ b/Photo-App/Controls/ScanFolderControl.xaml
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Photo-App/Controls/ScanFolderControl.xaml.cs b/Photo-App/Controls/ScanFolderControl.xaml.cs
new file mode 100644
index 0000000..9393a07
--- /dev/null
+++ b/Photo-App/Controls/ScanFolderControl.xaml.cs
@@ -0,0 +1,1256 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Controls
+{
+ using DatabaseLibrary;
+ using DatabaseLibrary.Data;
+ using Microsoft.ProjectOxford.Face;
+ using Microsoft.ProjectOxford.Face.Contract;
+ using Photo_Detect_Catalogue_Search_WPF_App.Helpers;
+ using Photo_Detect_Catalogue_Search_WPF_App.Models;
+ using System;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.ComponentModel;
+ using System.IO;
+ using System.Linq;
+ using System.Text;
+ using System.Threading.Tasks;
+ using System.Windows;
+ using System.Windows.Controls;
+ using System.Windows.Input;
+ using System.Windows.Media;
+ using System.Windows.Media.Imaging;
+ using System.Windows.Shapes;
+
+ ///
+ /// Interaction logic for ScanFolderPage.xaml
+ ///
+ public partial class ScanFolderControl : UserControl, INotifyPropertyChanged
+ {
+ ///
+ /// The selected folder
+ ///
+ private string _selectedFolder;
+
+ ///
+ /// The files count
+ ///
+ private int _filesCount;
+
+ ///
+ /// The files
+ ///
+ private Queue _files;
+
+ ///
+ /// The can scan
+ ///
+ private bool _canScan;
+
+ ///
+ /// The scan group
+ ///
+ private LargePersonGroupExtended _scanGroup;
+
+ ///
+ /// The detected faces
+ ///
+ private ObservableCollection _detectedFaces = new ObservableCollection();
+
+ ///
+ /// The result collection
+ ///
+ private ObservableCollection _resultCollection = new ObservableCollection();
+
+ ///
+ /// The selected file
+ ///
+ private ImageSource _selectedFile;
+
+ ///
+ /// The selected file path
+ ///
+ private string _selectedFilePath;
+
+ ///
+ /// The is dragging
+ ///
+ private bool _isDragging;
+
+ ///
+ /// The select rectangle
+ ///
+ private Rectangle _selectRectangle;
+
+ ///
+ /// The select rectangle start point
+ ///
+ private Point _selectRectangleStartPoint;
+
+ ///
+ /// The database provider
+ ///
+ private IDataProvider _db = DataProviderManager.Current;
+
+ ///
+ /// The face service client
+ ///
+ private FaceServiceClient _faceServiceClient;
+
+ ///
+ /// The main window
+ ///
+ private MainWindow _mainWindow;
+
+ ///
+ /// The main window log trace writer
+ ///
+ private MainWindowLogTraceWriter _mainWindowLogTraceWriter;
+
+ ///
+ /// The modal lock
+ ///
+ private bool _showLock;
+
+ ///
+ /// The image rotate transform of the image
+ ///
+ private double _imageRotateTransform = 0;
+
+ ///
+ /// The zoom of the image
+ ///
+ private double _zoom = 1;
+
+ ///
+ /// The image translate x
+ ///
+ private double _imageTranslateX;
+
+ ///
+ /// The image translate y
+ ///
+ private double _imageTranslateY;
+
+ ///
+ /// The manage groups parent
+ ///
+ private ManageGroupsControl _manageGroupsParent;
+
+ ///
+ /// Gets or sets the image translate x.
+ ///
+ ///
+ /// The image translate x.
+ ///
+ public double ImageTranslateX
+ {
+ get { return _imageTranslateX; }
+ set
+ {
+ _imageTranslateX = value;
+ PropertyChanged(this, new PropertyChangedEventArgs("ImageTranslateX"));
+ }
+ }
+
+ ///
+ /// Gets or sets the image translate y.
+ ///
+ ///
+ /// The image translate y.
+ ///
+ public double ImageTranslateY
+ {
+ get { return _imageTranslateY; }
+ set
+ {
+ _imageTranslateY = value;
+ PropertyChanged(this, new PropertyChangedEventArgs("ImageTranslateY"));
+ }
+ }
+
+ ///
+ /// Gets or sets the zoom of the image.
+ ///
+ ///
+ /// The zoom of the image.
+ ///
+ public double Zoom
+ {
+ get { return _zoom; }
+ set
+ {
+ _zoom = value;
+ PropertyChanged(this, new PropertyChangedEventArgs("Zoom"));
+ }
+ }
+
+ ///
+ /// The left mouse is button pressed
+ ///
+ private bool _isLeftMouseButtonPressed;
+
+ ///
+ /// Gets or sets a value indicating whether this instance is left mouse button pressed.
+ ///
+ ///
+ /// true if this instance is left mouse button pressed; otherwise, false.
+ ///
+ public bool IsLeftMouseButtonPressed
+ {
+ get { return _isLeftMouseButtonPressed; }
+ set { _isLeftMouseButtonPressed = value; }
+ }
+
+
+ ///
+ /// Gets or sets the image rotate transform.
+ ///
+ ///
+ /// The image rotate transform.
+ ///
+ public double ImageRotateTransform
+ {
+ get { return _imageRotateTransform; }
+ set
+ {
+ _imageRotateTransform = value;
+ PropertyChanged(this, new PropertyChangedEventArgs("ImageRotateTransform"));
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether [show lock].
+ ///
+ ///
+ /// true if [show lock]; otherwise, false.
+ ///
+ public bool ShowLock
+ {
+ get { return _showLock; }
+ set
+ {
+ _showLock = value;
+ PropertyChanged(this, new PropertyChangedEventArgs("ShowLock"));
+ }
+ }
+
+ ///
+ /// Gets or sets the select rectangle.
+ ///
+ ///
+ /// The select rectangle.
+ ///
+ public Rectangle SelectRectangle
+ {
+ get
+ {
+ return _selectRectangle;
+ }
+
+ set
+ {
+ _selectRectangle = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("SelectRectangle"));
+ CanScan = true;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the files count.
+ ///
+ ///
+ /// The files count.
+ ///
+ public int FilesCount
+ {
+ get
+ {
+ return _filesCount;
+ }
+
+ set
+ {
+ _filesCount = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("FilesCount"));
+ CanScan = true;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the files.
+ ///
+ ///
+ /// The files.
+ ///
+ public Queue Files
+ {
+ get
+ {
+ return _files;
+ }
+
+ set
+ {
+ _files = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("Files"));
+ CanScan = true;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the seleceted folder.
+ ///
+ ///
+ /// The seleceted folder.
+ ///
+ public string SelecetedFolder
+ {
+ get
+ {
+ return _selectedFolder;
+ }
+
+ set
+ {
+ _selectedFolder = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("SelecetedFolder"));
+ CanScan = true;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this instance can scan.
+ ///
+ ///
+ /// true if this instance can scan; otherwise, false.
+ ///
+ public bool CanScan
+ {
+ get
+ {
+ return _canScan;
+ }
+
+ set
+ {
+ _canScan = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("CanScan"));
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the scan group.
+ ///
+ ///
+ /// The scan group.
+ ///
+ public LargePersonGroupExtended ScanGroup
+ {
+ get
+ {
+ return _scanGroup;
+ }
+
+ set
+ {
+ _scanGroup = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("ScanGroup"));
+ }
+ }
+ }
+
+ ///
+ /// Gets the detected faces.
+ ///
+ ///
+ /// The detected faces.
+ ///
+ public ObservableCollection DetectedFaces
+ {
+ get
+ {
+ return _detectedFaces;
+ }
+ }
+
+
+ ///
+ /// Gets or sets the selected file.
+ ///
+ ///
+ /// The selected file.
+ ///
+ public ImageSource SelectedFile
+ {
+ get
+ {
+ return _selectedFile;
+ }
+
+ set
+ {
+ _selectedFile = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("SelectedFile"));
+ }
+ }
+ }
+
+ ///
+ /// Gets the result collection.
+ ///
+ ///
+ /// The result collection.
+ ///
+ public ObservableCollection ResultCollection
+ {
+ get
+ {
+ return _resultCollection;
+ }
+ }
+
+ ///
+ /// Gets the maximum size of the image.
+ ///
+ ///
+ /// The maximum size of the image.
+ ///
+ public int MaxImageSize
+ {
+ get
+ {
+ return 300;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The group.
+ /// The main window.
+ public ScanFolderControl(LargePersonGroupExtended group, MainWindow mainWindow, ManageGroupsControl manageGroupsParent)
+ {
+ _manageGroupsParent = manageGroupsParent;
+ _scanGroup = group;
+ _mainWindow = mainWindow;
+ _mainWindowLogTraceWriter = new MainWindowLogTraceWriter();
+ InitializeComponent();
+ Loaded += ScanFolderControl_Loaded;
+ }
+
+ ///
+ /// Handles the Loaded event of the ScanFolderControl control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void ScanFolderControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ string subscriptionKey = _mainWindow._scenariosControl.SubscriptionKey;
+ string endpoint = _mainWindow._scenariosControl.SubscriptionEndpoint;
+
+ _faceServiceClient = new FaceServiceClient(subscriptionKey, endpoint);
+
+ await CheckGroupIsTrained();
+ }
+
+ ///
+ /// Occurs when a property value changes.
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// Handles the Click event of the BtnFolderSelect control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void BtnFolderSelect_Click(object sender, RoutedEventArgs e)
+ {
+ using (var dialog = new System.Windows.Forms.FolderBrowserDialog())
+ {
+ dialog.SelectedPath = SelecetedFolder;
+ System.Windows.Forms.DialogResult result = dialog.ShowDialog();
+
+ if (dialog.SelectedPath == null)
+ {
+ return;
+ }
+
+ SelecetedFolder = dialog.SelectedPath;
+ Files = new Queue(Directory.GetFiles(_selectedFolder)
+ .Where(s => s.EndsWith(".bmp", StringComparison.CurrentCultureIgnoreCase)
+ || s.EndsWith(".jpg", StringComparison.CurrentCultureIgnoreCase)
+ || s.EndsWith(".png", StringComparison.CurrentCultureIgnoreCase)
+ || s.EndsWith(".gif", StringComparison.CurrentCultureIgnoreCase)
+ || s.EndsWith(".jpeg", StringComparison.CurrentCultureIgnoreCase)));
+ FilesCount = Files.Count;
+ }
+ }
+
+ ///
+ /// Handles the Click event of the BtnScan control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void BtnScan_Click(object sender, RoutedEventArgs e)
+ {
+ await GetNextFile();
+ }
+
+ ///
+ /// Checks the group is trained, else trains it.
+ ///
+ ///
+ private async Task CheckGroupIsTrained()
+ {
+ // Start train large person group
+ MainWindow.Log("Request: Training group \"{0}\"", _scanGroup.Group.LargePersonGroupId);
+ await RetryHelper.VoidOperationWithBasicRetryAsync(() =>
+ _faceServiceClient.TrainLargePersonGroupAsync(_scanGroup.Group.LargePersonGroupId),
+ new[] { "RateLimitExceeded" },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ // Wait until train completed
+ while (true)
+ {
+ await Task.Delay(1000);
+
+ try // Temporary
+ {
+ var status = await _faceServiceClient.GetLargePersonGroupTrainingStatusAsync(_scanGroup.Group.LargePersonGroupId);
+ MainWindow.Log("Response: {0}. Group \"{1}\" training process is {2}", "Success", _scanGroup.Group.LargePersonGroupId, status.Status);
+ if (status.Status != Microsoft.ProjectOxford.Face.Contract.Status.Running)
+ {
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ MainWindow.Log($"Error: {ex.Message}");
+ // retry
+ }
+ }
+ }
+
+ ///
+ /// Gets the next file.
+ ///
+ private async Task GetNextFile()
+ {
+ ShowLock = true;
+
+ DetectedFaces.Clear();
+ btnNext.IsEnabled = false;
+
+ while (Files.Count > 0)
+ {
+ var file = Files.Dequeue();
+ var dbFile = _db.GetFile(file);
+ if (dbFile == null)
+ {
+ _selectedFilePath = file;
+ await ProcessFile(file);
+ break;
+ }
+ }
+
+ ShowLock = false;
+ if (Files.Count > 0)
+ {
+ MainWindow.Log("No more files in this folder to process");
+ }
+ }
+
+ ///
+ /// Processes the file.
+ ///
+ /// The file path.
+ private async Task ProcessFile(string filePath)
+ {
+ using (var fStream = File.OpenRead(filePath))
+ {
+ try
+ {
+ // Show the image to be analysed
+ var renderingImage = UIHelper.LoadImageAppliedOrientation(filePath);
+ var imageInfo = UIHelper.GetImageInfoForRendering(renderingImage);
+ SelectedFile = renderingImage;
+
+ var faces = await RetryHelper.OperationWithBasicRetryAsync(async () => await
+ _faceServiceClient.DetectAsync(fStream, false, true, new FaceAttributeType[] { FaceAttributeType.Gender, FaceAttributeType.Age, FaceAttributeType.Smile, FaceAttributeType.Glasses, FaceAttributeType.HeadPose, FaceAttributeType.FacialHair, FaceAttributeType.Emotion, FaceAttributeType.Hair, FaceAttributeType.Makeup, FaceAttributeType.Occlusion, FaceAttributeType.Accessories, FaceAttributeType.Noise, FaceAttributeType.Exposure, FaceAttributeType.Blur }),
+ new[] { "RateLimitExceeded" },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ MainWindow.Log("Response: Success. Detected {0} face(s) in {1}", faces.Length, filePath);
+
+ if (faces.Length == 0)
+ {
+ btnNext.IsEnabled = true;
+ return;
+ }
+
+ foreach (var face in faces)
+ {
+ DetectedFaces.Add(new Models.Face()
+ {
+ ImageFile = renderingImage,
+ Left = face.FaceRectangle.Left,
+ Top = face.FaceRectangle.Top,
+ Width = face.FaceRectangle.Width,
+ Height = face.FaceRectangle.Height,
+ FaceRectangle = new FaceRectangle { Height = face.FaceRectangle.Height, Width = face.FaceRectangle.Width, Left = face.FaceRectangle.Left, Top = face.FaceRectangle.Top },
+ FaceId = face.FaceId.ToString(),
+ Age = string.Format("{0:#} years old", face.FaceAttributes.Age),
+ Gender = face.FaceAttributes.Gender,
+ HeadPose = string.Format("Pitch: {0}, Roll: {1}, Yaw: {2}", Math.Round(face.FaceAttributes.HeadPose.Pitch, 2), Math.Round(face.FaceAttributes.HeadPose.Roll, 2), Math.Round(face.FaceAttributes.HeadPose.Yaw, 2)),
+ FacialHair = string.Format("FacialHair: {0}", face.FaceAttributes.FacialHair.Moustache + face.FaceAttributes.FacialHair.Beard + face.FaceAttributes.FacialHair.Sideburns > 0 ? "Yes" : "No"),
+ Glasses = string.Format("GlassesType: {0}", face.FaceAttributes.Glasses.ToString()),
+ Emotion = $"{GetEmotion(face.FaceAttributes.Emotion)}",
+ Hair = string.Format("Hair: {0}", GetHair(face.FaceAttributes.Hair)),
+ Makeup = string.Format("Makeup: {0}", ((face.FaceAttributes.Makeup.EyeMakeup || face.FaceAttributes.Makeup.LipMakeup) ? "Yes" : "No")),
+ EyeOcclusion = string.Format("EyeOccluded: {0}", ((face.FaceAttributes.Occlusion.EyeOccluded) ? "Yes" : "No")),
+ ForeheadOcclusion = string.Format("ForeheadOccluded: {0}", (face.FaceAttributes.Occlusion.ForeheadOccluded ? "Yes" : "No")),
+ MouthOcclusion = string.Format("MouthOccluded: {0}", (face.FaceAttributes.Occlusion.MouthOccluded ? "Yes" : "No")),
+ Accessories = $"{GetAccessories(face.FaceAttributes.Accessories)}",
+ Blur = string.Format("Blur: {0}", face.FaceAttributes.Blur.BlurLevel.ToString()),
+ Exposure = string.Format("{0}", face.FaceAttributes.Exposure.ExposureLevel.ToString()),
+ Noise = string.Format("Noise: {0}", face.FaceAttributes.Noise.NoiseLevel.ToString()),
+ });
+ }
+
+ // Convert detection result into UI binding object for rendering
+ foreach (var face in UIHelper.CalculateFaceRectangleForRendering(faces, MaxImageSize, imageInfo))
+ {
+ ResultCollection.Add(face);
+ }
+
+ await GoGetMatches();
+ }
+ catch (FaceAPIException ex)
+ {
+ MainWindow.Log("Response: {0}. {1}", ex.ErrorCode, ex.ErrorMessage);
+ GC.Collect();
+ return;
+ }
+ GC.Collect();
+ }
+
+ btnNext.IsEnabled = true;
+ }
+
+ ///
+ /// Goes the get matches.
+ ///
+ ///
+ private async Task GoGetMatches()
+ {
+ // Identify each face
+ // Call identify REST API, the result contains identified person information
+ //var identifyResult = await _faceServiceClient.IdentifyAsync(_detectedFaces.Select(ff => new Guid(ff.FaceId)).ToArray(), largePersonGroupId: this._scanGroup.Group.LargePersonGroupId);
+ var identifyResult = await RetryHelper.OperationWithBasicRetryAsync(async () => await
+ _faceServiceClient.IdentifyAsync(_detectedFaces.Select(ff => new Guid(ff.FaceId)).ToArray(), largePersonGroupId: this._scanGroup.Group.LargePersonGroupId),
+ new[] { "RateLimitExceeded" },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ for (int idx = 0; idx < _detectedFaces.Count; idx++)
+ {
+ // Update identification result for rendering
+ var face = DetectedFaces[idx];
+ var idResult = identifyResult[idx];
+ face.Identifications = idResult;
+ if (idResult.Candidates.Length > 0)
+ {
+ var pers = _scanGroup.GroupPersons.Where(p => p.Person.PersonId == idResult.Candidates[0].PersonId).First().Person;
+ face.PersonName = pers.Name;
+ face.PersonId = pers.PersonId;
+ face.PersonSourcePath = pers.UserData;
+ }
+ else
+ {
+ face.PersonName = "Unknown";
+ }
+ }
+
+ var outString = new StringBuilder();
+ foreach (var face in DetectedFaces)
+ {
+ outString.AppendFormat("Face {0} is identified as {1}. ", face.FaceId, face.PersonName);
+ }
+
+ btnSave.IsEnabled = DetectedFaces.Count > 0; // hack
+
+ MainWindow.Log("Response: Success. {0}", outString);
+
+ }
+
+ ///
+ /// Gets the hair.
+ ///
+ /// The hair.
+ ///
+ private string GetHair(Hair hair)
+ {
+ if (hair.HairColor.Length == 0)
+ {
+ if (hair.Invisible)
+ return "Invisible";
+ else
+ return "Bald";
+ }
+ else
+ {
+ HairColorType returnColor = HairColorType.Unknown;
+ double maxConfidence = 0.0f;
+
+ for (int i = 0; i < hair.HairColor.Length; ++i)
+ {
+ if (hair.HairColor[i].Confidence > maxConfidence)
+ {
+ maxConfidence = hair.HairColor[i].Confidence;
+ returnColor = hair.HairColor[i].Color;
+ }
+ }
+
+ return returnColor.ToString();
+ }
+ }
+
+ ///
+ /// Gets the accessories.
+ ///
+ /// The accessories.
+ ///
+ private string GetAccessories(Accessory[] accessories)
+ {
+ if (accessories.Length == 0)
+ {
+ return "NoAccessories";
+ }
+
+ string[] accessoryArray = new string[accessories.Length];
+
+ for (int i = 0; i < accessories.Length; ++i)
+ {
+ accessoryArray[i] = accessories[i].Type.ToString();
+ }
+
+ return "Accessories: " + String.Join(",", accessoryArray);
+ }
+
+ ///
+ /// Gets the emotion.
+ ///
+ /// The emotion.
+ ///
+ private string GetEmotion(Microsoft.ProjectOxford.Common.Contract.EmotionScores emotion)
+ {
+ string emotionType = string.Empty;
+ double emotionValue = 0.0;
+ if (emotion.Anger > emotionValue)
+ {
+ emotionValue = emotion.Anger;
+ emotionType = "Anger";
+ }
+ if (emotion.Contempt > emotionValue)
+ {
+ emotionValue = emotion.Contempt;
+ emotionType = "Contempt";
+ }
+ if (emotion.Disgust > emotionValue)
+ {
+ emotionValue = emotion.Disgust;
+ emotionType = "Disgust";
+ }
+ if (emotion.Fear > emotionValue)
+ {
+ emotionValue = emotion.Fear;
+ emotionType = "Fear";
+ }
+ if (emotion.Happiness > emotionValue)
+ {
+ emotionValue = emotion.Happiness;
+ emotionType = "Happiness";
+ }
+ if (emotion.Neutral > emotionValue)
+ {
+ emotionValue = emotion.Neutral;
+ emotionType = "Neutral";
+ }
+ if (emotion.Sadness > emotionValue)
+ {
+ emotionValue = emotion.Sadness;
+ emotionType = "Sadness";
+ }
+ if (emotion.Surprise > emotionValue)
+ {
+ emotionValue = emotion.Surprise;
+ emotionType = "Surprise";
+ }
+ return $"{emotionType}";
+ }
+
+ ///
+ /// Handles the MouseUp event of the imgCurrent control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void imgCurrent_MouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (_isDragging)
+ {
+ var scale = ((1 / imgCurrent.Source.Width) * imgCurrent.ActualWidth);
+ var face = new Models.Face { Height = (int)(SelectRectangle.Height / scale), Width = (int)(SelectRectangle.Width / scale), Left = (int)(_selectRectangleStartPoint.X / scale), Top = (int)(_selectRectangleStartPoint.Y / scale), ImageFile = SelectedFile };
+ DetectedFaces.Add(face);
+ canvDrag.Children.Clear();
+ }
+
+ IsLeftMouseButtonPressed = false;
+ _isDragging = false;
+ }
+
+ ///
+ /// Handles the Click event of the btnSave control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void btnSave_Click(object sender, RoutedEventArgs e)
+ {
+ ShowLock = true;
+
+ var file = new PictureFile { DateAdded = DateTime.Now, FilePath = _selectedFilePath, IsConfirmed = true };
+ _db.AddFile(file, _scanGroup.Group.LargePersonGroupId, 2);
+
+ var newFacesForTraining = false;
+ for (var ix = 0; ix < DetectedFaces.Count; ix++)
+ {
+ var face = DetectedFaces[ix];
+ var person = new PicturePerson
+ {
+ DateAdded = DateTime.Now,
+ PersonId = face.PersonId,
+ PictureFileId = file.Id,
+ LargePersonGroupId = _scanGroup.Group.LargePersonGroupId,
+ FaceJSON = Newtonsoft.Json.JsonConvert.SerializeObject(face),
+ IsConfirmed = true
+ };
+ _db.AddPerson(person);
+
+ if (face.AddToGroup)
+ {
+ var contentControl = face.ContextBinder.Parent as ContentControl;
+ var parentGrid = contentControl.Parent as Grid;
+ var croppedImage = parentGrid.Children[1] as Image;
+
+ var filePath = face.ImageFile.ToString().Replace("file:///", "");
+ filePath = filePath.Replace('\\', '/');
+
+ var fileName = $"DbId-{person.Id}_" + System.IO.Path.GetFileName(filePath); // unique file name, into training folder
+ var newFilePath = System.IO.Path.Combine(face.PersonSourcePath, fileName);
+
+ CropToSquare(filePath, newFilePath, face.Left, face.Top, face.Width, face.Height);
+
+ await AddFaceToLargePersonGroup(_scanGroup.Group.LargePersonGroupId, newFilePath, face.PersonId);
+
+ newFacesForTraining = true;
+ }
+ }
+
+ if (newFacesForTraining)
+ {
+ await CheckGroupIsTrained();
+ }
+
+ try
+ {
+ await GetNextFile();
+ }
+ catch (Exception exc)
+ {
+ MainWindow.Log("Error getting next file: " + exc.Message);
+ }
+ }
+
+ ///
+ /// Crops an image to square.
+ ///
+ /// The old filename.
+ /// Name of the file.
+ /// The left position.
+ /// The top position.
+ /// The width.
+ /// The height.
+ private void CropToSquare(string oldFilename, string fileName, int left, int top, int width, int height)
+ {
+ // Create a new image at the cropped size
+ System.Drawing.Bitmap cropped = new System.Drawing.Bitmap(width, height);
+
+ //Load image from file
+ using (System.Drawing.Image image = System.Drawing.Image.FromFile(oldFilename))
+ {
+ // Create a Graphics object to do the drawing, *with the new bitmap as the target*
+ using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(cropped))
+ {
+ // Draw the desired area of the original into the graphics object
+ g.DrawImage(image, new System.Drawing.Rectangle(0, 0, width, height), new System.Drawing.Rectangle(left, top, width, height), System.Drawing.GraphicsUnit.Pixel);
+ // Save the result
+ cropped.Save(fileName);
+ }
+ cropped.Dispose();
+ }
+ }
+
+ ///
+ /// Adds the face to a LargePersonGroup.
+ ///
+ /// The large person group identifier.
+ /// The img path.
+ /// The person identifier.
+ ///
+ private async Task AddFaceToLargePersonGroup(string largePersonGroupId, string imgPath, Guid PersonId)
+ {
+ var imageList = new ConcurrentBag(new [] { imgPath });
+
+ string img;
+ while (imageList.TryTake(out img))
+ {
+ using (var fStream = File.OpenRead(img))
+ {
+ try
+ {
+ //face.image.Save(m, image.RawFormat);
+ // Update person faces on server side
+ var persistFace = await _faceServiceClient.AddPersonFaceInLargePersonGroupAsync(largePersonGroupId, PersonId, fStream, img);
+ return;
+ }
+ catch (FaceAPIException ex)
+ {
+ // if operation conflict, retry.
+ if (ex.ErrorCode.Equals("ConcurrentOperationConflict"))
+ {
+ MainWindow.Log("Concurrent Operation Conflict. Re-queuing");
+ imageList.Add(img);
+ continue;
+ }
+ // if operation cause rate limit exceed, retry.
+ else if (ex.ErrorCode.Equals("RateLimitExceeded"))
+ {
+ imageList.Add(img);
+ MainWindow.Log("Rate Limit Exceeded. Re-queuing in 1 second");
+ await Task.Delay(1000);
+ continue;
+ }
+
+ MainWindow.Log($"Error: {ex.Message}");
+
+ // Here we simply ignore all detection failure in this sample
+ // You may handle these exceptions by check the Error.Error.Code and Error.Message property for ClientException object
+ return;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Handles the 1 event of the btnRemove_Click control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void btnRemove_Click(object sender, RoutedEventArgs e)
+ {
+ var ctrl = sender as Button;
+
+ var f = ctrl.DataContext as Models.Face;
+ DetectedFaces.Remove(f);
+ }
+
+ ///
+ /// Handles the Click event of the btnNext control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void btnNext_Click(object sender, RoutedEventArgs e)
+ {
+ await GetNextFile();
+ }
+
+ ///
+ /// Handles the Click event of the btnRotateAnti control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void btnRotateAnti_Click(object sender, RoutedEventArgs e)
+ {
+ ImageRotateTransform -= 22.5; // new RotateTransform { Angle = ImageRotateTransform.Angle - 22.5 };
+ }
+
+ ///
+ /// Handles the Click event of the btnRotateClock control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void btnRotateClock_Click(object sender, RoutedEventArgs e)
+ {
+ ImageRotateTransform += 22.5; // new RotateTransform { Angle = ImageRotateTransform.Angle - 22.5 };
+ }
+
+ ///
+ /// Retests the image.
+ ///
+ ///
+ private async Task RetestImage()
+ {
+ ShowLock = true;
+
+ var fileName = System.IO.Path.GetFileName(_selectedFilePath);
+ var folder = System.IO.Path.GetDirectoryName(_selectedFilePath);
+ var newFile = System.IO.Path.Combine(folder, Guid.NewGuid().ToString() + fileName);
+ SaveToBmp(imgCurrent, newFile);
+ await Task.Delay(200);
+
+ await ProcessFile(newFile);
+ await Task.Delay(200);
+
+ // load original again for user to see.
+ var renderingImage = UIHelper.LoadImageAppliedOrientation(_selectedFilePath);
+ var imageInfo = UIHelper.GetImageInfoForRendering(renderingImage);
+ SelectedFile = renderingImage;
+ await Task.Delay(200);
+
+ //File.Delete(newFile); // delete compare file
+
+ ShowLock = false;
+ }
+
+ ///
+ /// Saves to BMP.
+ ///
+ /// The visual.
+ /// Name of the file.
+ void SaveToBmp(FrameworkElement visual, string fileName)
+ {
+ var encoder = new BmpBitmapEncoder();
+ RenderTargetBitmap bitmap = new RenderTargetBitmap((int)visual.ActualWidth, (int)visual.ActualHeight, 96, 96, PixelFormats.Pbgra32);
+ bitmap.Render(visual);
+ BitmapFrame frame = BitmapFrame.Create(bitmap);
+ encoder.Frames.Add(frame);
+
+ using (var stream = File.Create(fileName))
+ {
+ encoder.Save(stream);
+ }
+ }
+
+
+ ///
+ /// Handles the MouseDown event of the imgCurrent control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void imgCurrent_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.RightButton == MouseButtonState.Pressed)
+ {
+ IsLeftMouseButtonPressed = true;
+ _selectRectangleStartPoint = e.GetPosition((IInputElement)sender);
+ return;
+ }
+ }
+
+ ///
+ /// Handles the MouseMove event of the imgCurrent control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void imgCurrent_MouseMove(object sender, MouseEventArgs e)
+ {
+ if (_isDragging)
+ {
+ var point = e.GetPosition((IInputElement)sender);
+ var width = point.X - _selectRectangleStartPoint.X;
+ var height = point.Y - _selectRectangleStartPoint.Y;
+
+ if (width == 0 || height == 0)
+ {
+ return;
+ }
+
+ if (width > 0)
+ {
+ SelectRectangle.Width = width;
+ }
+ else
+ {
+ _selectRectangleStartPoint.X += width;
+ SelectRectangle.SetValue(Canvas.LeftProperty, _selectRectangleStartPoint.X);
+ SelectRectangle.Width -= width;
+ }
+
+ if (height > 0)
+ {
+ SelectRectangle.Height = height;
+ }
+ else
+ {
+ _selectRectangleStartPoint.Y += height;
+ SelectRectangle.SetValue(Canvas.TopProperty, _selectRectangleStartPoint.Y);
+ SelectRectangle.Height -= height;
+ }
+ return;
+ }
+
+ if (_isLeftMouseButtonPressed)
+ {
+ var point = e.GetPosition((IInputElement)sender);
+ var xDelta = point.X - _selectRectangleStartPoint.X;
+ var yDelta = point.Y - _selectRectangleStartPoint.Y;
+ ImageTranslateX += xDelta;
+ ImageTranslateY += yDelta;
+ _selectRectangleStartPoint = new Point(point.X, point.Y);
+ }
+ }
+
+ ///
+ /// Handles the MouseLeave event of the imgCurrent control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void imgCurrent_MouseLeave(object sender, MouseEventArgs e)
+ {
+ _isDragging = false;
+ IsLeftMouseButtonPressed = false;
+ canvDrag.Children.Clear();
+ }
+
+ ///
+ /// Handles the MouseLeftButtonDown event of the canvDrag control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void canvDrag_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+
+ canvDrag.Children.Clear();
+ _isDragging = true;
+
+ _selectRectangleStartPoint = e.GetPosition((IInputElement)sender);
+ SelectRectangle = new Rectangle() { IsHitTestVisible = false, Width = 1, Height = 1, Stroke = new SolidColorBrush(Colors.Black), StrokeThickness = 1 };
+ SelectRectangle.SetValue(Canvas.LeftProperty, _selectRectangleStartPoint.X);
+ SelectRectangle.SetValue(Canvas.TopProperty, _selectRectangleStartPoint.Y);
+
+ canvDrag.Children.Add(SelectRectangle);
+ }
+
+ ///
+ /// Handles the MouseWheel event of the canvDrag control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void canvDrag_MouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ var delta = e.Delta;
+ Zoom *= delta > 0 ? 1.1 : 0.9;
+ }
+
+ ///
+ /// Handles the MouseLeftButtonUp event of the canvDrag control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void canvDrag_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ IsLeftMouseButtonPressed = false;
+ }
+
+ ///
+ /// Handles the Click event of the btnRecheck control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private async void btnRecheck_Click(object sender, RoutedEventArgs e)
+ {
+ await RetestImage();
+ }
+
+ private void btnOthers_Click(object sender, RoutedEventArgs e)
+ {
+ var ctrl = sender as Button;
+ var f = ctrl.DataContext as Models.Face;
+ f.IsShowingOthers = true;
+ }
+
+ private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ var ctrl = sender as ListView;
+
+ if (ctrl.SelectedItem != null)
+ {
+ var f = ctrl.DataContext as Models.Face;
+ var p = ctrl.SelectedItem as Candidate;
+ f.PersonId = p.PersonId;
+ f.IsShowingOthers = false;
+ Dispatcher.Invoke(() =>
+ {
+ ctrl.SelectedItem = null;
+ });
+ }
+ }
+
+ private void btnClose_Click(object sender, RoutedEventArgs e)
+ {
+ ((Grid)this.Parent).Children.Remove(this); // could dispose of better! :)
+ _manageGroupsParent.GetPeopleForSelectedGroup();
+ }
+ }
+}
diff --git a/Photo-App/Controls/ShowPersonMatchedFilesControl.xaml b/Photo-App/Controls/ShowPersonMatchedFilesControl.xaml
new file mode 100644
index 0000000..f0c2572
--- /dev/null
+++ b/Photo-App/Controls/ShowPersonMatchedFilesControl.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Photo-App/Controls/ShowPersonMatchedFilesControl.xaml.cs b/Photo-App/Controls/ShowPersonMatchedFilesControl.xaml.cs
new file mode 100644
index 0000000..8212bd3
--- /dev/null
+++ b/Photo-App/Controls/ShowPersonMatchedFilesControl.xaml.cs
@@ -0,0 +1,136 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Controls
+{
+ using DatabaseLibrary;
+ using DatabaseLibrary.Data;
+ using Photo_Detect_Catalogue_Search_WPF_App.Models;
+ using System.Collections.ObjectModel;
+ using System.ComponentModel;
+ using System.Windows;
+ using System.Windows.Controls;
+
+ ///
+ /// Interaction logic for ShowPersonMatchedFilesControl.xaml
+ ///
+ public partial class ShowPersonMatchedFilesControl : UserControl, INotifyPropertyChanged
+ {
+ ///
+ /// The person
+ ///
+ private PersonExtended _person;
+
+ ///
+ /// The database
+ ///
+ private IDataProvider _db = DataProviderManager.Current;
+
+ ///
+ /// The matched files
+ ///
+ private ObservableCollection _matchedFiles = new ObservableCollection();
+
+ ///
+ /// Occurs when a property value changes.
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// Gets or sets the matched files.
+ ///
+ ///
+ /// The matched files.
+ ///
+ public ObservableCollection MatchedFiles
+ {
+ get
+ {
+ return _matchedFiles;
+ }
+ set
+ {
+ _matchedFiles = value;
+ PropertyChanged(this, new PropertyChangedEventArgs("MatchedFiles"));
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The person.
+ public ShowPersonMatchedFilesControl(PersonExtended person)
+ {
+ _person = person;
+ InitializeComponent();
+ txtTitle.Text = $"Listing matched files for {person.Person.Name}";
+ Loaded += ShowPersonMatchedFilesControl_Loaded;
+ }
+
+ ///
+ /// Handles the Loaded event of the ShowPersonMatchedFilesControl control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void ShowPersonMatchedFilesControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ var people = _db.GetFilesForPersonId(_person.Person.PersonId);
+ foreach(var person in people)
+ {
+ // add value
+ MatchedFiles.Add(person);
+ }
+ }
+
+ ///
+ /// Handles the Click event of the btnOpenFileLocation control.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void btnOpenFileLocation_Click(object sender, RoutedEventArgs e)
+ {
+ var person = lstFiles.SelectedItem as PicturePerson;
+ var directory = System.IO.Path.GetDirectoryName(person.PictureFile.FilePath);
+
+ var runExplorer = new System.Diagnostics.ProcessStartInfo();
+ runExplorer.FileName = "explorer.exe";
+ runExplorer.Arguments = "/select,\"" + person.PictureFile.FilePath + "\"";
+ System.Diagnostics.Process.Start(runExplorer);
+ }
+
+ private void btnClose_Click(object sender, RoutedEventArgs e)
+ {
+ ((Grid)this.Parent).Children.Remove(this); // could dispose of better! :)
+ }
+ }
+}
diff --git a/Photo-App/Controls/SortMyPhotosPage.xaml b/Photo-App/Controls/SortMyPhotosPage.xaml
new file mode 100644
index 0000000..7f4eb29
--- /dev/null
+++ b/Photo-App/Controls/SortMyPhotosPage.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/Photo-App/Controls/SortMyPhotosPage.xaml.cs b/Photo-App/Controls/SortMyPhotosPage.xaml.cs
new file mode 100644
index 0000000..2b1ad5a
--- /dev/null
+++ b/Photo-App/Controls/SortMyPhotosPage.xaml.cs
@@ -0,0 +1,59 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+// SCENARIO: Photo Manager. Identify and tag photos automatically.
+//
+// This scenario combines several Face API features into a fully working example
+// of a photo store management tool. With this tool, you can catalog all your
+// photos and holiday snaps. Once the AI service is sufficiently trained, the
+// hope is that it can identify and tag up to 80% of images. The tool also
+// enables you to show all photos containing a certain person.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Controls
+{
+ using System.Windows.Controls;
+
+ ///
+ /// Interaction logic for SortMyPhotosPage.xaml
+ ///
+ public partial class SortMyPhotosPage : Page
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SortMyPhotosPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Photo-App/Controls/UIHelper.cs b/Photo-App/Controls/UIHelper.cs
new file mode 100644
index 0000000..d8dc6bf
--- /dev/null
+++ b/Photo-App/Controls/UIHelper.cs
@@ -0,0 +1,345 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Controls
+{
+ using Microsoft.ProjectOxford.Face.Contract;
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Drawing;
+ using System.IO;
+ using System.Linq;
+ using System.Windows;
+ using System.Windows.Media;
+ using System.Windows.Media.Imaging;
+
+ ///
+ /// UI helper functions
+ ///
+ internal static class UIHelper
+ {
+ #region Methods
+
+ ///
+ /// Rotate image by its orientation
+ ///
+ /// image path
+ /// image for rendering
+ public static BitmapImage LoadImageAppliedOrientation(string imagePath)
+ {
+ var im = new BitmapImage();
+
+ try
+ {
+ im.BeginInit();
+ im.UriSource = new Uri(imagePath, UriKind.RelativeOrAbsolute);
+ im.Rotation = GetImageOrientation(imagePath);
+ im.EndInit();
+ return im;
+ }
+ catch (Exception exc)
+ {
+ var writeableBitmap = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32,
+ new BitmapPalette(new List { Colors.LightGray }));
+
+ Bitmap bmp = BitmapFromWriteableBitmap(writeableBitmap);
+
+ Graphics g = Graphics.FromImage(bmp);
+ g.DrawString("FAILED", new Font("Tahoma", 8), System.Drawing.Brushes.White, new PointF(11, 25));
+
+ return BitmapToImageSource(bmp);
+ // return ConvertWriteableBitmapToBitmapImage(src);
+ }
+ }
+
+ ///
+ /// Bitmaps to image source.
+ ///
+ /// The bitmap.
+ ///
+ private static BitmapImage BitmapToImageSource(Bitmap bitmap)
+ {
+ using (MemoryStream memory = new MemoryStream())
+ {
+ bitmap.Save(memory, System.Drawing.Imaging.ImageFormat.Bmp);
+ memory.Position = 0;
+ BitmapImage bitmapimage = new BitmapImage();
+ bitmapimage.BeginInit();
+ bitmapimage.StreamSource = memory;
+ bitmapimage.CacheOption = BitmapCacheOption.OnLoad;
+ bitmapimage.EndInit();
+
+ return bitmapimage;
+ }
+ }
+
+ ///
+ /// Bitmaps from writeable bitmap.
+ ///
+ /// The write BMP.
+ ///
+ private static Bitmap BitmapFromWriteableBitmap(WriteableBitmap writeBmp)
+ {
+ Bitmap bmp;
+ using (MemoryStream outStream = new MemoryStream())
+ {
+ BitmapEncoder enc = new BmpBitmapEncoder();
+ enc.Frames.Add(BitmapFrame.Create((BitmapSource)writeBmp));
+ enc.Save(outStream);
+ bmp = new Bitmap(outStream);
+ }
+ return bmp;
+ }
+
+ ///
+ /// Get image orientation flag.
+ ///
+ /// image path
+ ///
+ public static Rotation GetImageOrientation(string imagePath)
+ {
+ using (var fs = new FileStream(imagePath, FileMode.Open, FileAccess.Read))
+ {
+ // See WIC Photo metadata policies for orientation query
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ee872007(v=vs.85).aspx
+ const string query = "System.Photo.Orientation";
+ var metadata = (BitmapMetadata)(BitmapFrame.Create(fs).Metadata);
+
+ if (metadata != null && metadata.ContainsQuery(query))
+ {
+ var orientationFlag = metadata.GetQuery(query);
+ if (orientationFlag != null)
+ {
+ switch ((ushort)orientationFlag)
+ {
+ case 6:
+ return Rotation.Rotate90;
+ case 3:
+ return Rotation.Rotate180;
+ case 8:
+ return Rotation.Rotate270;
+ }
+ }
+ }
+ }
+ return Rotation.Rotate0;
+ }
+
+ ///
+ /// Calculate the rendering face rectangle
+ ///
+ /// Detected face from service
+ /// Image rendering size
+ /// Image width and height
+ /// Face structure for rendering
+ public static IEnumerable CalculateFaceRectangleForRendering(IEnumerable faces, int maxSize, Tuple imageInfo)
+ {
+ var imageWidth = imageInfo.Item1;
+ var imageHeight = imageInfo.Item2;
+ float ratio = (float)imageWidth / imageHeight;
+ int uiWidth = 0;
+ int uiHeight = 0;
+ if (ratio > 1.0)
+ {
+ uiWidth = maxSize;
+ uiHeight = (int)(maxSize / ratio);
+ }
+ else
+ {
+ uiHeight = maxSize;
+ uiWidth = (int)(ratio * uiHeight);
+ }
+
+ int uiXOffset = (maxSize - uiWidth) / 2;
+ int uiYOffset = (maxSize - uiHeight) / 2;
+ float scale = (float)uiWidth / imageWidth;
+
+ foreach (var face in faces)
+ {
+ yield return new Models.Face()
+ {
+ FaceId = face.FaceId.ToString(),
+ Left = (int)((face.FaceRectangle.Left * scale) + uiXOffset),
+ Top = (int)((face.FaceRectangle.Top * scale) + uiYOffset),
+ Height = (int)(face.FaceRectangle.Height * scale),
+ Width = (int)(face.FaceRectangle.Width * scale),
+ FaceRectangle = face.FaceRectangle,
+ };
+ }
+ }
+
+ ///
+ /// Get image basic information for further rendering usage
+ ///
+ /// image file
+ /// Image width and height
+ public static Tuple GetImageInfoForRendering(BitmapImage imageFile)
+ {
+ try
+ {
+ return new Tuple(imageFile.PixelWidth, imageFile.PixelHeight);
+ }
+ catch
+ {
+ return new Tuple(0, 0);
+ }
+ }
+
+ ///
+ /// Append detected face to UI binding collection
+ ///
+ /// UI binding collection
+ /// Original image path, used for rendering face region
+ /// Face structure returned from service
+ public static void UpdateFace(ObservableCollection collections, string imagePath, Microsoft.ProjectOxford.Face.Contract.AddPersistedFaceResult face)
+ {
+ var renderingImage = LoadImageAppliedOrientation(imagePath);
+ collections.Add(new Models.Face()
+ {
+ ImageFile = renderingImage,
+ FaceId = face.PersistedFaceId.ToString(),
+ });
+ }
+ public static void UpdateFace(ObservableCollection collections, string imagePath, PersistedFace face)
+ {
+ var renderingImage = LoadImageAppliedOrientation(imagePath);
+ collections.Add(new Models.Face()
+ {
+ ImageFile = renderingImage,
+ FaceId = face.PersistedFaceId.ToString(),
+ });
+ }
+
+ ///
+ /// Append detected face to UI binding collection
+ ///
+ /// UI binding collection
+ /// Original image path, used for rendering face region
+ /// Face structure returned from service
+ public static void UpdateFace(ObservableCollection collections, string imagePath, Microsoft.ProjectOxford.Face.Contract.Face face)
+ {
+ var renderingImage = LoadImageAppliedOrientation(imagePath);
+ collections.Add(new Models.Face()
+ {
+ ImageFile = renderingImage,
+ Left = face.FaceRectangle.Left,
+ Top = face.FaceRectangle.Top,
+ Width = face.FaceRectangle.Width,
+ Height = face.FaceRectangle.Height,
+ FaceId = face.FaceId.ToString(),
+ });
+ }
+
+ ///
+ /// Logging helper function
+ ///
+ /// log output instance
+ /// message to append
+ /// log string
+ public static string AppendLine(this string log, string newMessage)
+ {
+ return string.Format("{0}[{3}]: {2}{1}", log, Environment.NewLine, newMessage, DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss"));
+ }
+
+ #endregion Methods
+ }
+
+ public static class ControlHelper
+ {
+ public static readonly DependencyProperty PassMouseWheelToParentProperty =
+ DependencyProperty.RegisterAttached("PassMouseWheelToParent", typeof(bool), typeof(UIElement),
+ new PropertyMetadata((bool)false));
+
+ public static void SetPassMouseWheelToParent(UIElement obj, bool value)
+ {
+ obj.SetValue(PassMouseWheelToParentProperty, value);
+ if (value)
+ {
+ obj.PreviewMouseWheel += Obj_PreviewMouseWheel;
+ }
+ else
+ {
+ obj.PreviewMouseWheel -= Obj_PreviewMouseWheel;
+ }
+ }
+
+ public static bool GetPassMouseWheelToParent(UIElement obj)
+ {
+ return (bool)obj.GetValue(PassMouseWheelToParentProperty);
+ }
+
+ private static void Obj_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
+ {
+ if (!e.Handled)
+ {
+ e.Handled = true;
+ var eventArg = new System.Windows.Input.MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
+ eventArg.RoutedEvent = UIElement.MouseWheelEvent;
+ eventArg.Source = sender;
+ var parent = FindVisualParent((DependencyObject)sender);
+ if (parent != null)
+ parent.RaiseEvent(eventArg);
+ }
+ }
+
+ ///
+ /// Finds a parent of a given item on the visual tree.
+ ///
+ /// The type of the queried item.
+ /// A direct or indirect child of the queried item.
+ /// The first parent item that matches the submitted type parameter.
+ /// If not matching item can be found, a null reference is being returned.
+ public static T FindVisualParent(DependencyObject child)
+ where T : DependencyObject
+ {
+ // get parent item
+ DependencyObject parentObject = VisualTreeHelper.GetParent(child);
+
+ // we’ve reached the end of the tree
+ if (parentObject == null) return null;
+
+ // check if the parent matches the type we’re looking for
+ T parent = parentObject as T;
+ if (parent != null)
+ {
+ return parent;
+ }
+ else
+ {
+ // use recursion to proceed with next level
+ return FindVisualParent(parentObject);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Photo-App/Data/IDataProvider.cs b/Photo-App/Data/IDataProvider.cs
new file mode 100644
index 0000000..623b311
--- /dev/null
+++ b/Photo-App/Data/IDataProvider.cs
@@ -0,0 +1,64 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Data
+{
+ interface IDataProvider
+ {
+ void AddFile(PictureFile file, string groupId);
+
+ PictureFile GetFile(string path);
+
+ void RemoveFile(int fileId); // Must also remove any persons associated with the file
+
+ void AddPerson(PicturePerson person);
+
+ void RemovePerson(int personId, int fileId);
+
+ int GetFileCountForPersonId(Guid personId);
+
+ Person GetPerson(Guid personId);
+
+ void AddPerson(Guid personId, string name, string userData);
+
+ void RemovePersonsForGroup(string groupId);
+
+ List GetFilesForPersonId(Guid personId);
+ }
+}
diff --git a/Photo-App/Data/Person.cs b/Photo-App/Data/Person.cs
new file mode 100644
index 0000000..e273628
--- /dev/null
+++ b/Photo-App/Data/Person.cs
@@ -0,0 +1,55 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Data
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Data.Entity.Spatial;
+
+ [Table("Person")]
+ public partial class Person
+ {
+ public Guid PersonId { get; set; }
+
+ [Required]
+ [StringLength(300)]
+ public string Name { get; set; }
+
+ public string UserData { get; set; }
+
+ public virtual ICollection PicturePersons { get; set; }
+ }
+}
diff --git a/Photo-App/Data/PhotosDatabase.cs b/Photo-App/Data/PhotosDatabase.cs
new file mode 100644
index 0000000..8193a4d
--- /dev/null
+++ b/Photo-App/Data/PhotosDatabase.cs
@@ -0,0 +1,60 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Data
+{
+ using System;
+ using System.Data.Entity;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Linq;
+
+ public partial class PhotosDatabase : DbContext
+ {
+ public PhotosDatabase()
+ : base("name=PhotosDatabase")
+ {
+ }
+
+ public virtual DbSet People { get; set; }
+ public virtual DbSet PictureFiles { get; set; }
+ public virtual DbSet PictureFileGroupLookups { get; set; }
+ public virtual DbSet PicturePersons { get; set; }
+
+ protected override void OnModelCreating(DbModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .Property(e => e.LargePersonGroupId)
+ .IsUnicode(false);
+ }
+ }
+}
diff --git a/Photo-App/Data/PictureFile.cs b/Photo-App/Data/PictureFile.cs
new file mode 100644
index 0000000..71e6148
--- /dev/null
+++ b/Photo-App/Data/PictureFile.cs
@@ -0,0 +1,61 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Data
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Data.Entity.Spatial;
+
+ [Table("PictureFile")]
+ public partial class PictureFile
+ {
+ public int Id { get; set; }
+
+ [Required]
+ public string FilePath { get; set; }
+
+ public DateTime DateAdded { get; set; }
+
+ public DateTime? DateTaken { get; set; }
+
+ public bool IsConfirmed { get; set; }
+
+ public virtual ICollection PicturePersons { get; set; }
+
+ public virtual ICollection PictureFileGroupLookups { get; set; }
+
+ }
+}
diff --git a/Photo-App/Data/PictureFileGroupLookup.cs b/Photo-App/Data/PictureFileGroupLookup.cs
new file mode 100644
index 0000000..db3a811
--- /dev/null
+++ b/Photo-App/Data/PictureFileGroupLookup.cs
@@ -0,0 +1,59 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Data
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Data.Entity.Spatial;
+
+ [Table("PictureFileGroupLookup")]
+ public partial class PictureFileGroupLookup
+ {
+ [Key]
+ [Column(Order = 0)]
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public int PictureFileId { get; set; }
+
+ [Key]
+ [Column(Order = 1)]
+ [StringLength(50)]
+ public string LargePersonGroupId { get; set; }
+
+ public int ProcessingState { get; set; }
+
+ public virtual PictureFile PictureFile { get; set; }
+ }
+}
diff --git a/Photo-App/Data/PicturePerson.cs b/Photo-App/Data/PicturePerson.cs
new file mode 100644
index 0000000..ac554b3
--- /dev/null
+++ b/Photo-App/Data/PicturePerson.cs
@@ -0,0 +1,64 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Data
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Data.Entity.Spatial;
+
+ [Table("PicturePerson")]
+ public partial class PicturePerson
+ {
+ public int Id { get; set; }
+
+ public int PictureFileId { get; set; }
+
+ [Required]
+ [StringLength(50)]
+ public string LargePersonGroupId { get; set; }
+
+ public Guid? PersonId { get; set; }
+
+ public DateTime DateAdded { get; set; }
+
+ public string FaceJSON { get; set; }
+
+ public bool IsConfirmed { get; set; }
+
+ public virtual PictureFile PictureFile { get; set; }
+ public virtual Person Person { get; set; }
+ }
+}
diff --git a/Photo-App/Data/SqlDataProvider.cs b/Photo-App/Data/SqlDataProvider.cs
new file mode 100644
index 0000000..ef7f4ee
--- /dev/null
+++ b/Photo-App/Data/SqlDataProvider.cs
@@ -0,0 +1,116 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.ProjectOxford.Face.Contract;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Data
+{
+ public class SqlDataProvider : IDataProvider
+ {
+ PhotosDatabase db = new PhotosDatabase();
+
+ public void AddFile(PictureFile file, string groupId)
+ {
+ var lookup = new PictureFileGroupLookup
+ {
+ LargePersonGroupId = groupId,
+ ProcessingState = 2,
+ };
+ lookup.PictureFile = file;
+ db.PictureFileGroupLookups.Add(lookup);
+ db.SaveChanges();
+
+
+ //db.PictureFiles.Add(file);
+ //db.SaveChanges();
+ //db.PictureFileGroupLookups.Add(new PictureFileGroupLookup { LargePersonGroupId = groupId, PictureFileId = })
+ }
+
+ public PictureFile GetFile(string path)
+ {
+ return db.PictureFiles.Where(a => a.FilePath == path).SingleOrDefault();
+ }
+
+ public void AddPerson(PicturePerson person)
+ {
+ db.PicturePersons.Add(person);
+ db.SaveChanges();
+ }
+
+ public void RemoveFile(int fileId)
+ {
+ var dbFile = db.PictureFiles.Find(fileId);
+ db.PictureFiles.Remove(dbFile);
+ db.SaveChanges();
+ }
+
+ public void RemovePerson(int personId, int fileId)
+ {
+ var dbPerson = db.PicturePersons.Find(fileId);
+ db.PicturePersons.Remove(dbPerson);
+ db.SaveChanges();
+ }
+
+ public int GetFileCountForPersonId(Guid personId)
+ {
+ return db.PicturePersons.Where(a => a.PersonId == personId).Count();
+ }
+
+ public List GetFilesForPersonId(Guid personId)
+ {
+ return db.PicturePersons.Where(a => a.PersonId == personId).ToList();
+ }
+
+ public Person GetPerson(Guid personId)
+ {
+ return db.People.Where(a => a.PersonId == personId).SingleOrDefault();
+ }
+
+ public void AddPerson(Guid personId, string name, string userData)
+ {
+ db.People.Add(new Person { Name = name, PersonId = personId, UserData = userData });
+ db.SaveChanges();
+ }
+
+ public void RemovePersonsForGroup(string groupId)
+ {
+ db.Database.ExecuteSqlCommand($"DELETE FROM PicturePerson WHERE LargePersonGroupId = '{groupId}'");
+ }
+ }
+}
diff --git a/Photo-App/Helpers/FileTraceWriter.cs b/Photo-App/Helpers/FileTraceWriter.cs
new file mode 100644
index 0000000..4858a12
--- /dev/null
+++ b/Photo-App/Helpers/FileTraceWriter.cs
@@ -0,0 +1,158 @@
+using Newtonsoft.Json.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Helpers
+{
+ public class FileTraceWriter : ITraceWriter
+ {
+ static string LogFileName = "IotHub_Log.txt";
+ const string LogFolderName = "Logs";
+
+ public static string LogFilePath { get; set; }
+
+ public TraceLevel LevelFilter => throw new NotImplementedException();
+
+ static int timeout = 1000;
+
+ public static bool LoggingEnabled = true;
+ private static ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
+
+ private static FileTraceWriter _singleton;
+ private static TraceLevel _levelFilter;
+
+ private FileTraceWriter()
+ {
+ Initialise();
+ }
+
+ internal static FileTraceWriter GetInstance()
+ {
+ if (_singleton == null)
+ {
+ _singleton = new FileTraceWriter();
+ }
+ return _singleton;
+ }
+
+ public static void Initialise(TraceLevel levelFilter = TraceLevel.Verbose)
+ {
+ _levelFilter = levelFilter;
+ LogFilePath = System.IO.Path.Combine(Environment.GetFolderPath(
+ Environment.SpecialFolder.MyDocuments), "PhotoRecognizer");
+
+ if (!Directory.Exists(LogFilePath))
+ {
+ Directory.CreateDirectory(LogFilePath);
+ }
+
+ LogFileName = "Log_" + GetTimeStamp() + ".txt";
+ LogFilePath = Path.Combine(LogFilePath, LogFileName);
+
+ if (!File.Exists(LogFilePath))
+ {
+ using (StreamWriter sw = File.CreateText(LogFilePath))
+ {
+ sw.WriteLine($"{DateTime.Now}: File created");
+ }
+ }
+ }
+
+ private static string GetTimeStamp()
+ {
+ return DateTime.Now.ToString("yyyyMMdd");
+ }
+
+ public static void LogError(Exception exc, string message,
+ [CallerMemberName] string memberName = "",
+ [CallerFilePath] string sourceFilePath = "",
+ [CallerLineNumber] int sourceLineNumber = 0)
+ {
+ if (exc == null)
+ {
+ Debug.WriteLine("Error, but no exception to process");
+ return;
+ }
+
+ string errMsg = message;
+ try
+ {
+ errMsg = $"ERROR: {DateTime.Now}: {memberName}: Line {sourceLineNumber}: {exc}{Environment.NewLine}";
+ }
+ catch (Exception ex)
+ {
+ errMsg = "ERROR, Error: " + ex.Message;
+ }
+
+ Debug.WriteLine(errMsg);
+
+ if (!LoggingEnabled)
+ return;
+
+ try
+ {
+ if (cacheLock.TryEnterWriteLock(timeout))
+ {
+ if (LogFilePath == null)
+ FileTraceWriter.Initialise();
+
+ using (StreamWriter sw = File.AppendText(LogFilePath))
+ {
+ sw.WriteLine(errMsg);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // to do
+ }
+ }
+
+ public static void LogMessage(string Message,
+ [CallerMemberName] string memberName = "",
+ [CallerFilePath] string sourceFilePath = "",
+ [CallerLineNumber] int sourceLineNumber = 0)
+ {
+ Debug.WriteLine(Message);
+
+ if (!LoggingEnabled)
+ return;
+
+ try
+ {
+ if (cacheLock.TryEnterWriteLock(timeout))
+ {
+ if (LogFilePath == null)
+ FileTraceWriter.Initialise();
+
+ using (StreamWriter sw = File.AppendText(LogFilePath))
+ {
+ sw.WriteLine($"{DateTime.Now}: {memberName}: Line {sourceLineNumber}: " + Message);
+ }
+ }
+ }
+ catch (Exception exc)
+ {
+ // to do
+ }
+ }
+
+ public void Trace(TraceLevel level, string message, Exception ex)
+ {
+ if (ex != null)
+ {
+ LogError(ex, message);
+ }
+
+ LogMessage(message);
+ }
+ }
+
+}
diff --git a/Photo-App/Helpers/MainWindowLogTraceWriter.cs b/Photo-App/Helpers/MainWindowLogTraceWriter.cs
new file mode 100644
index 0000000..5b1cde2
--- /dev/null
+++ b/Photo-App/Helpers/MainWindowLogTraceWriter.cs
@@ -0,0 +1,26 @@
+namespace Photo_Detect_Catalogue_Search_WPF_App.Helpers
+{
+ using Newtonsoft.Json.Serialization;
+ using System;
+ using System.Diagnostics;
+
+ class MainWindowLogTraceWriter : ITraceWriter
+ {
+ private TraceLevel _levelFilter;
+
+ public MainWindowLogTraceWriter(TraceLevel levelFilter = TraceLevel.Verbose)
+ {
+ _levelFilter = levelFilter;
+ }
+
+ public TraceLevel LevelFilter
+ {
+ get { return _levelFilter; }
+ }
+
+ public void Trace(TraceLevel level, string message, Exception ex)
+ {
+ MainWindow.Log(message);
+ }
+ }
+}
diff --git a/Photo-App/Helpers/RetryHelper.cs b/Photo-App/Helpers/RetryHelper.cs
new file mode 100644
index 0000000..fd558bb
--- /dev/null
+++ b/Photo-App/Helpers/RetryHelper.cs
@@ -0,0 +1,165 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Helpers
+{
+ using Microsoft.ProjectOxford.Face;
+ using Newtonsoft.Json.Serialization;
+ using System;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ ///
+ /// This class retries on transient errors
+ ///
+ public class RetryHelper
+ {
+ ///
+ /// The singleton
+ ///
+ static RetryHelper _singleton;
+
+ ///
+ /// Prevents a default instance of the class from being created.
+ ///
+ private RetryHelper(){}
+
+ ///
+ /// Gets the current.
+ ///
+ ///
+ /// The current.
+ ///
+ public static RetryHelper Current
+ {
+ get
+ {
+ if (_singleton == null)
+ {
+ _singleton = new RetryHelper();
+ }
+ return _singleton;
+ }
+ }
+
+ ///
+ /// Operations the with basic retry asynchronous.
+ ///
+ ///
+ /// The asynchronous operation.
+ /// The transient exception types.
+ /// The retry delay milliseconds.
+ /// The maximum retries.
+ /// The trace writer.
+ ///
+ public static async Task OperationWithBasicRetryAsync(Func> asyncOperation, string[] transientExceptionCodes, int retryDelayMilliseconds = 1000, int maxRetries = 60, ITraceWriter traceWriter = null)
+ {
+ int retryCount = 0;
+
+ while (true)
+ {
+ try
+ {
+ return await asyncOperation();
+ }
+ catch (FaceAPIException ex)
+ when (IsTransientError(ex.ErrorCode, transientExceptionCodes))
+ {
+ if (traceWriter != null)
+ {
+ traceWriter.Trace(System.Diagnostics.TraceLevel.Error, $"Error: {ex.Message}. Retrying {retryCount}/{maxRetries}", ex);
+ }
+
+ if (++retryCount >= maxRetries)
+ {
+ throw;
+ }
+
+ Thread.Sleep(retryDelayMilliseconds);
+ }
+ }
+ }
+
+ ///
+ /// Voids the operation with basic retry asynchronous.
+ ///
+ /// The asynchronous operation.
+ /// The transient exception types.
+ /// The retry delay milliseconds.
+ /// The maximum retries.
+ /// The trace writer.
+ ///
+ public static async Task VoidOperationWithBasicRetryAsync(Func asyncOperation, string[] transientExceptionCodes, int retryDelayMilliseconds = 1000, int maxRetries = 60, ITraceWriter traceWriter = null)
+ {
+ int retryCount = 0;
+
+ while (true)
+ {
+ try
+ {
+ await asyncOperation();
+ return;
+ }
+ catch (FaceAPIException ex)
+ when (IsTransientError(ex.ErrorCode, transientExceptionCodes))
+ {
+ if (traceWriter != null)
+ {
+ traceWriter.Trace(System.Diagnostics.TraceLevel.Error, $"Error: {ex.Message}. Retrying {retryCount}/{maxRetries}", ex);
+ }
+
+ if (++retryCount >= maxRetries)
+ {
+ throw;
+ }
+
+ Thread.Sleep(retryDelayMilliseconds);
+ }
+ }
+ }
+
+ ///
+ /// Determines whether [is transient error] [the specified ex].
+ ///
+ /// The ex.
+ /// The transient exception types.
+ ///
+ /// true if [is transient error] [the specified ex]; otherwise, false.
+ ///
+ private static bool IsTransientError(string code, string[] transientExceptionCodes)
+ {
+ return transientExceptionCodes.Contains(code);
+ }
+ }
+}
diff --git a/Photo-App/MainWindow.xaml b/Photo-App/MainWindow.xaml
new file mode 100644
index 0000000..6e7b206
--- /dev/null
+++ b/Photo-App/MainWindow.xaml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/Photo-App/MainWindow.xaml.cs b/Photo-App/MainWindow.xaml.cs
new file mode 100644
index 0000000..a4cb638
--- /dev/null
+++ b/Photo-App/MainWindow.xaml.cs
@@ -0,0 +1,192 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+//
+// SCENARIO: Photo Manager. Identify and tag photos automatically.
+//
+// This scenario combines several Face API features into a fully working example
+// of a photo store management tool. With this tool, you can catalog all your
+// photos and holiday snaps. Once the AI service is sufficiently trained, the
+// hope is that it can identify and tag up to 80% of images. The tool also
+// enables you to show all photos containing a certain person.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App
+{
+ using System.ComponentModel;
+ using System.Net;
+ using System.Runtime.CompilerServices;
+ using System.Windows;
+ using Photo_Detect_Catalogue_Search_WPF_App.Controls;
+ using Photo_Detect_Catalogue_Search_WPF_App.Helpers;
+ using SampleUserControlLibrary;
+
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow
+ {
+ #region Constants
+
+ private const string DefaultEndpoint = "https://westus.api.cognitive.microsoft.com/face/v1.0/";
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class
+ ///
+ public MainWindow()
+ {
+ FileTraceWriter.LogMessage("App Started");
+
+ InitializeComponent();
+ ServicePointManager.DefaultConnectionLimit = 1000;
+
+ // You can use the next line to insert your own subscription key, instead of using UI to set license key.
+ this.ViewModel = new MainViewModel()
+ {
+ FaceIdentificationDescription = "Tell whom an input face belongs to given a tagged person database. Here we only handle tagged person database in following format: 1). One root folder. 2). Sub-folders are named as person's name. 3). Each person's images are put into their own sub-folder. Pick the root folder, then choose an image to identify, all faces will be shown on the image with the identified person's name.",
+ SortMyPhotosDescription = "Process your photo stores into a people searchable database.",
+ };
+ this.DataContext = this.ViewModel;
+ this._scenariosControl.SampleScenarioList = new Scenario[]
+ {
+ new Scenario()
+ {
+ PageClass = typeof(FaceIdentificationPage),
+ Title = "Face Identification",
+ },
+ new Scenario()
+ {
+ PageClass = typeof(SortMyPhotosPage),
+ Title = "Sort My Photos",
+ },
+ };
+
+ // Set the default endpoint when main windows is initiated.
+ var mainWindow = GetWindow(this) as MainWindow;
+ mainWindow?._scenariosControl.SetSubscriptionPageEndpoint(DefaultEndpoint);
+
+ FileTraceWriter.LogMessage("Main Window constructed");
+ }
+
+ #endregion Constructors
+
+ #region Properties
+
+ ///
+ /// Gets view model instance for MainWindow
+ ///
+ public MainViewModel ViewModel
+ {
+ get; private set;
+ }
+
+ #endregion Properties
+
+ #region Methods
+
+ ///
+ /// Log message in main window log pane
+ ///
+ /// format string
+ /// format arguments
+ public static void Log(string format, params object[] args)
+ {
+ ((MainWindow)Application.Current.MainWindow)._scenariosControl.Log(string.Format(format, args));
+ }
+
+ #endregion Methods
+
+ #region Nested Types
+
+ ///
+ /// View model for MainWindow, covers display image, text
+ ///
+ public class MainViewModel : INotifyPropertyChanged
+ {
+ #region Events
+
+ ///
+ /// Implements INotifyPropertyChanged interface
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ #endregion Events
+
+ #region Properties
+
+ ///
+ /// Gets or sets description of identification
+ ///
+ public string FaceIdentificationDescription
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets description of identification
+ ///
+ public string SortMyPhotosDescription
+ {
+ get;
+ set;
+ }
+
+ #endregion Properties
+
+ #region Methods
+
+ ///
+ /// Helper function for INotifyPropertyChanged interface
+ ///
+ /// Property type
+ /// Property name
+ private void OnPropertyChanged([CallerMemberName]string caller = null)
+ {
+ var handler = PropertyChanged;
+ if (handler != null)
+ {
+ handler(this, new PropertyChangedEventArgs(caller));
+ }
+ }
+
+ #endregion Methods
+ }
+
+ #endregion Nested Types
+ }
+}
diff --git a/Photo-App/Models/Converters.cs b/Photo-App/Models/Converters.cs
new file mode 100644
index 0000000..b05a9a0
--- /dev/null
+++ b/Photo-App/Models/Converters.cs
@@ -0,0 +1,209 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Models
+{
+ using System;
+ using System.Globalization;
+ using System.Windows;
+ using System.Windows.Data;
+
+ ///
+ /// This class returns Visible if the value is not null
+ ///
+ ///
+ public class NotNullVisibilityConverter : IValueConverter
+ {
+ ///
+ /// Converts a value.
+ ///
+ /// The value produced by the binding source.
+ /// The type of the binding target property.
+ /// The converter parameter to use.
+ /// The culture to use in the converter.
+ ///
+ /// A converted value. If the method returns null, the valid null value is used.
+ ///
+ object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value != null)
+ {
+ return Visibility.Visible;
+ }
+ return Visibility.Collapsed;
+ }
+
+ ///
+ /// Converts a value back.
+ ///
+ /// The value that is produced by the binding target.
+ /// The type to convert to.
+ /// The converter parameter to use.
+ /// The culture to use in the converter.
+ ///
+ /// A converted value. If the method returns null, the valid null value is used.
+ ///
+ ///
+ object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ ///
+ /// This class returns Visible if the value has items
+ ///
+ ///
+ public class HasItemsVisibilityConverter : IValueConverter
+ {
+ ///
+ /// Converts a value.
+ ///
+ /// The value produced by the binding source.
+ /// The type of the binding target property.
+ /// The converter parameter to use.
+ /// The culture to use in the converter.
+ ///
+ /// A converted value. If the method returns null, the valid null value is used.
+ ///
+ object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value != null)
+ {
+ return ((int)value) > 0 ? Visibility.Visible : Visibility.Collapsed;
+ }
+ return Visibility.Collapsed;
+ }
+
+ ///
+ /// Converts a value back.
+ ///
+ /// The value that is produced by the binding target.
+ /// The type to convert to.
+ /// The converter parameter to use.
+ /// The culture to use in the converter.
+ ///
+ /// A converted value. If the method returns null, the valid null value is used.
+ ///
+ ///
+ object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ ///
+ /// This class returns Visible if the value has items
+ ///
+ ///
+ public class HasMoreThanBoolConverter : IValueConverter
+ {
+ ///
+ /// Converts a value.
+ ///
+ /// The value produced by the binding source.
+ /// The type of the binding target property.
+ /// The converter parameter to use.
+ /// The culture to use in the converter.
+ ///
+ /// A converted value. If the method returns null, the valid null value is used.
+ ///
+ object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value != null)
+ {
+ var moreThanVal = 0;
+ if (parameter != null)
+ {
+ moreThanVal = int.Parse(parameter.ToString());
+ }
+ return ((int)value) > moreThanVal;
+ }
+ return false;
+ }
+
+ ///
+ /// Converts a value back.
+ ///
+ /// The value that is produced by the binding target.
+ /// The type to convert to.
+ /// The converter parameter to use.
+ /// The culture to use in the converter.
+ ///
+ /// A converted value. If the method returns null, the valid null value is used.
+ ///
+ ///
+ object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class BoolVisibilityConverter : IValueConverter
+ {
+ ///
+ /// Converts a value.
+ ///
+ /// The value produced by the binding source.
+ /// The type of the binding target property.
+ /// The converter parameter to use.
+ /// The culture to use in the converter.
+ ///
+ /// A converted value. If the method returns null, the valid null value is used.
+ ///
+ object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value != null)
+ {
+ return ((bool)value) ? Visibility.Visible : Visibility.Collapsed;
+ }
+ return Visibility.Collapsed;
+ }
+
+ ///
+ /// Converts a value back.
+ ///
+ /// The value that is produced by the binding target.
+ /// The type to convert to.
+ /// The converter parameter to use.
+ /// The culture to use in the converter.
+ ///
+ /// A converted value. If the method returns null, the valid null value is used.
+ ///
+ ///
+ object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Photo-App/Models/Face.cs b/Photo-App/Models/Face.cs
new file mode 100644
index 0000000..ad370da
--- /dev/null
+++ b/Photo-App/Models/Face.cs
@@ -0,0 +1,647 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.ProjectOxford.Face.Contract;
+using Newtonsoft.Json;
+using System;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Models
+{
+ ///
+ /// Face view model
+ ///
+ public class Face : INotifyPropertyChanged
+ {
+ #region Fields
+
+ ///
+ /// Face age text string
+ ///
+ private string _age;
+
+ ///
+ /// Face gender text string
+ ///
+ private string _gender;
+
+ ///
+ /// confidence value of this face to a target face
+ ///
+ private double _confidence;
+
+ ///
+ /// Person name
+ ///
+ private string _personName;
+
+ ///
+ /// Person name
+ ///
+ private Guid _personId;
+
+ ///
+ /// Person name
+ ///
+ private string _personSourcePath;
+
+ ///
+ /// Face height in pixel
+ ///
+ private int _height;
+
+ ///
+ /// Face position X relative to image left-top in pixel
+ ///
+ private int _left;
+
+ ///
+ /// Face position Y relative to image left-top in pixel
+ ///
+ private int _top;
+
+ ///
+ /// Face width in pixel
+ ///
+ private int _width;
+
+ ///
+ /// Indicates the headPose
+ ///
+ private string _headPose;
+
+ ///
+ /// Facial hair display string
+ ///
+ private string _facialHair;
+
+ ///
+ /// Indicates the glasses type
+ ///
+ private string _glasses;
+
+ ///
+ /// Indicates the emotion
+ ///
+ private string _emotion;
+
+ ///
+ /// Indicates the hair
+ ///
+ private string _hair;
+
+ ///
+ /// Indicates the makeup
+ ///
+ private string _makeup;
+
+ ///
+ /// Indicates the eye occlusion
+ ///
+ private string _eyeOcclusion;
+
+ ///
+ /// Indicates the forehead occlusion
+ ///
+ private string _foreheadOcclusion;
+
+ ///
+ /// Indicates the mouth occlusion
+ ///
+ private string _mouthOcclusion;
+
+ ///
+ /// Indicates the accessories
+ ///
+ private string _accessories;
+
+ ///
+ /// Indicates the blur
+ ///
+ private string _blur;
+
+ ///
+ /// Indicates the exposure
+ ///
+ private string _exposure;
+
+ ///
+ /// Indicates the noise
+ ///
+ private string _noise;
+
+ private bool _addToGroup;
+
+ private IdentifyResult _identifyResult;
+
+ public IdentifyResult Identifications
+ {
+ get { return _identifyResult; }
+ set
+ {
+ _identifyResult = value;
+ OnPropertyChanged();
+ PropertyChanged(this, new PropertyChangedEventArgs("Identifications"));
+ PropertyChanged(this, new PropertyChangedEventArgs("IdentificationsButtonString"));
+ }
+ }
+
+ public string IdentificationsButtonString
+ {
+ get
+ {
+ if (Identifications != null && Identifications.Candidates != null)
+ {
+ return $"+{Identifications.Candidates.Length - 1}";
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+
+ private bool _isShowingOthers;
+
+ public bool IsShowingOthers
+ {
+ get { return _isShowingOthers; }
+ set
+ {
+ _isShowingOthers = value;
+ OnPropertyChanged();
+ }
+ }
+
+
+
+ #endregion Fields
+
+ #region Events
+
+ ///
+ /// Implement INotifyPropertyChanged interface
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ #endregion Events
+
+ #region Properties
+
+ public bool AddToGroup
+ {
+ get
+ {
+ return _addToGroup;
+ }
+
+ set
+ {
+ _addToGroup = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets age text string
+ ///
+ public string Age
+ {
+ get
+ {
+ return _age;
+ }
+
+ set
+ {
+ _age = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets gender text string
+ ///
+ public string Gender
+ {
+ get
+ {
+ return _gender;
+ }
+
+ set
+ {
+ _gender = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets confidence value
+ ///
+ public double Confidence
+ {
+ get
+ {
+ return _confidence;
+ }
+
+ set
+ {
+ _confidence = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets face rectangle on image
+ ///
+ public System.Windows.Int32Rect UIRect
+ {
+ get
+ {
+ return new System.Windows.Int32Rect(Left, Top, Width, Height);
+ }
+ }
+
+ ///
+ /// Gets or sets image path
+ ///
+ public ImageSource ImageFile
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets face id
+ ///
+ public string FaceId
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets person's name
+ ///
+ public string PersonName
+ {
+ get
+ {
+ return _personName;
+ }
+
+ set
+ {
+ _personName = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets person's Id
+ ///
+ public Guid PersonId
+ {
+ get
+ {
+ return _personId;
+ }
+
+ set
+ {
+ _personId = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets person's name
+ ///
+ public string PersonSourcePath
+ {
+ get
+ {
+ return _personSourcePath;
+ }
+
+ set
+ {
+ _personSourcePath = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets face height
+ ///
+ public int Height
+ {
+ get
+ {
+ return _height;
+ }
+
+ set
+ {
+ _height = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets face position X
+ ///
+ public int Left
+ {
+ get
+ {
+ return _left;
+ }
+
+ set
+ {
+ _left = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets face position Y
+ ///
+ public int Top
+ {
+ get
+ {
+ return _top;
+ }
+
+ set
+ {
+ _top = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets face width
+ ///
+ public int Width
+ {
+ get
+ {
+ return _width;
+ }
+
+ set
+ {
+ _width = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the head pose value.
+ ///
+ public string HeadPose
+ {
+ get { return _headPose; }
+ set
+ {
+ _headPose = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets facial hair display string
+ ///
+ public string FacialHair
+ {
+ get
+ {
+ return _facialHair;
+ }
+
+ set
+ {
+ _facialHair = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the glasses type
+ ///
+ public string Glasses
+ {
+ get
+ {
+ return _glasses;
+ }
+
+ set
+ {
+ _glasses = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the emotion type
+ ///
+ public string Emotion
+ {
+ get { return _emotion; }
+ set
+ {
+ _emotion = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the hair type
+ ///
+ public string Hair
+ {
+ get { return _hair; }
+ set
+ {
+ _hair = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the makeup type
+ ///
+ public string Makeup
+ {
+ get { return _makeup; }
+ set
+ {
+ _makeup = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the occlusion type of eye
+ ///
+ public string EyeOcclusion
+ {
+ get { return _eyeOcclusion; }
+ set
+ {
+ _eyeOcclusion = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the occlusion type of forehead
+ ///
+ public string ForeheadOcclusion
+ {
+ get { return _foreheadOcclusion; }
+ set
+ {
+ _foreheadOcclusion = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the occlusion type of mouth
+ ///
+ public string MouthOcclusion
+ {
+ get { return _mouthOcclusion; }
+ set
+ {
+ _mouthOcclusion = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the accessories type
+ ///
+ public string Accessories
+ {
+ get { return _accessories; }
+ set
+ {
+ _accessories = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the blur type
+ ///
+ public string Blur
+ {
+ get { return _blur; }
+ set
+ {
+ _blur = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the exposure type
+ ///
+ public string Exposure
+ {
+ get { return _exposure; }
+ set
+ {
+ _exposure = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating the noise type
+ ///
+ public string Noise
+ {
+ get { return _noise; }
+ set
+ {
+ _noise = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public FaceRectangle FaceRectangle { get; set; }
+
+ System.Windows.Controls.Control _contextBinder = new System.Windows.Controls.Control { Visibility = System.Windows.Visibility.Hidden };
+
+ [JsonIgnore]
+ public virtual System.Windows.Controls.Control ContextBinder
+ {
+ get
+ {
+ return _contextBinder;
+ }
+ }
+
+ #endregion Properties
+
+ #region Methods
+
+ ///
+ /// NotifyProperty Helper functions
+ ///
+ /// property type
+ /// property change caller
+ private void OnPropertyChanged([CallerMemberName]string caller = null)
+ {
+ var handler = PropertyChanged;
+ if (handler != null)
+ {
+ handler(this, new PropertyChangedEventArgs(caller));
+ }
+ }
+
+ #endregion Methods
+ }
+}
\ No newline at end of file
diff --git a/Photo-App/Models/LargePersonGroupExtended.cs b/Photo-App/Models/LargePersonGroupExtended.cs
new file mode 100644
index 0000000..d310490
--- /dev/null
+++ b/Photo-App/Models/LargePersonGroupExtended.cs
@@ -0,0 +1,83 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Models
+{
+ using Microsoft.ProjectOxford.Face.Contract;
+ using System.Collections.ObjectModel;
+
+ ///
+ /// Extends the LargePersonGroup class to include other useful bits, could change to inherited
+ ///
+ public class LargePersonGroupExtended
+ {
+ ///
+ /// The group persons
+ ///
+ private ObservableCollection _groupPersons = new ObservableCollection();
+
+ ///
+ /// Gets or sets the group.
+ ///
+ ///
+ /// The group.
+ ///
+ public LargePersonGroup Group { get; set; }
+
+ ///
+ /// Gets or sets the group persons.
+ ///
+ ///
+ /// The group persons.
+ ///
+ public ObservableCollectionGroupPersons
+ {
+ get
+ {
+ return _groupPersons;
+ }
+ set
+ {
+ _groupPersons = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the selected person.
+ ///
+ ///
+ /// The selected person.
+ ///
+ public PersonExtended SelectedPerson { get; set; }
+ }
+}
diff --git a/Photo-App/Models/PersonExtended.cs b/Photo-App/Models/PersonExtended.cs
new file mode 100644
index 0000000..6e12dec
--- /dev/null
+++ b/Photo-App/Models/PersonExtended.cs
@@ -0,0 +1,118 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Models
+{
+ using System.Collections.ObjectModel;
+ using System.ComponentModel;
+
+ ///
+ /// Extends the Person class to include other useful bits, could change to inherited
+ ///
+ ///
+ public class PersonExtended : INotifyPropertyChanged
+ {
+ ///
+ /// The faces
+ ///
+ private ObservableCollection _faces
+ = new ObservableCollection();
+
+ ///
+ /// The is selected
+ ///
+ private bool _isSelected;
+
+ ///
+ /// Gets or sets the faces.
+ ///
+ ///
+ /// The faces.
+ ///
+ public ObservableCollection Faces
+ {
+ get
+ {
+ return _faces;
+ }
+ set
+ {
+ _faces = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the person.
+ ///
+ ///
+ /// The person.
+ ///
+ public Microsoft.ProjectOxford.Face.Contract.Person Person { get; set; }
+
+ ///
+ /// Gets or sets the person files database count.
+ ///
+ ///
+ /// The person files database count.
+ ///
+ public int PersonFilesDbCount { get; set; }
+
+ ///
+ /// Occurs when a property value changes.
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// Gets or sets a value indicating whether this instance is selected.
+ ///
+ ///
+ /// true if this instance is selected; otherwise, false.
+ ///
+ public bool IsSelected
+ {
+ get
+ {
+ return _isSelected;
+ }
+
+ set
+ {
+ _isSelected = value;
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
+ }
+ }
+ }
+ }
+}
diff --git a/Photo-App/Photo-App.sln b/Photo-App/Photo-App.sln
new file mode 100644
index 0000000..bb6bc43
--- /dev/null
+++ b/Photo-App/Photo-App.sln
@@ -0,0 +1,65 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27703.2018
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Photo-Detect-Catalogue-Search-WPF-App", "Photo-Detect-Catalogue-Search-WPF-App.csproj", "{80C32B84-C751-4241-B676-D16A110C6708}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleUserControlLibrary", "..\Cognitive-Common-Windows\SampleUserControlLibrary\SampleUserControlLibrary.csproj", "{735929F0-F8F1-4D93-B027-5D034FA7892B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DatabaseLibrary", "..\DatabaseLibrary\DatabaseLibrary.csproj", "{D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {80C32B84-C751-4241-B676-D16A110C6708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Debug|x64.Build.0 = Debug|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Debug|x86.Build.0 = Debug|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Release|Any CPU.Build.0 = Release|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Release|x64.ActiveCfg = Release|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Release|x64.Build.0 = Release|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Release|x86.ActiveCfg = Release|Any CPU
+ {80C32B84-C751-4241-B676-D16A110C6708}.Release|x86.Build.0 = Release|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Debug|x64.Build.0 = Debug|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Debug|x86.Build.0 = Debug|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Release|x64.ActiveCfg = Release|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Release|x64.Build.0 = Release|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Release|x86.ActiveCfg = Release|Any CPU
+ {735929F0-F8F1-4D93-B027-5D034FA7892B}.Release|x86.Build.0 = Release|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Debug|x64.Build.0 = Debug|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Debug|x86.Build.0 = Debug|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Release|x64.ActiveCfg = Release|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Release|x64.Build.0 = Release|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Release|x86.ActiveCfg = Release|Any CPU
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {86A98D5F-1B4A-48DE-AC8A-BB6B1EC46E3C}
+ EndGlobalSection
+EndGlobal
diff --git a/Photo-App/Photo-Detect-Catalogue-Search-WPF-App.csproj b/Photo-App/Photo-Detect-Catalogue-Search-WPF-App.csproj
new file mode 100644
index 0000000..fcf87bb
--- /dev/null
+++ b/Photo-App/Photo-Detect-Catalogue-Search-WPF-App.csproj
@@ -0,0 +1,248 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {80C32B84-C751-4241-B676-D16A110C6708}
+ WinExe
+ Photo_Detect_Catalogue_Search_WPF_App
+ Photo-Detect-Catalogue-Search-WPF-App
+ v4.7.1
+ 512
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 4
+ true
+
+ true
+ publish\
+ true
+ Web
+ true
+ Foreground
+ 7
+ Days
+ false
+ false
+ true
+ http://iot.fit/Azure/PhotoRecogniser/
+ true
+ publish.htm
+ 3
+ 1.0.0.%2a
+ false
+ true
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ C079015197079A7481ED622F1402A59164492C85
+
+
+ Photo-Detect-Catalogue-Search-WPF-App_TemporaryKey.pfx
+
+
+ true
+
+
+ true
+
+
+
+ ..\Sample-WPF\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll
+
+
+ ..\Sample-WPF\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll
+
+
+ ..\Sample-WPF\packages\Microsoft.ProjectOxford.Common.1.0.324\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+xamarinios10+MonoTouch10\Microsoft.ProjectOxford.Common.dll
+
+
+ ..\Sample-WPF\packages\Microsoft.ProjectOxford.Face.1.4.0\lib\portable-net45+wp80+win8+wpa81+aspnetcore50\Microsoft.ProjectOxford.Face.dll
+
+
+ packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+
+
+ ..\Sample-WPF\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll
+
+
+ ..\Sample-WPF\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll
+
+
+
+
+
+
+
+
+
+
+
+
+ 4.0
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ App.xaml
+ Code
+
+
+ FaceIdentificationPage.xaml
+
+
+ ManageGroupsControl.xaml
+
+
+ PopupWindow.xaml
+
+
+ ScanFolderControl.xaml
+
+
+ ShowPersonMatchedFilesControl.xaml
+
+
+ SortMyPhotosPage.xaml
+
+
+
+
+
+
+
+
+ MainWindow.xaml
+ Code
+
+
+
+
+
+
+ Code
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ Settings.settings
+ True
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
+ Designer
+
+
+
+
+ {735929f0-f8f1-4d93-b027-5d034fa7892b}
+ SampleUserControlLibrary
+
+
+ {D7D7A8CA-8E2E-4FF3-9EB4-78DE8A514332}
+ DatabaseLibrary
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Designer
+ XamlIntelliSenseFileGenerator
+
+
+ Designer
+ XamlIntelliSenseFileGenerator
+
+
+ Designer
+ XamlIntelliSenseFileGenerator
+
+
+ Designer
+ XamlIntelliSenseFileGenerator
+
+
+ Designer
+ XamlIntelliSenseFileGenerator
+
+
+ Designer
+ XamlIntelliSenseFileGenerator
+
+
+ Designer
+ XamlIntelliSenseFileGenerator
+
+
+
+
+ False
+ Microsoft .NET Framework 4.5.2 %28x86 and x64%29
+ true
+
+
+ False
+ .NET Framework 3.5 SP1
+ false
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Photo-App/Properties/AssemblyInfo.cs b/Photo-App/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..ddc58e3
--- /dev/null
+++ b/Photo-App/Properties/AssemblyInfo.cs
@@ -0,0 +1,55 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Photo-Detect-Catalogue-Search-WPF-App")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Photo-Detect-Catalogue-Search-WPF-App")]
+[assembly: AssemblyCopyright("Copyright © 2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//CultureYouAreCodingWith in your .csproj file
+//inside a . For example, if you are using US english
+//in your source files, set the to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Photo-App/Properties/Resources.Designer.cs b/Photo-App/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..146e07c
--- /dev/null
+++ b/Photo-App/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Photo_Detect_Catalogue_Search_WPF_App.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/Photo-App/Properties/Resources.resx b/Photo-App/Properties/Resources.resx
new file mode 100644
index 0000000..af7dbeb
--- /dev/null
+++ b/Photo-App/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Photo-App/Properties/Settings.Designer.cs b/Photo-App/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..644b95d
--- /dev/null
+++ b/Photo-App/Properties/Settings.Designer.cs
@@ -0,0 +1,26 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Photo_Detect_Catalogue_Search_WPF_App.Properties {
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default {
+ get {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/Photo-App/Properties/Settings.settings b/Photo-App/Properties/Settings.settings
new file mode 100644
index 0000000..033d7a5
--- /dev/null
+++ b/Photo-App/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Photo-App/StandardDatabaseLibrary/Data/IDataProvider.cs b/Photo-App/StandardDatabaseLibrary/Data/IDataProvider.cs
new file mode 100644
index 0000000..13a2722
--- /dev/null
+++ b/Photo-App/StandardDatabaseLibrary/Data/IDataProvider.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace StandardDatabaseLibrary.Data
+{
+ interface IDataProvider
+ {
+ void AddFile(PictureFile file, string groupId, int processingState);
+
+ PictureFile GetFile(string path);
+
+ void RemoveFile(int fileId); // Must also remove any persons associated with the file
+
+ void AddPerson(PicturePerson person);
+
+ void RemovePerson(int personId, int fileId);
+
+ int GetFileCountForPersonId(Guid personId);
+
+ Person GetPerson(Guid personId);
+
+ void AddPerson(Guid personId, string name, string userData);
+
+ void RemovePersonsForGroup(string groupId);
+
+ List GetFilesForPersonId(Guid personId);
+ }
+}
diff --git a/Photo-App/StandardDatabaseLibrary/Data/Person.cs b/Photo-App/StandardDatabaseLibrary/Data/Person.cs
new file mode 100644
index 0000000..2782d01
--- /dev/null
+++ b/Photo-App/StandardDatabaseLibrary/Data/Person.cs
@@ -0,0 +1,22 @@
+namespace StandardDatabaseLibrary.Data
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Data.Entity.Spatial;
+
+ [Table("Person")]
+ public partial class Person
+ {
+ public Guid PersonId { get; set; }
+
+ [Required]
+ [StringLength(300)]
+ public string Name { get; set; }
+
+ public string UserData { get; set; }
+
+ public virtual ICollection PicturePersons { get; set; }
+ }
+}
diff --git a/Photo-App/StandardDatabaseLibrary/Data/PhotosDatabase.cs b/Photo-App/StandardDatabaseLibrary/Data/PhotosDatabase.cs
new file mode 100644
index 0000000..3b6b4c4
--- /dev/null
+++ b/Photo-App/StandardDatabaseLibrary/Data/PhotosDatabase.cs
@@ -0,0 +1,28 @@
+namespace StandardDatabaseLibrary.Data
+{
+ using System;
+ using System.Data.Entity;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Linq;
+ using System.Runtime.Remoting.Contexts;
+
+ public partial class PhotosDatabase : Context
+ {
+ public PhotosDatabase()
+ : base("name=PhotosDatabase")
+ {
+ }
+
+ public virtual DbSet People { get; set; }
+ public virtual DbSet PictureFiles { get; set; }
+ public virtual DbSet PictureFileGroupLookups { get; set; }
+ public virtual DbSet PicturePersons { get; set; }
+
+ protected override void OnModelCreating(DbModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .Property(e => e.LargePersonGroupId)
+ .IsUnicode(false);
+ }
+ }
+}
diff --git a/Photo-App/StandardDatabaseLibrary/Data/PictureFile.cs b/Photo-App/StandardDatabaseLibrary/Data/PictureFile.cs
new file mode 100644
index 0000000..b961441
--- /dev/null
+++ b/Photo-App/StandardDatabaseLibrary/Data/PictureFile.cs
@@ -0,0 +1,27 @@
+namespace StandardDatabaseLibrary.Data
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+
+ [Table("PictureFile")]
+ public partial class PictureFile
+ {
+ public int Id { get; set; }
+
+ [Required]
+ public string FilePath { get; set; }
+
+ public DateTime DateAdded { get; set; }
+
+ public DateTime? DateTaken { get; set; }
+
+ public bool IsConfirmed { get; set; }
+
+ public virtual ICollection PicturePersons { get; set; }
+
+ public virtual ICollection PictureFileGroupLookups { get; set; }
+
+ }
+}
diff --git a/Photo-App/StandardDatabaseLibrary/Data/PictureFileGroupLookup.cs b/Photo-App/StandardDatabaseLibrary/Data/PictureFileGroupLookup.cs
new file mode 100644
index 0000000..79d58a7
--- /dev/null
+++ b/Photo-App/StandardDatabaseLibrary/Data/PictureFileGroupLookup.cs
@@ -0,0 +1,26 @@
+namespace StandardDatabaseLibrary.Data
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Data.Entity.Spatial;
+
+ [Table("PictureFileGroupLookup")]
+ public partial class PictureFileGroupLookup
+ {
+ [Key]
+ [Column(Order = 0)]
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public int PictureFileId { get; set; }
+
+ [Key]
+ [Column(Order = 1)]
+ [StringLength(50)]
+ public string LargePersonGroupId { get; set; }
+
+ public int ProcessingState { get; set; }
+
+ public virtual PictureFile PictureFile { get; set; }
+ }
+}
diff --git a/Photo-App/StandardDatabaseLibrary/Data/PicturePerson.cs b/Photo-App/StandardDatabaseLibrary/Data/PicturePerson.cs
new file mode 100644
index 0000000..875cb7b
--- /dev/null
+++ b/Photo-App/StandardDatabaseLibrary/Data/PicturePerson.cs
@@ -0,0 +1,31 @@
+namespace StandardDatabaseLibrary.Data
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;
+ using System.Data.Entity.Spatial;
+
+ [Table("PicturePerson")]
+ public partial class PicturePerson
+ {
+ public int Id { get; set; }
+
+ public int PictureFileId { get; set; }
+
+ [Required]
+ [StringLength(50)]
+ public string LargePersonGroupId { get; set; }
+
+ public Guid? PersonId { get; set; }
+
+ public DateTime DateAdded { get; set; }
+
+ public string FaceJSON { get; set; }
+
+ public bool IsConfirmed { get; set; }
+
+ public virtual PictureFile PictureFile { get; set; }
+ public virtual Person Person { get; set; }
+ }
+}
diff --git a/Photo-App/StandardDatabaseLibrary/SqlDataProvider.cs b/Photo-App/StandardDatabaseLibrary/SqlDataProvider.cs
new file mode 100644
index 0000000..5aedbaa
--- /dev/null
+++ b/Photo-App/StandardDatabaseLibrary/SqlDataProvider.cs
@@ -0,0 +1,84 @@
+using StandardDatabaseLibrary.Data;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace StandardDatabaseLibrary
+{
+ public class SqlDataProvider : IDataProvider
+ {
+ PhotosDatabase db = new PhotosDatabase();
+
+ public void AddFile(PictureFile file, string groupId, int processingState)
+ {
+ var lookup = new PictureFileGroupLookup
+ {
+ LargePersonGroupId = groupId,
+ ProcessingState = processingState,
+ };
+
+ lookup.PictureFile = file;
+ db.PictureFileGroupLookups.Add(lookup);
+ db.SaveChanges();
+
+
+ //db.PictureFiles.Add(file);
+ //db.SaveChanges();
+ //db.PictureFileGroupLookups.Add(new PictureFileGroupLookup { LargePersonGroupId = groupId, PictureFileId = })
+ }
+
+ public PictureFile GetFile(string path)
+ {
+ return db.PictureFiles.Where(a => a.FilePath == path).SingleOrDefault();
+ }
+
+ public void AddPerson(PicturePerson person)
+ {
+ db.PicturePersons.Add(person);
+ db.SaveChanges();
+ }
+
+ public void RemoveFile(int fileId)
+ {
+ var dbFile = db.PictureFiles.Find(fileId);
+ db.PictureFiles.Remove(dbFile);
+ db.SaveChanges();
+ }
+
+ public void RemovePerson(int personId, int fileId)
+ {
+ var dbPerson = db.PicturePersons.Find(fileId);
+ db.PicturePersons.Remove(dbPerson);
+ db.SaveChanges();
+ }
+
+ public int GetFileCountForPersonId(Guid personId)
+ {
+ return db.PicturePersons.Where(a => a.PersonId == personId).Count();
+ }
+
+ public List GetFilesForPersonId(Guid personId)
+ {
+ return db.PicturePersons.Where(a => a.PersonId == personId).ToList();
+ }
+
+ public Person GetPerson(Guid personId)
+ {
+ return db.People.Where(a => a.PersonId == personId).SingleOrDefault();
+ }
+
+ public void AddPerson(Guid personId, string name, string userData)
+ {
+ db.People.Add(new Person { Name = name, PersonId = personId, UserData = userData });
+ db.SaveChanges();
+ }
+
+ public void RemovePersonsForGroup(string groupId)
+ {
+ db.Database.ExecuteSqlCommand($"DELETE FROM PicturePerson WHERE LargePersonGroupId = '{groupId}'");
+ }
+ }
+}
diff --git a/Photo-App/StandardDatabaseLibrary/StandardDatabaseLibrary.csproj b/Photo-App/StandardDatabaseLibrary/StandardDatabaseLibrary.csproj
new file mode 100644
index 0000000..1de1479
--- /dev/null
+++ b/Photo-App/StandardDatabaseLibrary/StandardDatabaseLibrary.csproj
@@ -0,0 +1,11 @@
+
+
+
+ netstandard1.4
+
+
+
+
+
+
+
diff --git a/Photo-App/packages.config b/Photo-App/packages.config
new file mode 100644
index 0000000..1c475f8
--- /dev/null
+++ b/Photo-App/packages.config
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Sample-WPF/Controls/FaceIdentificationPage.xaml b/Sample-WPF/Controls/FaceIdentificationPage.xaml
index 829bdb6..6c1c8a4 100644
--- a/Sample-WPF/Controls/FaceIdentificationPage.xaml
+++ b/Sample-WPF/Controls/FaceIdentificationPage.xaml
@@ -2,7 +2,7 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
-//
+//
// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
//
// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
diff --git a/Sample-WPF/Controls/FaceIdentificationPage.xaml.cs b/Sample-WPF/Controls/FaceIdentificationPage.xaml.cs
index a414f9f..8d23563 100644
--- a/Sample-WPF/Controls/FaceIdentificationPage.xaml.cs
+++ b/Sample-WPF/Controls/FaceIdentificationPage.xaml.cs
@@ -44,12 +44,32 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
-
using ClientContract = Microsoft.ProjectOxford.Face.Contract;
using System.Windows.Media;
+using Microsoft.ProjectOxford.Face.Contract;
+using Microsoft.ProjectOxford.Face;
+using Newtonsoft.Json.Serialization;
+using Microsoft.ProjectOxford.Face.Helpers;
namespace Microsoft.ProjectOxford.Face.Controls
{
+ //using System;
+ //using System.Collections.Concurrent;
+ //using System.Collections.Generic;
+ //using System.Collections.ObjectModel;
+ //using System.ComponentModel;
+ //using System.Diagnostics;
+ //using System.IO;
+ //using System.Linq;
+ //using System.Text;
+ //using System.Threading;
+ //using System.Threading.Tasks;
+ //using System.Windows;
+ //using System.Windows.Controls;
+
+ //using ClientContract = Microsoft.ProjectOxford.Face.Contract;
+ //using System.Windows.Media;
+
///
/// Interaction logic for FaceDetection.xaml
///
@@ -70,7 +90,7 @@ public partial class FaceIdentificationPage : Page, INotifyPropertyChanged
///
/// Faces to identify
///
- private ObservableCollection _faces = new ObservableCollection();
+ private ObservableCollection _faces = new ObservableCollection();
///
/// Person database
@@ -87,6 +107,8 @@ public partial class FaceIdentificationPage : Page, INotifyPropertyChanged
///
private int _maxConcurrentProcesses;
+ private MainWindowLogTraceWriter _mainWindowLogTraceWriter;
+
#endregion Fields
#region Constructors
@@ -98,6 +120,8 @@ public FaceIdentificationPage()
{
InitializeComponent();
_maxConcurrentProcesses = 4;
+
+ _mainWindowLogTraceWriter = new MainWindowLogTraceWriter();
}
#endregion Constructors
@@ -190,7 +214,7 @@ public ImageSource SelectedFile
///
/// Gets faces to identify
///
- public ObservableCollection TargetFaces
+ public ObservableCollection TargetFaces
{
get
{
@@ -215,9 +239,9 @@ private async void FolderPicker_Click(object sender, RoutedEventArgs e)
MainWindow mainWindow = Window.GetWindow(this) as MainWindow;
string subscriptionKey = mainWindow._scenariosControl.SubscriptionKey;
- string endpoint= mainWindow._scenariosControl.SubscriptionEndpoint;
+ string endpoint = mainWindow._scenariosControl.SubscriptionEndpoint;
- var faceServiceClient = new FaceServiceClient(subscriptionKey,endpoint);
+ var faceServiceClient = new FaceServiceClient(subscriptionKey, endpoint);
// Test whether the group already exists
try
@@ -277,7 +301,7 @@ private async void FolderPicker_Click(object sender, RoutedEventArgs e)
MainWindow.Log("Request: Creating group \"{0}\"", this.GroupId);
try
{
- await faceServiceClient.CreateLargePersonGroupAsync(this.GroupId, this.GroupId);
+ await faceServiceClient.CreateLargePersonGroupAsync(this.GroupId, this.GroupId, dlg.SelectedPath);
MainWindow.Log("Response: Success. Group \"{0}\" created", this.GroupId);
}
catch (FaceAPIException ex)
@@ -300,12 +324,18 @@ private async void FolderPicker_Click(object sender, RoutedEventArgs e)
Person p = new Person();
p.PersonName = tag;
- var faces = new ObservableCollection();
+ var faces = new ObservableCollection();
p.Faces = faces;
// Call create person REST API, the new create person id will be returned
MainWindow.Log("Request: Creating person \"{0}\"", p.PersonName);
- p.PersonId = (await faceServiceClient.CreatePersonInLargePersonGroupAsync(this.GroupId, p.PersonName)).PersonId.ToString();
+
+ p.PersonId = (await RetryHelper.OperationWithBasicRetryAsync(async () => await
+ faceServiceClient.CreatePersonInLargePersonGroupAsync(this.GroupId, p.PersonName, dir),
+ new[] { typeof(FaceAPIException) },
+ traceWriter: _mainWindowLogTraceWriter
+ )).PersonId.ToString();
+
MainWindow.Log("Response: Success. Person \"{0}\" (PersonID:{1}) created", p.PersonName, p.PersonId);
string img;
@@ -314,7 +344,7 @@ private async void FolderPicker_Click(object sender, RoutedEventArgs e)
new ConcurrentBag(
Directory.EnumerateFiles(dir, "*.*", SearchOption.AllDirectories)
.Where(s => s.ToLower().EndsWith(".jpg") || s.ToLower().EndsWith(".png") || s.ToLower().EndsWith(".bmp") || s.ToLower().EndsWith(".gif")));
-
+
while (imageList.TryTake(out img))
{
tasks.Add(Task.Factory.StartNew(
@@ -364,7 +394,7 @@ private async void FolderPicker_Click(object sender, RoutedEventArgs e)
}
this.Dispatcher.Invoke(
- new Action, string, ClientContract.AddPersistedFaceResult>(UIHelper.UpdateFace),
+ new Action, string, ClientContract.AddPersistedFaceResult>(UIHelper.UpdateFace),
faces,
detectionResult.Item1,
detectionResult.Item2);
@@ -401,7 +431,13 @@ private async void FolderPicker_Click(object sender, RoutedEventArgs e)
{
// Start train large person group
MainWindow.Log("Request: Training group \"{0}\"", this.GroupId);
- await faceServiceClient.TrainLargePersonGroupAsync(this.GroupId);
+
+ await RetryHelper.VoidOperationWithBasicRetryAsync(() =>
+ faceServiceClient.TrainLargePersonGroupAsync(this.GroupId),
+ new[] { typeof(FaceAPIException) },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ //await faceServiceClient.TrainLargePersonGroupAsync(this.GroupId);
// Wait until train completed
while (true)
@@ -409,7 +445,7 @@ private async void FolderPicker_Click(object sender, RoutedEventArgs e)
await Task.Delay(1000);
var status = await faceServiceClient.GetLargePersonGroupTrainingStatusAsync(this.GroupId);
MainWindow.Log("Response: {0}. Group \"{1}\" training process is {2}", "Success", this.GroupId, status.Status);
- if (status.Status != Contract.Status.Running)
+ if (status.Status != Status.Running)
{
break;
}
@@ -459,7 +495,12 @@ private async void Identify_Click(object sender, RoutedEventArgs e)
{
try
{
- var faces = await faceServiceClient.DetectAsync(fStream);
+ var faces = await RetryHelper.OperationWithBasicRetryAsync(async () => await
+ faceServiceClient.DetectAsync(fStream),
+ new[] { typeof(FaceAPIException) },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ //var faces = await faceServiceClient.DetectAsync(fStream);
// Convert detection result into UI binding object for rendering
foreach (var face in UIHelper.CalculateFaceRectangleForRendering(faces, MaxImageSize, imageInfo))
@@ -471,7 +512,14 @@ private async void Identify_Click(object sender, RoutedEventArgs e)
// Identify each face
// Call identify REST API, the result contains identified person information
- var identifyResult = await faceServiceClient.IdentifyAsync(faces.Select(ff => ff.FaceId).ToArray(), largePersonGroupId: this.GroupId);
+
+ var identifyResult = await RetryHelper.OperationWithBasicRetryAsync(async () => await
+ faceServiceClient.IdentifyAsync(faces.Select(ff => ff.FaceId).ToArray(), largePersonGroupId: this.GroupId),
+ new[] { typeof(FaceAPIException) },
+ traceWriter: _mainWindowLogTraceWriter);
+
+ //var identifyResult = await faceServiceClient.IdentifyAsync(faces.Select(ff => ff.FaceId).ToArray(), largePersonGroupId: this.GroupId);
+
for (int idx = 0; idx < faces.Length; idx++)
{
// Update identification result for rendering
@@ -591,7 +639,7 @@ public class Person : INotifyPropertyChanged
///
/// Person's faces from database
///
- private ObservableCollection _faces = new ObservableCollection();
+ private ObservableCollection _faces = new ObservableCollection();
///
/// Person's id
@@ -619,7 +667,7 @@ public class Person : INotifyPropertyChanged
///
/// Gets or sets person's faces from database
///
- public ObservableCollection Faces
+ public ObservableCollection Faces
{
get
{
diff --git a/Sample-WPF/FaceAPI-WPF-Samples.csproj b/Sample-WPF/FaceAPI-WPF-Samples.csproj
index bacba2f..de8e45e 100644
--- a/Sample-WPF/FaceAPI-WPF-Samples.csproj
+++ b/Sample-WPF/FaceAPI-WPF-Samples.csproj
@@ -140,6 +140,8 @@
FaceDetectionPage.xaml
+
+
MainWindow.xaml
Code
diff --git a/Sample-WPF/FaceAPI-WPF-Samples.sln b/Sample-WPF/FaceAPI-WPF-Samples.sln
index 6971450..f8b2192 100644
--- a/Sample-WPF/FaceAPI-WPF-Samples.sln
+++ b/Sample-WPF/FaceAPI-WPF-Samples.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.23107.0
+# Visual Studio 15
+VisualStudioVersion = 15.0.27703.2018
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FaceAPI-WPF-Samples", "FaceAPI-WPF-Samples.csproj", "{A25DFB19-C947-4A66-AEDE-FB2A960916AD}"
EndProject
@@ -24,6 +24,9 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {F5DF28CC-41E0-46BB-B82F-A5BC511D950C}
+ EndGlobalSection
GlobalSection( ) = preSolution
EndGlobalSection
EndGlobal
diff --git a/Sample-WPF/Helpers/MainWindowLogTraceWriter.cs b/Sample-WPF/Helpers/MainWindowLogTraceWriter.cs
new file mode 100644
index 0000000..9a5455e
--- /dev/null
+++ b/Sample-WPF/Helpers/MainWindowLogTraceWriter.cs
@@ -0,0 +1,59 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Microsoft.ProjectOxford.Face.Helpers
+{
+ using Newtonsoft.Json.Serialization;
+ using System;
+ using System.Diagnostics;
+
+ class MainWindowLogTraceWriter : ITraceWriter
+ {
+ private TraceLevel _levelFilter;
+
+ public MainWindowLogTraceWriter(TraceLevel levelFilter = TraceLevel.Verbose)
+ {
+ _levelFilter = levelFilter;
+ }
+
+ public TraceLevel LevelFilter
+ {
+ get { return _levelFilter; }
+ }
+
+ public void Trace(TraceLevel level, string message, Exception ex)
+ {
+ MainWindow.Log(message);
+ }
+ }
+}
diff --git a/Sample-WPF/Helpers/RetryHelper.cs b/Sample-WPF/Helpers/RetryHelper.cs
new file mode 100644
index 0000000..a31adf4
--- /dev/null
+++ b/Sample-WPF/Helpers/RetryHelper.cs
@@ -0,0 +1,164 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Cognitive Services (formerly Project Oxford): https://www.microsoft.com/cognitive-services
+//
+// Microsoft Cognitive Services (formerly Project Oxford) GitHub:
+// https://github.com/Microsoft/Cognitive-Face-Windows
+//
+// 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.
+//
+
+namespace Microsoft.ProjectOxford.Face.Helpers
+{
+ using Newtonsoft.Json.Serialization;
+ using System;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ ///
+ /// This class retries on transient errors
+ ///
+ public class RetryHelper
+ {
+ ///
+ /// The singleton
+ ///
+ static RetryHelper _singleton;
+
+ ///
+ /// Prevents a default instance of the class from being created.
+ ///
+ private RetryHelper(){}
+
+ ///
+ /// Gets the current.
+ ///
+ ///
+ /// The current.
+ ///
+ public static RetryHelper Current
+ {
+ get
+ {
+ if (_singleton == null)
+ {
+ _singleton = new RetryHelper();
+ }
+ return _singleton;
+ }
+ }
+
+ ///
+ /// Operations the with basic retry asynchronous.
+ ///
+ ///
+ /// The asynchronous operation.
+ /// The transient exception types.
+ /// The retry delay milliseconds.
+ /// The maximum retries.
+ /// The trace writer.
+ ///
+ public static async Task OperationWithBasicRetryAsync(Func> asyncOperation, Type[] transientExceptionTypes, int retryDelayMilliseconds = 1000, int maxRetries = 60, ITraceWriter traceWriter = null)
+ {
+ int retryCount = 0;
+
+ while (true)
+ {
+ try
+ {
+ return await asyncOperation();
+ }
+ catch (Exception ex)
+ when (IsTransientError(ex, transientExceptionTypes))
+ {
+ if (traceWriter != null)
+ {
+ traceWriter.Trace(System.Diagnostics.TraceLevel.Error, $"Error: {ex.Message}. Retrying {retryCount}/{maxRetries}", ex);
+ }
+
+ if (++retryCount >= maxRetries)
+ {
+ throw;
+ }
+
+ Thread.Sleep(retryDelayMilliseconds);
+ }
+ }
+ }
+
+ ///
+ /// Voids the operation with basic retry asynchronous.
+ ///
+ /// The asynchronous operation.
+ /// The transient exception types.
+ /// The retry delay milliseconds.
+ /// The maximum retries.
+ /// The trace writer.
+ ///
+ public static async Task VoidOperationWithBasicRetryAsync(Func asyncOperation, Type[] transientExceptionTypes, int retryDelayMilliseconds = 1000, int maxRetries = 60, ITraceWriter traceWriter = null)
+ {
+ int retryCount = 0;
+
+ while (true)
+ {
+ try
+ {
+ await asyncOperation();
+ return;
+ }
+ catch (Exception ex)
+ when (IsTransientError(ex, transientExceptionTypes))
+ {
+ if (traceWriter != null)
+ {
+ traceWriter.Trace(System.Diagnostics.TraceLevel.Error, $"Error: {ex.Message}. Retrying {retryCount}/{maxRetries}", ex);
+ }
+
+ if (++retryCount >= maxRetries)
+ {
+ throw;
+ }
+
+ Thread.Sleep(retryDelayMilliseconds);
+ }
+ }
+ }
+
+ ///
+ /// Determines whether [is transient error] [the specified ex].
+ ///
+ /// The ex.
+ /// The transient exception types.
+ ///
+ /// true if [is transient error] [the specified ex]; otherwise, false.
+ ///
+ private static bool IsTransientError(Exception ex, Type[] transientExceptionTypes)
+ {
+ return transientExceptionTypes.Contains(ex.GetType());
+ }
+ }
+}