private async Task RunDetection(RecognitionTask task, CancellationToken cancellationToken)
        {
            task.DetectionTask.State = BaseTaskState.Running;
            try
            {
                var results = await _upstream.FindFacesV2(task.ImageStream, cancellationToken);

                var maxHeight = task.ImageInstance.Height;
                var maxWidth  = task.ImageInstance.Width;
                task.Faces = results.Select(position => new RecognizedFace
                {
                    Position = new FacePosition
                    {
                        X1 = Math.Max(position.X1, 0),
                        X2 = Math.Min(position.X2, maxWidth),
                        Y1 = Math.Max(position.Y1, 0),
                        Y2 = Math.Min(position.Y2, maxHeight)
                    }
                }).ToArray();

                task.DetectionTask.State = BaseTaskState.Succeeded;
            }
            catch
            {
                task.DetectionTask.State = BaseTaskState.Failed;
                throw;
            }
        }
        public async Task RunTaskAsync(RecognitionTask task, CancellationToken cancellationToken)
        {
            task.State = BaseTaskState.Running;
            try
            {
                await RunDetection(task, cancellationToken);

                if (task.FaceCount > 0)
                {
                    await RunVectorization(task, cancellationToken);
                    await RunSearch(task, cancellationToken);
                }

                task.State = BaseTaskState.Succeeded;
            }
            catch (Exception e)
            {
                if (e is OperationCanceledException && !cancellationToken.IsCancellationRequested)
                {
                    _logger.LogWarning("Upstream service timed out, or too slow to respond\n" +
                                       $"Detection.State={task.DetectionTask.State}, Time={task.DetectionTask.Time}\n" +
                                       $"Vectorization.State={task.VectorizationTask.State}, Time={task.VectorizationTask.Time}\n" +
                                       $"Search.State={task.SearchTask.State}, Time={task.SearchTask.Time}");
                }

                task.State = BaseTaskState.Failed;
                throw;
            }
        }
        public RecognitionTask Create(Stream image)
        {
            var task = new RecognitionTask(image);

            task.OnStateChanged += TaskOnStateChanged;
            return(task);
        }
        private async Task RunSearch(RecognitionTask task, CancellationToken cancellationToken)
        {
            task.SearchTask.State = BaseTaskState.Running;
            try
            {
                var request = task.Faces.ToDictionary(f => f.Id.ToString(), f => f.FeatureVector);
                var result  = await _upstream.SearchFacesByFeatureVectors(request, cancellationToken);

                foreach (var face in task.Faces)
                {
                    face.SearchResults = result[face.Id.ToString()];
                }

                task.SearchTask.State = BaseTaskState.Succeeded;
            }
            catch
            {
                task.SearchTask.State = BaseTaskState.Failed;
                throw;
            }
        }
        private async Task RunVectorization(RecognitionTask task, CancellationToken cancellationToken)
        {
            task.VectorizationTask.State = BaseTaskState.Running;
            try
            {
                await Task.WhenAll(task.Faces.Select(async face =>
                {
                    // crop image
                    var faceStream = await Task.Run(() => CropFaceImage(face, task.ImageInstance), cancellationToken);

                    // vectorization
                    face.FeatureVector = await _upstream.GetFaceFeatureVector(faceStream, cancellationToken);
                }));

                task.VectorizationTask.State = BaseTaskState.Succeeded;
            }
            catch
            {
                task.VectorizationTask.State = BaseTaskState.Failed;
                throw;
            }
        }