├── .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 | [](https://travis-ci.org/xsolve-pl/xsolve-face-validator-bundle)
5 | [](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 |
--------------------------------------------------------------------------------