Skip to content

Commit

Permalink
Merge pull request #13 from regulaforensics/person-search
Browse files Browse the repository at this point in the history
Person search
  • Loading branch information
DzmitrySmaliakou authored Jul 10, 2023
2 parents 22c3c3f + 0331522 commit 1764d98
Show file tree
Hide file tree
Showing 28 changed files with 2,431 additions and 9 deletions.
88 changes: 88 additions & 0 deletions Catalog.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Catalog/Core/CatalogTableDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ final class CatalogTableDataProvider {
MatchFacesRequestItem(),
DetectFacesItem(),
ImageQualityItem(),

PersonDatabaseItem(),
LivenessAttemptsCountItem(),
FaceCaptureCameraPositionItem(),
FaceCaptureHideTorchConfigurationItem(),
Expand Down
22 changes: 22 additions & 0 deletions Catalog/Core/Extensions/Person+Metadata.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Person+Metadata.swift
// Catalog
//
// Created by Serge Rylko on 27.06.23.
// Copyright © 2023 Regula. All rights reserved.
//

import Foundation
import FaceSDK

extension PersonDatabase.Person {

var surname: String? {
get {
metadata["surname"] as? String
}
set {
metadata["surname"] = newValue
}
}
}
25 changes: 25 additions & 0 deletions Catalog/Core/Extensions/UIImageView+Load.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// UIImageView+Load.swift
// Catalog
//
// Created by Serge Rylko on 15.05.23.
// Copyright © 2023 Regula. All rights reserved.
//

import Foundation
import UIKit

extension UIImageView {

func load(url: URL) {
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self?.image = image
}
}
}.resume()
}
}

77 changes: 77 additions & 0 deletions Catalog/Core/ImagePicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// ImagePicker.swift
// Catalog
//
// Created by Serge Rylko on 26.08.22.
// Copyright © 2022 Regula. All rights reserved.
//

import Foundation
import UIKit

protocol ImagePickerDelegate: AnyObject {
func didPickImage(delegate: ImagePicker, image: UIImage)
}

final class ImagePicker: NSObject, UINavigationControllerDelegate {

private let pickerController = UIImagePickerController()
private unowned let presenter: UIViewController
private unowned let delegate: ImagePickerDelegate

init(presenter: UIViewController, delegate: ImagePickerDelegate) {
self.presenter = presenter
self.delegate = delegate
super.init()
setup()
}

private func setup() {
pickerController.delegate = self
pickerController.allowsEditing = true
}

private func action(for type: UIImagePickerController.SourceType, title: String) -> UIAlertAction? {
guard UIImagePickerController.isSourceTypeAvailable(type) else {
return nil
}

return UIAlertAction(title: title, style: .default) { [unowned self] _ in
self.pickerController.sourceType = type
self.presenter.present(self.pickerController, animated: true)
}
}

func presentDefaultActions(from sourceView: UIView) {
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)

if let action = self.action(for: .camera, title: "Take photo") {
alertController.addAction(action)
}
if let action = self.action(for: .savedPhotosAlbum, title: "Camera roll") {
alertController.addAction(action)
}
if let action = self.action(for: .photoLibrary, title: "Photo library") {
alertController.addAction(action)
}

alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))

if UIDevice.current.userInterfaceIdiom == .pad {
alertController.popoverPresentationController?.sourceView = sourceView
alertController.popoverPresentationController?.sourceRect = sourceView.bounds
alertController.popoverPresentationController?.permittedArrowDirections = [.down, .up]
}

presenter.present(alertController, animated: true)
}
}

extension ImagePicker: UIImagePickerControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[.editedImage] as? UIImage else { return }

delegate.didPickImage(delegate: self, image: image)
presenter.dismiss(animated: true)
}
}
206 changes: 206 additions & 0 deletions Catalog/Core/PersonDatabase/DatabaseCreatePersonViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//
// DatabaseCreatePersonViewController.swift
// Catalog
//
// Created by Serge Rylko on 21.06.23.
// Copyright © 2023 Regula. All rights reserved.
//

import UIKit
import FaceSDK

class DatabaseCreatePersonViewController: UIViewController {

@IBOutlet private weak var addImageButton: UIButton!
@IBOutlet private weak var createPersonButton: UIButton!
@IBOutlet private weak var nameTextField: UITextField!
@IBOutlet private weak var surnameTextField: UITextField!
@IBOutlet private weak var loadingIndicator: UIActivityIndicatorView!
@IBOutlet private weak var collectionView: UICollectionView!

private lazy var imagePicker: ImagePicker = ImagePicker(presenter: self, delegate: self)
private let database: PersonDatabase = FaceSDK.service.personDatabase

private var images: [UIImage] = []
private var name: String?
private var surname: String?
private let group: PersonDatabase.PersonGroup

init(group: PersonDatabase.PersonGroup) {
self.group = group
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
title = "Create Person in group"

let button = UIBarButtonItem(title: "Clear", style: .plain, target: self, action: #selector(didTapClearButton))
navigationItem.setRightBarButton(button, animated: false)

collectionView.register(PersonImageCell.self, forCellWithReuseIdentifier: PersonImageCell.reuseID)
}

//MARK: - Database
private func createPerson() {
showLoadingActivity()
createPerson { [weak self] result in
self?.hideLoadingActivity()
switch result {
case .success(let person):
self?.handleSuccess(person: person)
case .failure(let error):
self?.handleFailure(error: error)
}
}
}

private func createPerson(completion: @escaping ((Result<PersonDatabase.Person, Error>) -> Void)) {
guard let name = name, name.count > 0 else {
completion(.failure(CreatePersonError.requiredNameParameterMissed))
return
}
let metadata = ["surname": surname ?? ""]

database.createPerson(name: name, metadata: metadata, groupIds: [group.itemId]) { [weak self] response in
guard let self = self else { return }
if let person = response.item {
var error: Error?
let group = DispatchGroup()
for image in self.images {
guard let imageData = image.pngData() else { continue }
let upload = PersonDatabase.ImageUpload(imageData:imageData)
group.enter()
self.database.addPersonImage(personId: person.itemId, imageUpload: upload) { response in
error = response.error
group.leave()
}
}
group.notify(queue: .main) {
if let error = error {
completion(.failure(error))
} else {
completion(.success(person))
}
}
} else if let error = response.error {
completion(.failure(error))
}
}
}

//MARK: Actions
@IBAction private func didTapAddPersonImage(_ sender: Any) {
imagePicker.presentDefaultActions(from: view)
}

@IBAction private func didTapCreatePerson(_ sender: Any) {
createPerson()
}

@objc private func didTapClearButton(_ sender: Any) {
resetPersonData()
}

private func resetPersonData() {
name = nil
nameTextField.text = nil
surname = nil
surnameTextField.text = nil
images = []
collectionView.reloadData()
}

//MARK: - Dialogs
private func handleSuccess(person: PersonDatabase.Person) {
let message = "id: \(person.itemId) \nname: \(person.name) \nsurname: \(person.surname ?? "")"
let action = UIAlertAction(title: "OK", style: .cancel)
let alert = UIAlertController(title: "Person created.", message: message, preferredStyle: .alert)
alert.addAction(action)
present(alert, animated: true)
}

private func handleFailure(error: Error) {
let message = "Error: \(error.localizedDescription)"
let action = UIAlertAction(title: "OK", style: .cancel)
let alert = UIAlertController(title: "Failed to add Person.", message: message, preferredStyle: .alert)
alert.addAction(action)
present(alert, animated: true)
}

//MARK: - Loading states
private func showLoadingActivity() {
addImageButton.isEnabled = false
createPersonButton.isEnabled = false
loadingIndicator.startAnimating()
}

private func hideLoadingActivity() {
addImageButton.isEnabled = true
createPersonButton.isEnabled = true
loadingIndicator.stopAnimating()
}
}

extension DatabaseCreatePersonViewController: UITextFieldDelegate {

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
}

func textFieldDidEndEditing(_ textField: UITextField) {
switch textField {
case nameTextField:
name = textField.text
case surnameTextField:
surname = textField.text
default: break
}
}
}

extension DatabaseCreatePersonViewController: UICollectionViewDelegateFlowLayout {

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let divider = UIDevice.current.orientation.isPortrait ? 2.0 : 3.0
let side = collectionView.bounds.width / divider
let size = CGSize(width: side, height: side)
return size
}
}

extension DatabaseCreatePersonViewController: UICollectionViewDataSource {

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
images.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PersonImageCell.reuseID, for: indexPath) as? PersonImageCell else {
fatalError("Unable to reuse cell")
}

cell.imageView.image = images[indexPath.row]
return cell
}
}

extension DatabaseCreatePersonViewController: ImagePickerDelegate {

func didPickImage(delegate: ImagePicker, image: UIImage) {
images.append(image)
collectionView.reloadData()
}
}

private enum CreatePersonError: LocalizedError {
case requiredNameParameterMissed

var errorDescription: String? {
"Person name is required"
}
}
Loading

0 comments on commit 1764d98

Please sign in to comment.