├── .gitignore ├── tests └── XSolve │ └── FaceValidatorBundle │ ├── images │ ├── 1x1 │ └── 100x100 │ ├── Integration │ ├── config_test.yml │ ├── AzureClientMock.php │ ├── TestKernel.php │ ├── KernelTestCase.php │ ├── responses │ │ ├── glasses.json │ │ ├── sunglasses.json │ │ ├── small_face.json │ │ ├── medium_blur.json │ │ └── medium_noise.json │ └── FaceValidatorIntegrationTest.php │ ├── Calculator │ └── FaceToImageRatioCalculatorTest.php │ ├── Detector │ └── AzureAPIFaceDetectorTest.php │ ├── Result │ ├── BlurLevelTest.php │ └── NoiseLevelTest.php │ └── Validator │ └── Constraints │ └── FaceValidatorTest.php ├── src ├── .htaccess └── XSolve │ └── FaceValidatorBundle │ ├── Exception │ └── NoFaceDetectedException.php │ ├── Result │ ├── Gender.php │ ├── Accessory.php │ ├── ExposureLevel.php │ ├── Glasses.php │ ├── Makeup.php │ ├── Blur.php │ ├── Noise.php │ ├── Exposure.php │ ├── BlurLevel.php │ ├── NoiseLevel.php │ ├── Rotation.php │ └── FaceDetectionResult.php │ ├── Client │ ├── AzureFaceAPIClient.php │ └── GuzzleWrapper.php │ ├── XSolveFaceValidatorBundle.php │ ├── Detector │ ├── FaceDetector.php │ └── AzureAPIFaceDetector.php │ ├── Validator │ ├── Specification │ │ ├── FaceValidationSpecification.php │ │ ├── HairIsVisible.php │ │ ├── FaceIsOfSufficientSize.php │ │ ├── NoMakeup.php │ │ ├── Evaluation.php │ │ ├── IsNotWearingGlasses.php │ │ ├── IsNotWearingSunglasses.php │ │ ├── FaceIsNotCovered.php │ │ ├── BlurIsAcceptable.php │ │ ├── NoiseIsAcceptable.php │ │ └── FaceIsOfAcceptableAngle.php │ └── Constraints │ │ ├── FaceValidator.php │ │ └── Face.php │ ├── Calculator │ └── FaceToImageRatioCalculator.php │ ├── DependencyInjection │ ├── AllowedRegions.php │ ├── Configuration.php │ └── XSolveFaceValidatorExtension.php │ ├── Factory │ └── FaceDetectionResultFactory.php │ └── Resources │ └── config │ └── services.yml ├── .travis.yml ├── .php_cs ├── phpunit.xml.dist ├── LICENSE ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /phpunit.xml 3 | /vendor/ 4 | /.php_cs.cache 5 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/images/1x1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/xsolve-face-validator-bundle/HEAD/tests/XSolve/FaceValidatorBundle/images/1x1 -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/images/100x100: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boldare/xsolve-face-validator-bundle/HEAD/tests/XSolve/FaceValidatorBundle/images/100x100 -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Exception/NoFaceDetectedException.php: -------------------------------------------------------------------------------- 1 | onFace = $onFace; 20 | $this->onLips = $onLips; 21 | } 22 | 23 | public function isOnFace(): bool 24 | { 25 | return $this->onFace; 26 | } 27 | 28 | public function isOnLips(): bool 29 | { 30 | return $this->onLips; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Result/Blur.php: -------------------------------------------------------------------------------- 1 | level = $level; 20 | $this->value = $value; 21 | } 22 | 23 | public function getLevel(): BlurLevel 24 | { 25 | return $this->level; 26 | } 27 | 28 | public function getValue(): float 29 | { 30 | return $this->value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Result/Noise.php: -------------------------------------------------------------------------------- 1 | level = $level; 20 | $this->value = $value; 21 | } 22 | 23 | public function getLevel(): NoiseLevel 24 | { 25 | return $this->level; 26 | } 27 | 28 | public function getValue(): float 29 | { 30 | return $this->value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Specification/HairIsVisible.php: -------------------------------------------------------------------------------- 1 | allowNoHair || $result->isHairVisible()) { 13 | return new Evaluation(true); 14 | } 15 | 16 | return new Evaluation(false, $constraint->hairCoveredMessage); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Result/Exposure.php: -------------------------------------------------------------------------------- 1 | level = $level; 20 | $this->value = $value; 21 | } 22 | 23 | public function getLevel(): ExposureLevel 24 | { 25 | return $this->level; 26 | } 27 | 28 | public function getValue(): float 29 | { 30 | return $this->value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Specification/FaceIsOfSufficientSize.php: -------------------------------------------------------------------------------- 1 | getFaceToImageRatio() >= $constraint->minFaceRatio) { 13 | return new Evaluation(true); 14 | } 15 | 16 | return new Evaluation(false, $constraint->faceTooSmallMessage); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Specification/NoMakeup.php: -------------------------------------------------------------------------------- 1 | getMakeup(); 13 | 14 | if ($constraint->allowMakeup || ($makeup->isOnFace() && !$makeup->isOnLips())) { 15 | return new Evaluation(true); 16 | } 17 | 18 | return new Evaluation(false, $constraint->makeupMessage); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Result/BlurLevel.php: -------------------------------------------------------------------------------- 1 | successful = $successful; 20 | $this->message = $message; 21 | } 22 | 23 | public function isSuccessful(): bool 24 | { 25 | return $this->successful; 26 | } 27 | 28 | public function getMessage(): string 29 | { 30 | return $this->message; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Specification/IsNotWearingGlasses.php: -------------------------------------------------------------------------------- 1 | allowGlasses || Glasses::NONE() == $result->getGlasses()) { 14 | return new Evaluation(true); 15 | } 16 | 17 | return new Evaluation(false, $constraint->glassesMessage); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Specification/IsNotWearingSunglasses.php: -------------------------------------------------------------------------------- 1 | allowSunglasses || Glasses::SUN() != $result->getGlasses()) { 14 | return new Evaluation(true); 15 | } 16 | 17 | return new Evaluation(false, $constraint->sunglassesMessage); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Specification/FaceIsNotCovered.php: -------------------------------------------------------------------------------- 1 | allowCoveringFace || !in_array(Accessory::MASK(), $result->getAccessories())) { 14 | return new Evaluation(true); 15 | } 16 | 17 | return new Evaluation(false, $constraint->faceCoveredMessage); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRules([ 5 | '@Symfony' => true, 6 | 'array_syntax' => ['syntax' => 'short'], 7 | 'phpdoc_add_missing_param_annotation' => true, 8 | 'linebreak_after_opening_tag' => true, 9 | 'phpdoc_annotation_without_dot' => false, 10 | 'phpdoc_summary' => false, 11 | 'phpdoc_no_package' => false, 12 | 'phpdoc_order' => true, 13 | 'pre_increment' => false, 14 | 'phpdoc_align' => false, 15 | ]) 16 | ->setFinder( 17 | PhpCsFixer\Finder::create() 18 | ->in('./src') 19 | ); 20 | 21 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Integration/AzureClientMock.php: -------------------------------------------------------------------------------- 1 | responseData; 24 | } 25 | 26 | public function setResponseData(array $responseData) 27 | { 28 | $this->responseData = $responseData; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Result/Rotation.php: -------------------------------------------------------------------------------- 1 | x = $x; 25 | $this->y = $y; 26 | $this->z = $z; 27 | } 28 | 29 | public function getX(): float 30 | { 31 | return $this->x; 32 | } 33 | 34 | public function getY(): float 35 | { 36 | return $this->y; 37 | } 38 | 39 | public function getZ(): float 40 | { 41 | return $this->z; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Specification/BlurIsAcceptable.php: -------------------------------------------------------------------------------- 1 | maxBlurLevel); 14 | 15 | if ($result->getBlur()->getLevel()->isLowerOrEqual($acceptableLevel)) { 16 | return new Evaluation(true); 17 | } 18 | 19 | return new Evaluation(false, $constraint->blurredMessage); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Specification/NoiseIsAcceptable.php: -------------------------------------------------------------------------------- 1 | maxNoiseLevel); 14 | $actualLevel = $result->getNoise()->getLevel(); 15 | 16 | if ($actualLevel->isLowerOrEqual($acceptableLevel)) { 17 | return new Evaluation(true); 18 | } 19 | 20 | return new Evaluation(false, $constraint->noiseMessage); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Specification/FaceIsOfAcceptableAngle.php: -------------------------------------------------------------------------------- 1 | getRotation(); 13 | 14 | foreach ([$rotation->getX(), $rotation->getY(), $rotation->getZ()] as $rotationValue) { 15 | if ($rotationValue > $constraint->maxFaceRotation) { 16 | return new Evaluation(false, $constraint->tooMuchRotatedMessage); 17 | } 18 | } 19 | 20 | return new Evaluation(true); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Integration/TestKernel.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/config_test.yml'); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getCacheDir() 35 | { 36 | return $this->rootDir.'/cache'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('xsolve_face_validator'); 17 | 18 | $rootNode 19 | ->children() 20 | ->scalarNode('azure_subscription_key') 21 | ->isRequired() 22 | ->cannotBeEmpty() 23 | ->end() 24 | ->enumNode('region') 25 | ->isRequired() 26 | ->values(AllowedRegions::NAMES) 27 | ->end() 28 | ->end(); 29 | 30 | return $treeBuilder; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | tests 17 | 18 | 19 | 20 | 21 | 22 | src 23 | 24 | src/XSolve/FaceValidatorBundle/DependencyInjection 25 | src/XSolve/FaceValidatorBundle/Resources 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 XSolve Sp. z o.o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Detector/AzureAPIFaceDetector.php: -------------------------------------------------------------------------------- 1 | client = $client; 25 | $this->resultFactory = $resultFactory; 26 | } 27 | 28 | public function detect(string $filePath): FaceDetectionResult 29 | { 30 | $data = $this->client->detect($filePath); 31 | 32 | if (empty($data)) { 33 | throw new NoFaceDetectedException(); 34 | } 35 | 36 | return $this->resultFactory->create($filePath, $data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Integration/KernelTestCase.php: -------------------------------------------------------------------------------- 1 | 'test', 'debug' => true]); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public static function tearDownAfterClass() 22 | { 23 | static::ensureKernelShutdown(); 24 | static::removeDirWithFiles(static::$kernel->getCacheDir()); 25 | static::removeDirWithFiles(static::$kernel->getLogDir()); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function tearDown() 32 | { 33 | // shutting down the kernel after each test (which is done in parent class) is not really efficient 34 | } 35 | 36 | private static function removeDirWithFiles(string $path) 37 | { 38 | (new Filesystem())->remove($path); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/DependencyInjection/XSolveFaceValidatorExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 19 | 20 | $container->setParameter('xsolve_face_validator.client.azure.subscription_key', $config['azure_subscription_key']); 21 | $container->setParameter( 22 | 'xsolve_face_validator.client.azure.base_uri', 23 | sprintf('https://%s.api.cognitive.microsoft.com/face/v1.0/', $config['region']) 24 | ); 25 | 26 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 27 | $loader->load('services.yml'); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getAlias() 34 | { 35 | return 'xsolve_face_validator'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xsolve-pl/face-validator-bundle", 3 | "description": "Symfony3 bundle for validating face on picture using MS Azure Face API", 4 | "license": "MIT", 5 | "homepage": "https://xsolve.software", 6 | "type": "symfony-bundle", 7 | "keywords": ["face", "validator", "detect", "recognition", "machine learning", "azure"], 8 | "authors": [ 9 | { 10 | "name": "Pawel Krynicki", 11 | "email": "pawel.krynicki@xsolve.software" 12 | }, 13 | { 14 | "name": "Michal Nicinski", 15 | "email": "michal.nicinski@xsolve.software" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.0.0", 20 | "guzzlehttp/guzzle": "~6.3", 21 | "myclabs/php-enum": "~1.5", 22 | "symfony/http-kernel": "3.3.*", 23 | "symfony/config": "3.3.*", 24 | "symfony/dependency-injection": "3.3.*", 25 | "symfony/validator": "3.3.*", 26 | "symfony/property-access": "3.3.*" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^6.2", 30 | "symfony/framework-bundle": "3.3.*", 31 | "symfony/yaml": "3.3.*", 32 | "friendsofphp/php-cs-fixer": "^2.7" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "XSolve\\": "src/XSolve" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Tests\\": "tests/" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Calculator/FaceToImageRatioCalculatorTest.php: -------------------------------------------------------------------------------- 1 | calculator = new FaceToImageRatioCalculator(); 23 | } 24 | 25 | /** 26 | * @group integration 27 | * @requires extension gd 28 | * @dataProvider calculateProvider 29 | */ 30 | public function testCalculate(string $path, int $faceWidth, int $faceHeight, float $expectedResult) 31 | { 32 | $this->assertSame($expectedResult, $this->calculator->calculate($path, $faceWidth, $faceHeight)); 33 | } 34 | 35 | public function calculateProvider(): array 36 | { 37 | return [ 38 | [$this->generateImagePath('100x100'), 0, 0, 0.0], 39 | [$this->generateImagePath('100x100'), 100, 100, 1.0], 40 | [$this->generateImagePath('100x100'), 100, 50, 0.5], 41 | [$this->generateImagePath('100x100'), 50, 50, 0.25], 42 | ]; 43 | } 44 | 45 | private function generateImagePath(string $imageName): string 46 | { 47 | return sprintf('%s/%s', self::IMAGES_DIRECTORY, $imageName); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Detector/AzureAPIFaceDetectorTest.php: -------------------------------------------------------------------------------- 1 | client = $this->prophesize(AzureFaceAPIClient::class); 35 | $this->resultFactory = $this->prophesize(FaceDetectionResultFactory::class); 36 | $this->detector = new AzureAPIFaceDetector($this->client->reveal(), $this->resultFactory->reveal()); 37 | } 38 | 39 | public function testDetect() 40 | { 41 | $result = $this->prophesize(FaceDetectionResult::class)->reveal(); 42 | $this->client->detect('test file path')->willReturn(['data']); 43 | $this->resultFactory->create('test file path', ['data'])->willReturn($result); 44 | 45 | $this->assertSame($result, $this->detector->detect('test file path')); 46 | } 47 | 48 | /** 49 | * @expectedException \XSolve\FaceValidatorBundle\Exception\NoFaceDetectedException 50 | */ 51 | public function testDetectNoResult() 52 | { 53 | $this->client->detect('test file path')->willReturn([]); 54 | 55 | $this->detector->detect('test file path'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Client/GuzzleWrapper.php: -------------------------------------------------------------------------------- 1 | client = $client; 30 | $this->subscriptionKey = $subscriptionKey; 31 | } 32 | 33 | public function detect( 34 | string $filePath, 35 | bool $returnFaceId = false, 36 | bool $returnFaceLandmarks = true, 37 | array $returnFaceAttributes = null 38 | ): array { 39 | $fs = fopen($filePath, 'r'); 40 | 41 | if (null === $returnFaceAttributes) { 42 | $returnFaceAttributes = self::$faceAttributes; 43 | } 44 | 45 | $response = $this->client->request('POST', 'detect', [ 46 | 'headers' => [ 47 | 'Content-Type' => 'application/octet-stream', 48 | 'Ocp-Apim-Subscription-Key' => $this->subscriptionKey, 49 | ], 50 | 'query' => [ 51 | 'returnFaceId' => $returnFaceId, 52 | 'returnFaceLandmarks' => $returnFaceLandmarks, 53 | 'returnFaceAttributes' => implode(',', $returnFaceAttributes), 54 | ], 55 | 'body' => $fs, 56 | ]); 57 | $data = json_decode($response->getBody()->getContents(), true); 58 | 59 | return $data ? current($data) : []; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Result/BlurLevelTest.php: -------------------------------------------------------------------------------- 1 | testedBlurLevel = BlurLevel::LOW(); 20 | $this->assertTrue($this->testedBlurLevel->isLowerOrEqual($lowBlurLevel)); 21 | 22 | $this->testedBlurLevel = BlurLevel::MEDIUM(); 23 | $this->assertNotTrue($this->testedBlurLevel->isLowerOrEqual($lowBlurLevel)); 24 | 25 | $this->testedBlurLevel = BlurLevel::HIGH(); 26 | $this->assertNotTrue($this->testedBlurLevel->isLowerOrEqual($lowBlurLevel)); 27 | } 28 | 29 | public function testIsLowerOrEqualToMediumBlurLvl() 30 | { 31 | $mediumBlurLevel = BlurLevel::MEDIUM(); 32 | 33 | $this->testedBlurLevel = BlurLevel::LOW(); 34 | $this->assertTrue($this->testedBlurLevel->isLowerOrEqual($mediumBlurLevel)); 35 | 36 | $this->testedBlurLevel = BlurLevel::MEDIUM(); 37 | $this->assertTrue($this->testedBlurLevel->isLowerOrEqual($mediumBlurLevel)); 38 | 39 | $this->testedBlurLevel = BlurLevel::HIGH(); 40 | $this->assertNotTrue($this->testedBlurLevel->isLowerOrEqual($mediumBlurLevel)); 41 | } 42 | 43 | public function testIsLowerOrEqualToHighBlurLvl() 44 | { 45 | $highBlurLevel = BlurLevel::HIGH(); 46 | 47 | $this->testedBlurLevel = BlurLevel::LOW(); 48 | $this->assertTrue($this->testedBlurLevel->isLowerOrEqual($highBlurLevel)); 49 | 50 | $this->testedBlurLevel = BlurLevel::MEDIUM(); 51 | $this->assertTrue($this->testedBlurLevel->isLowerOrEqual($highBlurLevel)); 52 | 53 | $this->testedBlurLevel = BlurLevel::HIGH(); 54 | $this->assertTrue($this->testedBlurLevel->isLowerOrEqual($highBlurLevel)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Result/NoiseLevelTest.php: -------------------------------------------------------------------------------- 1 | testedNoiseLevel = NoiseLevel::LOW(); 20 | $this->assertTrue($this->testedNoiseLevel->isLowerOrEqual($lowNoiseLevel)); 21 | 22 | $this->testedNoiseLevel = NoiseLevel::MEDIUM(); 23 | $this->assertNotTrue($this->testedNoiseLevel->isLowerOrEqual($lowNoiseLevel)); 24 | 25 | $this->testedNoiseLevel = NoiseLevel::HIGH(); 26 | $this->assertNotTrue($this->testedNoiseLevel->isLowerOrEqual($lowNoiseLevel)); 27 | } 28 | 29 | public function testIsLowerOrEqualToMediumNoiseLvl() 30 | { 31 | $mediumNoiseLevel = NoiseLevel::MEDIUM(); 32 | 33 | $this->testedNoiseLevel = NoiseLevel::LOW(); 34 | $this->assertTrue($this->testedNoiseLevel->isLowerOrEqual($mediumNoiseLevel)); 35 | 36 | $this->testedNoiseLevel = NoiseLevel::MEDIUM(); 37 | $this->assertTrue($this->testedNoiseLevel->isLowerOrEqual($mediumNoiseLevel)); 38 | 39 | $this->testedNoiseLevel = NoiseLevel::HIGH(); 40 | $this->assertNotTrue($this->testedNoiseLevel->isLowerOrEqual($mediumNoiseLevel)); 41 | } 42 | 43 | public function testIsLowerOrEqualToHighNoiseLvl() 44 | { 45 | $highNoiseLevel = NoiseLevel::HIGH(); 46 | 47 | $this->testedNoiseLevel = NoiseLevel::LOW(); 48 | $this->assertTrue($this->testedNoiseLevel->isLowerOrEqual($highNoiseLevel)); 49 | 50 | $this->testedNoiseLevel = NoiseLevel::MEDIUM(); 51 | $this->assertTrue($this->testedNoiseLevel->isLowerOrEqual($highNoiseLevel)); 52 | 53 | $this->testedNoiseLevel = NoiseLevel::HIGH(); 54 | $this->assertTrue($this->testedNoiseLevel->isLowerOrEqual($highNoiseLevel)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Integration/responses/glasses.json: -------------------------------------------------------------------------------- 1 | { 2 | "faceId": "glasses", 3 | "faceRectangle": { 4 | "top": 83, 5 | "left": 76, 6 | "width": 97, 7 | "height": 97 8 | }, 9 | "faceAttributes": { 10 | "smile": 0.112, 11 | "headPose": { 12 | "pitch": 0, 13 | "roll": -4, 14 | "yaw": 0.3 15 | }, 16 | "gender": "male", 17 | "age": 68.9, 18 | "facialHair": { 19 | "moustache": 0, 20 | "beard": 0, 21 | "sideburns": 0 22 | }, 23 | "glasses": "ReadingGlasses", 24 | "emotion": { 25 | "anger": 0, 26 | "contempt": 0, 27 | "disgust": 0, 28 | "fear": 0, 29 | "happiness": 0.112, 30 | "neutral": 0.857, 31 | "sadness": 0.03, 32 | "surprise": 0 33 | }, 34 | "blur": { 35 | "blurLevel": "low", 36 | "value": 0.19 37 | }, 38 | "exposure": { 39 | "exposureLevel": "goodExposure", 40 | "value": 0.68 41 | }, 42 | "noise": { 43 | "noiseLevel": "low", 44 | "value": 0.21 45 | }, 46 | "makeup": { 47 | "eyeMakeup": false, 48 | "lipMakeup": false 49 | }, 50 | "accessories": [{ 51 | "type": "glasses", 52 | "confidence": 0.99 53 | }], 54 | "occlusion": { 55 | "foreheadOccluded": false, 56 | "eyeOccluded": false, 57 | "mouthOccluded": false 58 | }, 59 | "hair": { 60 | "bald": 0.02, 61 | "invisible": false, 62 | "hairColor": [{ 63 | "color": "brown", 64 | "confidence": 0.99 65 | }, { 66 | "color": "blond", 67 | "confidence": 0.59 68 | }, { 69 | "color": "gray", 70 | "confidence": 0.59 71 | }, { 72 | "color": "red", 73 | "confidence": 0.2 74 | }, { 75 | "color": "black", 76 | "confidence": 0.19 77 | }, { 78 | "color": "other", 79 | "confidence": 0.11 80 | }] 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Integration/responses/sunglasses.json: -------------------------------------------------------------------------------- 1 | { 2 | "faceId": "sunglasses", 3 | "faceRectangle": { 4 | "top": 83, 5 | "left": 76, 6 | "width": 97, 7 | "height": 97 8 | }, 9 | "faceAttributes": { 10 | "smile": 0.112, 11 | "headPose": { 12 | "pitch": 0, 13 | "roll": -4, 14 | "yaw": 0.3 15 | }, 16 | "gender": "male", 17 | "age": 68.9, 18 | "facialHair": { 19 | "moustache": 0, 20 | "beard": 0, 21 | "sideburns": 0 22 | }, 23 | "glasses": "Sunglasses", 24 | "emotion": { 25 | "anger": 0, 26 | "contempt": 0, 27 | "disgust": 0, 28 | "fear": 0, 29 | "happiness": 0.112, 30 | "neutral": 0.857, 31 | "sadness": 0.03, 32 | "surprise": 0 33 | }, 34 | "blur": { 35 | "blurLevel": "low", 36 | "value": 0.19 37 | }, 38 | "exposure": { 39 | "exposureLevel": "goodExposure", 40 | "value": 0.68 41 | }, 42 | "noise": { 43 | "noiseLevel": "low", 44 | "value": 0.21 45 | }, 46 | "makeup": { 47 | "eyeMakeup": false, 48 | "lipMakeup": false 49 | }, 50 | "accessories": [{ 51 | "type": "glasses", 52 | "confidence": 0.99 53 | }], 54 | "occlusion": { 55 | "foreheadOccluded": false, 56 | "eyeOccluded": false, 57 | "mouthOccluded": false 58 | }, 59 | "hair": { 60 | "bald": 0.02, 61 | "invisible": false, 62 | "hairColor": [{ 63 | "color": "brown", 64 | "confidence": 0.99 65 | }, { 66 | "color": "blond", 67 | "confidence": 0.59 68 | }, { 69 | "color": "gray", 70 | "confidence": 0.59 71 | }, { 72 | "color": "red", 73 | "confidence": 0.2 74 | }, { 75 | "color": "black", 76 | "confidence": 0.19 77 | }, { 78 | "color": "other", 79 | "confidence": 0.11 80 | }] 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Integration/responses/small_face.json: -------------------------------------------------------------------------------- 1 | { 2 | "faceId": "small_face", 3 | "faceRectangle": { 4 | "top": 25, 5 | "left": 25, 6 | "width": 10, 7 | "height": 10 8 | }, 9 | "faceAttributes": { 10 | "smile": 0.112, 11 | "headPose": { 12 | "pitch": 0, 13 | "roll": -4, 14 | "yaw": 0.3 15 | }, 16 | "gender": "male", 17 | "age": 68.9, 18 | "facialHair": { 19 | "moustache": 0, 20 | "beard": 0, 21 | "sideburns": 0 22 | }, 23 | "glasses": "ReadingGlasses", 24 | "emotion": { 25 | "anger": 0, 26 | "contempt": 0, 27 | "disgust": 0, 28 | "fear": 0, 29 | "happiness": 0.112, 30 | "neutral": 0.857, 31 | "sadness": 0.03, 32 | "surprise": 0 33 | }, 34 | "blur": { 35 | "blurLevel": "low", 36 | "value": 0.19 37 | }, 38 | "exposure": { 39 | "exposureLevel": "goodExposure", 40 | "value": 0.68 41 | }, 42 | "noise": { 43 | "noiseLevel": "low", 44 | "value": 0.21 45 | }, 46 | "makeup": { 47 | "eyeMakeup": false, 48 | "lipMakeup": false 49 | }, 50 | "accessories": [{ 51 | "type": "glasses", 52 | "confidence": 0.99 53 | }], 54 | "occlusion": { 55 | "foreheadOccluded": false, 56 | "eyeOccluded": false, 57 | "mouthOccluded": false 58 | }, 59 | "hair": { 60 | "bald": 0.02, 61 | "invisible": false, 62 | "hairColor": [{ 63 | "color": "brown", 64 | "confidence": 0.99 65 | }, { 66 | "color": "blond", 67 | "confidence": 0.59 68 | }, { 69 | "color": "gray", 70 | "confidence": 0.59 71 | }, { 72 | "color": "red", 73 | "confidence": 0.2 74 | }, { 75 | "color": "black", 76 | "confidence": 0.19 77 | }, { 78 | "color": "other", 79 | "confidence": 0.11 80 | }] 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Integration/responses/medium_blur.json: -------------------------------------------------------------------------------- 1 | { 2 | "faceId": "medium_blur", 3 | "faceRectangle": { 4 | "top": 83, 5 | "left": 76, 6 | "width": 97, 7 | "height": 97 8 | }, 9 | "faceAttributes": { 10 | "smile": 0.112, 11 | "headPose": { 12 | "pitch": 0, 13 | "roll": -4, 14 | "yaw": 0.3 15 | }, 16 | "gender": "male", 17 | "age": 68.9, 18 | "facialHair": { 19 | "moustache": 0, 20 | "beard": 0, 21 | "sideburns": 0 22 | }, 23 | "glasses": "ReadingGlasses", 24 | "emotion": { 25 | "anger": 0, 26 | "contempt": 0, 27 | "disgust": 0, 28 | "fear": 0, 29 | "happiness": 0.112, 30 | "neutral": 0.857, 31 | "sadness": 0.03, 32 | "surprise": 0 33 | }, 34 | "blur": { 35 | "blurLevel": "medium", 36 | "value": 0.38 37 | }, 38 | "exposure": { 39 | "exposureLevel": "goodExposure", 40 | "value": 0.68 41 | }, 42 | "noise": { 43 | "noiseLevel": "low", 44 | "value": 0.21 45 | }, 46 | "makeup": { 47 | "eyeMakeup": false, 48 | "lipMakeup": false 49 | }, 50 | "accessories": [{ 51 | "type": "glasses", 52 | "confidence": 0.99 53 | }], 54 | "occlusion": { 55 | "foreheadOccluded": false, 56 | "eyeOccluded": false, 57 | "mouthOccluded": false 58 | }, 59 | "hair": { 60 | "bald": 0.02, 61 | "invisible": false, 62 | "hairColor": [{ 63 | "color": "brown", 64 | "confidence": 0.99 65 | }, { 66 | "color": "blond", 67 | "confidence": 0.59 68 | }, { 69 | "color": "gray", 70 | "confidence": 0.59 71 | }, { 72 | "color": "red", 73 | "confidence": 0.2 74 | }, { 75 | "color": "black", 76 | "confidence": 0.19 77 | }, { 78 | "color": "other", 79 | "confidence": 0.11 80 | }] 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Integration/responses/medium_noise.json: -------------------------------------------------------------------------------- 1 | { 2 | "faceId": "medium_noise", 3 | "faceRectangle": { 4 | "top": 83, 5 | "left": 76, 6 | "width": 97, 7 | "height": 97 8 | }, 9 | "faceAttributes": { 10 | "smile": 0.112, 11 | "headPose": { 12 | "pitch": 0, 13 | "roll": -4, 14 | "yaw": 0.3 15 | }, 16 | "gender": "male", 17 | "age": 68.9, 18 | "facialHair": { 19 | "moustache": 0, 20 | "beard": 0, 21 | "sideburns": 0 22 | }, 23 | "glasses": "ReadingGlasses", 24 | "emotion": { 25 | "anger": 0, 26 | "contempt": 0, 27 | "disgust": 0, 28 | "fear": 0, 29 | "happiness": 0.112, 30 | "neutral": 0.857, 31 | "sadness": 0.03, 32 | "surprise": 0 33 | }, 34 | "blur": { 35 | "blurLevel": "low", 36 | "value": 0.19 37 | }, 38 | "exposure": { 39 | "exposureLevel": "goodExposure", 40 | "value": 0.68 41 | }, 42 | "noise": { 43 | "noiseLevel": "medium", 44 | "value": 0.51 45 | }, 46 | "makeup": { 47 | "eyeMakeup": false, 48 | "lipMakeup": false 49 | }, 50 | "accessories": [{ 51 | "type": "glasses", 52 | "confidence": 0.99 53 | }], 54 | "occlusion": { 55 | "foreheadOccluded": false, 56 | "eyeOccluded": false, 57 | "mouthOccluded": false 58 | }, 59 | "hair": { 60 | "bald": 0.02, 61 | "invisible": false, 62 | "hairColor": [{ 63 | "color": "brown", 64 | "confidence": 0.99 65 | }, { 66 | "color": "blond", 67 | "confidence": 0.59 68 | }, { 69 | "color": "gray", 70 | "confidence": 0.59 71 | }, { 72 | "color": "red", 73 | "confidence": 0.2 74 | }, { 75 | "color": "black", 76 | "confidence": 0.19 77 | }, { 78 | "color": "other", 79 | "confidence": 0.11 80 | }] 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Constraints/FaceValidator.php: -------------------------------------------------------------------------------- 1 | faceDetector = $faceDetector; 28 | $this->conditions = $conditions; 29 | } 30 | 31 | public function validate($value, Constraint $constraint) 32 | { 33 | if (!$constraint instanceof Face) { 34 | throw new UnexpectedTypeException($constraint, Face::class); 35 | } 36 | 37 | $path = $this->extractPath($value); 38 | 39 | if (null === $path) { 40 | return; 41 | } 42 | 43 | try { 44 | $detectionResult = $this->faceDetector->detect($path); 45 | } catch (NoFaceDetectedException $e) { 46 | $this->context->addViolation($constraint->noFaceMessage); 47 | 48 | return; 49 | } 50 | 51 | $this->evaluateConditions($detectionResult, $constraint); 52 | } 53 | 54 | private function extractPath($value) 55 | { 56 | if (is_string($value)) { 57 | return $value; 58 | } 59 | 60 | if ($value instanceof \SplFileInfo && $value->isFile()) { 61 | return $value->getRealPath(); 62 | } 63 | 64 | return null; 65 | } 66 | 67 | private function evaluateConditions(FaceDetectionResult $result, Face $constraint) 68 | { 69 | foreach ($this->conditions as $condition) { 70 | $evaluation = $condition->evaluate($result, $constraint); 71 | 72 | if (!$evaluation->isSuccessful()) { 73 | $this->context->addViolation($evaluation->getMessage()); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Validator/Constraints/Face.php: -------------------------------------------------------------------------------- 1 | faceToImageRatio = $faceToImageRatio; 64 | $this->rotation = $rotation; 65 | $this->glasses = $glasses; 66 | $this->blur = $blur; 67 | $this->exposure = $exposure; 68 | $this->noise = $noise; 69 | $this->makeup = $makeup; 70 | $this->accessories = $accessories; 71 | $this->hairVisible = $hairVisible; 72 | } 73 | 74 | public function getFaceToImageRatio(): float 75 | { 76 | return $this->faceToImageRatio; 77 | } 78 | 79 | public function getRotation(): Rotation 80 | { 81 | return $this->rotation; 82 | } 83 | 84 | public function getGlasses(): Glasses 85 | { 86 | return $this->glasses; 87 | } 88 | 89 | public function getBlur(): Blur 90 | { 91 | return $this->blur; 92 | } 93 | 94 | public function getExposure(): Exposure 95 | { 96 | return $this->exposure; 97 | } 98 | 99 | public function getNoise(): Noise 100 | { 101 | return $this->noise; 102 | } 103 | 104 | public function getMakeup(): Makeup 105 | { 106 | return $this->makeup; 107 | } 108 | 109 | public function getAccessories(): array 110 | { 111 | return $this->accessories; 112 | } 113 | 114 | public function isHairVisible(): bool 115 | { 116 | return $this->hairVisible; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Factory/FaceDetectionResultFactory.php: -------------------------------------------------------------------------------- 1 | propertyAccessor = $propertyAccessor; 34 | $this->faceToImageRatioCalculator = $faceToImageRatioCalculator; 35 | } 36 | 37 | public function create(string $imagePath, array $data): FaceDetectionResult 38 | { 39 | return new FaceDetectionResult( 40 | $this->faceToImageRatioCalculator->calculate( 41 | $imagePath, 42 | $this->propertyAccessor->getValue($data, '[faceRectangle][width]'), 43 | $this->propertyAccessor->getValue($data, '[faceRectangle][height]') 44 | ), 45 | new Rotation( 46 | $this->propertyAccessor->getValue($data, '[faceAttributes][headPose][pitch]'), 47 | $this->propertyAccessor->getValue($data, '[faceAttributes][headPose][roll]'), 48 | $this->propertyAccessor->getValue($data, '[faceAttributes][headPose][yaw]') 49 | ), 50 | new Glasses( 51 | $this->propertyAccessor->getValue($data, '[faceAttributes][glasses]') 52 | ), 53 | new Blur( 54 | new BlurLevel( 55 | $this->propertyAccessor->getValue($data, '[faceAttributes][blur][blurLevel]') 56 | ), 57 | $this->propertyAccessor->getValue($data, '[faceAttributes][blur][value]') 58 | ), 59 | new Exposure( 60 | new ExposureLevel( 61 | $this->propertyAccessor->getValue($data, '[faceAttributes][exposure][exposureLevel]') 62 | ), 63 | $this->propertyAccessor->getValue($data, '[faceAttributes][exposure][value]') 64 | ), 65 | new Noise( 66 | new NoiseLevel( 67 | $this->propertyAccessor->getValue($data, '[faceAttributes][noise][noiseLevel]') 68 | ), 69 | $this->propertyAccessor->getValue($data, '[faceAttributes][noise][value]') 70 | ), 71 | new Makeup( 72 | $this->propertyAccessor->getValue($data, '[faceAttributes][makeup][eyeMakeup]'), 73 | $this->propertyAccessor->getValue($data, '[faceAttributes][makeup][lipMakeup]') 74 | ), 75 | array_map( 76 | function (array $accessoryData) { 77 | return new Accessory($accessoryData['type']); 78 | }, 79 | $this->propertyAccessor->getValue($data, '[faceAttributes][accessories]') 80 | ), 81 | !$this->propertyAccessor->getValue($data, '[faceAttributes][hair][invisible]') 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/XSolve/FaceValidatorBundle/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | xsolve_face_validator.client.azure.class: XSolve\FaceValidatorBundle\Client\GuzzleWrapper 3 | xsolve_face_validator.detector.face.class: XSolve\FaceValidatorBundle\Detector\AzureAPIFaceDetector 4 | 5 | services: 6 | _defaults: 7 | autowire: false 8 | autoconfigure: false 9 | public: false 10 | xsolve_face_validator.guzzle_client: 11 | class: GuzzleHttp\Client 12 | public: false 13 | arguments: 14 | - { base_uri: "%xsolve_face_validator.client.azure.base_uri%" } 15 | xsolve_face_validator.client.azure: 16 | class: "%xsolve_face_validator.client.azure.class%" 17 | arguments: 18 | - "@xsolve_face_validator.guzzle_client" 19 | - "%xsolve_face_validator.client.azure.subscription_key%" 20 | xsolve_face_validator.detector.face: 21 | class: "%xsolve_face_validator.detector.face.class%" 22 | arguments: 23 | - "@xsolve_face_validator.client.azure" 24 | - "@xsolve_face_validator.factory.face_detection_result" 25 | xsolve_face_validator.factory.face_to_image_ratio_calculator: 26 | class: XSolve\FaceValidatorBundle\Calculator\FaceToImageRatioCalculator 27 | xsolve_face_validator.factory.face_detection_result: 28 | class: XSolve\FaceValidatorBundle\Factory\FaceDetectionResultFactory 29 | arguments: 30 | - "@property_accessor" 31 | - "@xsolve_face_validator.factory.face_to_image_ratio_calculator" 32 | xsolve_face_validator.validator.specification.blur_is_acceptable: 33 | class: XSolve\FaceValidatorBundle\Validator\Specification\BlurIsAcceptable 34 | xsolve_face_validator.validator.specification.face_is_not_covered: 35 | class: XSolve\FaceValidatorBundle\Validator\Specification\FaceIsNotCovered 36 | xsolve_face_validator.validator.specification.face_is_of_acceptable_angle: 37 | class: XSolve\FaceValidatorBundle\Validator\Specification\FaceIsOfAcceptableAngle 38 | xsolve_face_validator.validator.specification.face_is_of_sufficient_size: 39 | class: XSolve\FaceValidatorBundle\Validator\Specification\FaceIsOfSufficientSize 40 | xsolve_face_validator.validator.specification.hair_is_visible: 41 | class: XSolve\FaceValidatorBundle\Validator\Specification\HairIsVisible 42 | xsolve_face_validator.validator.specification.is_not_wearing_glasses: 43 | class: XSolve\FaceValidatorBundle\Validator\Specification\IsNotWearingGlasses 44 | xsolve_face_validator.validator.specification.is_not_wearing_sunglasses: 45 | class: XSolve\FaceValidatorBundle\Validator\Specification\IsNotWearingSunglasses 46 | xsolve_face_validator.validator.specification.no_makeup: 47 | class: XSolve\FaceValidatorBundle\Validator\Specification\NoMakeup 48 | xsolve_face_validator.validator.specification.noise_is_acceptable: 49 | class: XSolve\FaceValidatorBundle\Validator\Specification\NoiseIsAcceptable 50 | 51 | xsolve_face_validator.validator.validator.face: 52 | class: XSolve\FaceValidatorBundle\Validator\Constraints\FaceValidator 53 | public: true 54 | tags: 55 | - { name: validator.constraint_validator } 56 | arguments: 57 | - "@xsolve_face_validator.detector.face" 58 | - 59 | - "@xsolve_face_validator.validator.specification.blur_is_acceptable" 60 | - "@xsolve_face_validator.validator.specification.face_is_not_covered" 61 | - "@xsolve_face_validator.validator.specification.face_is_of_acceptable_angle" 62 | - "@xsolve_face_validator.validator.specification.face_is_of_sufficient_size" 63 | - "@xsolve_face_validator.validator.specification.hair_is_visible" 64 | - "@xsolve_face_validator.validator.specification.is_not_wearing_glasses" 65 | - "@xsolve_face_validator.validator.specification.is_not_wearing_sunglasses" 66 | - "@xsolve_face_validator.validator.specification.no_makeup" 67 | - "@xsolve_face_validator.validator.specification.noise_is_acceptable" 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Validator/Constraints/FaceValidatorTest.php: -------------------------------------------------------------------------------- 1 | executionContext = $this->prophesize(ExecutionContextInterface::class); 51 | $this->faceDetector = $this->prophesize(FaceDetector::class); 52 | $this->firstCondition = $this->prophesize(FaceValidationSpecification::class); 53 | $this->secondCondition = $this->prophesize(FaceValidationSpecification::class); 54 | $this->validator = new FaceValidator($this->faceDetector->reveal(), [ 55 | $this->firstCondition->reveal(), 56 | $this->secondCondition->reveal(), 57 | ]); 58 | $this->validator->initialize($this->executionContext->reveal()); 59 | } 60 | 61 | /** 62 | * @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException 63 | */ 64 | public function testValidateInvalidConstraint() 65 | { 66 | $constraint = $this->prophesize(Constraint::class); 67 | 68 | $this->validator->validate('test file', $constraint->reveal()); 69 | } 70 | 71 | /** 72 | * @param mixed $invalidValue 73 | * 74 | * @dataProvider invalidValuesProvider 75 | */ 76 | public function testValidateInvalidValue($invalidValue) 77 | { 78 | $this->faceDetector->detect(Argument::any())->shouldNotBeCalled(); 79 | $this->executionContext->addViolation(Argument::any())->shouldNotBeCalled(); 80 | 81 | $this->validator->validate($invalidValue, new Face()); 82 | } 83 | 84 | public function testValidateNoFace() 85 | { 86 | $constraint = new Face(); 87 | $this->faceDetector->detect('test file')->willThrow(NoFaceDetectedException::class); 88 | $this->executionContext->addViolation($constraint->noFaceMessage)->shouldBeCalled(); 89 | 90 | $this->validator->validate('test file', $constraint); 91 | } 92 | 93 | public function testValidate() 94 | { 95 | $constraint = new Face(); 96 | $result = $this->prophesize(FaceDetectionResult::class); 97 | $this->faceDetector->detect('test file')->willReturn($result->reveal()); 98 | $this->firstCondition->evaluate($result->reveal(), $constraint)->willReturn(new Evaluation(true)); 99 | $this->secondCondition->evaluate($result->reveal(), $constraint)->willReturn(new Evaluation(false, 'test message')); 100 | $this->executionContext->addViolation('test message')->shouldBeCalled(); 101 | 102 | $this->validator->validate('test file', $constraint); 103 | } 104 | 105 | public function invalidValuesProvider(): array 106 | { 107 | return [ 108 | [null], 109 | [new \stdClass()], 110 | [new \SplFileInfo('dummy')], 111 | ]; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/XSolve/FaceValidatorBundle/Integration/FaceValidatorIntegrationTest.php: -------------------------------------------------------------------------------- 1 | validator = static::$kernel->getContainer()->get('validator'); 30 | $this->client = static::$kernel->getContainer()->get('xsolve_face_validator.client.azure'); 31 | } 32 | 33 | /** 34 | * @dataProvider validateProvider 35 | */ 36 | public function testValidate(Face $constraint, string $imagePath, array $apiResponse, array $expectedViolations) 37 | { 38 | $this->client->setResponseData($apiResponse); 39 | $constraintViolations = $this->validator->validate(new \SplFileInfo($imagePath), [$constraint]); 40 | $this->assertCount(count($expectedViolations), $constraintViolations); 41 | $violationMessages = array_map( 42 | function (ConstraintViolationInterface $violation) { 43 | return $violation->getMessage(); 44 | }, 45 | iterator_to_array($constraintViolations) 46 | ); 47 | 48 | foreach ($expectedViolations as $expectedViolationMessage) { 49 | $this->assertContains($expectedViolationMessage, $violationMessages); 50 | } 51 | } 52 | 53 | /** 54 | * @todo add missing cases 55 | */ 56 | public function validateProvider(): array 57 | { 58 | return [ 59 | [ 60 | new Face(), 61 | $this->generateImagePath('1x1'), 62 | [], 63 | [ 64 | 'Face is not visible.', 65 | ], 66 | ], 67 | [ 68 | new Face(), 69 | $this->generateImagePath('100x100'), 70 | $this->loadResponseFromFile('glasses.json'), 71 | [], 72 | ], 73 | [ 74 | new Face(['allowGlasses' => false]), 75 | $this->generateImagePath('100x100'), 76 | $this->loadResponseFromFile('glasses.json'), 77 | [ 78 | 'There should be no glasses in the picture.', 79 | ], 80 | ], 81 | [ 82 | new Face(['maxBlurLevel' => Face::LEVEL_MEDIUM]), 83 | $this->generateImagePath('100x100'), 84 | $this->loadResponseFromFile('medium_blur.json'), 85 | [], 86 | ], 87 | [ 88 | new Face(['maxBlurLevel' => Face::LEVEL_LOW]), 89 | $this->generateImagePath('100x100'), 90 | $this->loadResponseFromFile('medium_blur.json'), 91 | [ 92 | 'The picture is too blurred.', 93 | ], 94 | ], 95 | [ 96 | new Face(['maxNoiseLevel' => Face::LEVEL_LOW]), 97 | $this->generateImagePath('100x100'), 98 | $this->loadResponseFromFile('medium_noise.json'), 99 | [ 100 | 'The picture is too noisy.', 101 | ], 102 | ], 103 | [ 104 | new Face(), 105 | $this->generateImagePath('100x100'), 106 | $this->loadResponseFromFile('small_face.json'), 107 | [ 108 | 'Face is too small.', 109 | ], 110 | ], 111 | [ 112 | new Face(['allowSunglasses' => false]), 113 | $this->generateImagePath('100x100'), 114 | $this->loadResponseFromFile('sunglasses.json'), 115 | [ 116 | 'There should be no sunglasses in the picture.', 117 | ], 118 | ], 119 | ]; 120 | } 121 | 122 | private function loadResponseFromFile(string $name): array 123 | { 124 | $path = sprintf('%s/%s', self::RESPONSES_DIRECTORY, $name); 125 | $this->assertFileExists($path); 126 | 127 | return json_decode(file_get_contents($path), true); 128 | } 129 | 130 | private function generateImagePath(string $imageName): string 131 | { 132 | return sprintf('%s/%s', self::IMAGES_DIRECTORY, $imageName); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ---------- 2 | # XSolve Face Validator Bundle 3 | 4 | [![Build Status](https://travis-ci.org/xsolve-pl/xsolve-face-validator-bundle.svg?branch=master)](https://travis-ci.org/xsolve-pl/xsolve-face-validator-bundle) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/xsolve-pl/xsolve-face-validator-bundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/xsolve-pl/xsolve-face-validator-bundle/?branch=master) 6 | 7 | ============================ 8 | 9 | Table of contents 10 | ================= 11 | 12 | * [Introduction](#introduction) 13 | * [License](#license) 14 | * [Getting started](#getting-started) 15 | * [Usage](#usage) 16 | 17 | Introduction 18 | ================= 19 | This Symfony3 bundle allows to validate whether an image (for instance uploaded by a user of your app) contains person's face. 20 | Internally it uses MS Azure Face API so in order to use it you need an account in MS Azure. In free plan the API allows 21 | to make 30 000 requests per month and 20 per minute so it should be enough to be useful for low traffic apps. 22 | 23 | All the following features are configurable on the constraint level and can be easily enabled/disabled: 24 | * requiring certain face size (ratio to the image size) 25 | * disallowing an image when the face is covered 26 | * requiring the hair to be visible (the image must not be cut) 27 | * allowing the face to be rotated in any of the 3 axes to given level 28 | * disallowing to wear glasses 29 | * disallowing to wear sunglasses 30 | * disallowing any makeup 31 | * requiring an image not to be blurred over given level (low/medium/high) 32 | * requiring an image not to contain noises over given level (low/medium/high) 33 | 34 | Licence 35 | ================= 36 | This library is under the MIT license. See the complete license in [LICENSE](LICENSE) file. 37 | 38 | Getting started 39 | ================= 40 | Add the bundle to your Symfony3 project using [Composer](https://getcomposer.org/): 41 | ```bash 42 | $ composer require xsolve-pl/face-validator-bundle 43 | ``` 44 | 45 | You'll need also to register the bundle in your kernel: 46 | ```php 47 | createFormBuilder($user) 138 | ->add('profilePicture', FileType::class) 139 | ->getForm(); 140 | $form->handleRequest($request); 141 | 142 | if ($form->isSubmitted() && $form->isValid()) { 143 | // ... 144 | } 145 | 146 | // ... 147 | } 148 | } 149 | ``` 150 | 151 | the image will be validated whether it contains a face. The way the face is being validated is customizable, all the possible 152 | options with their default values are shown on the example below: 153 | 154 | ```php 155 | // src/AppBundle/Entity/User.php 156 | 157 | use Symfony\Component\Validator\Constraints as Assert; 158 | use XSolve\FaceValidatorBundle\Validator\Constraints as XSolveAssert; 159 | 160 | class User 161 | { 162 | /** 163 | * @var Symfony\Component\HttpFoundation\File\UploadedFile 164 | 165 | * @Assert\Image() 166 | * @XSolveAssert\Face( 167 | * minFaceRatio = 0.15, 168 | * allowCoveringFace = true, 169 | * maxFaceRotation = 20.0, 170 | * allowGlasses = true, 171 | * allowSunglasses = true, 172 | * allowMakeup = true, 173 | * allowNoHair = true, 174 | * maxBlurLevel = high, 175 | * maxNoiseLevel = high, 176 | * noFaceMessage = 'Face is not visible.', 177 | * faceTooSmallMessage = 'Face is too small.', 178 | * faceCoveredMessage = 'Face cannot be covered.', 179 | * hairCoveredMessage = 'Hair cannot be covered.', 180 | * tooMuchRotatedMessage = 'Face is too much rotated.', 181 | * glassesMessage = 'There should be no glasses in the picture.', 182 | * sunglassesMessage = 'There should be no sunglasses in the picture.', 183 | * makeupMessage = 'The person should not be wearing any makeup.', 184 | * blurredMessage = 'The picture is too blurred.', 185 | * noiseMessage = 'The picture is too noisy.' 186 | * ) 187 | */ 188 | public $profilePicture; 189 | } 190 | ``` 191 | 192 | Note that you can omit any (or even all) of the options listed above, then the defaults will be used. 193 | 194 | For blur and noise levels the possible options are: 195 | * low 196 | * medium 197 | * high 198 | 199 | It's also possible, just like with any other Symfony validator, to use it directly against given value (either file path or an instance of \SplFileInfo). 200 | 201 | ```php 202 | // src/AppBundle/Controller/ImageController.php 203 | 204 | namespace AppBundle\Controller; 205 | 206 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 207 | use Symfony\Component\Validator\Validator\ValidatorInterface; 208 | 209 | class ImageController extends Controller 210 | { 211 | public function validateAction(Request $request) 212 | { 213 | /* @var $validator ValidatorInterface */ 214 | $validator = $this->get('validator'); 215 | $constraintViolations = $validator->validate( 216 | '/path/to/your/image/file.png', 217 | new Face([ 218 | // you can pass the options mentioned before to the validation constraint 219 | ]) 220 | ); 221 | } 222 | } 223 | ``` 224 | --------------------------------------------------------------------------------