Skip to content

Commit 5cac260

Browse files
authored
Merge pull request #5 from byWulf/feature/improve-code-coverage-part2
Add more unittests
2 parents 7def26c + 9486467 commit 5cac260

31 files changed

+377
-131
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Currently only standard rectangle puzzles are supported by the algorithm. Althou
3838
### PieceAnalyzer
3939
You need a high definition image of your puzzle piece. It should be 1000px wide and its borders should be distinct. Best practice: use a backlight when taking the images, so it becomes black/white and the borders really stand out.
4040

41-
If you want to get an image returned where the background got transparent (f.e. to display it somewhere connected to other pieces), specify it also. Best practice: take two images of the piece in the same position. First with backlight for better border detection, and one with normal light for the colored piece becoming transparent.
41+
If you want to get an image returned where the background got transparent (f.e. to display it somewhere connected to other pieces), specify it also. Best practice: take two images of the piece in the same position. First with backlight for better border detection, and one with normal light for the colored piece becoming transparent. You can request multiple images to become transparent and the size of these image doesn't have to be the same as the original (10 times smaller transparent images are good for displaying the solution to the user instead of the original high definition sized images).
4242
```injectablephp
4343
use Bywulf\Jigsawlutioner\Service\BorderFinder\ByWulfBorderFinder;
4444
use Bywulf\Jigsawlutioner\Service\SideFinder\ByWulfSideFinder;
@@ -54,7 +54,7 @@ $transparentImage = imagecreatefromjpeg('piece_color.jpg');
5454
5555
$piece = $pieceAnalyzer->getPieceFromImage(1, $image, new ByWulfBorderFinderContext(
5656
threshold: 0.65,
57-
transparentImage: $transparentImage,
57+
transparentImages: [$transparentImage],
5858
));
5959
```
6060

src/Dto/Context/ByWulfBorderFinderContext.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88

99
class ByWulfBorderFinderContext implements BorderFinderContextInterface
1010
{
11+
/**
12+
* @param GdImage[] $transparentImages
13+
*/
1114
public function __construct(
1215
private float $threshold,
13-
private ?GdImage $transparentImage = null,
14-
private ?GdImage $smallTransparentImage = null,
16+
private array $transparentImages = [],
1517
) {
1618
}
1719

@@ -20,13 +22,11 @@ public function getThreshold(): float
2022
return $this->threshold;
2123
}
2224

23-
public function getTransparentImage(): ?GdImage
25+
/**
26+
* @return GdImage[]
27+
*/
28+
public function getTransparentImages(): array
2429
{
25-
return $this->transparentImage;
26-
}
27-
28-
public function getSmallTransparentImage(): ?GdImage
29-
{
30-
return $this->smallTransparentImage;
30+
return $this->transparentImages;
3131
}
3232
}

src/Dto/Piece.php

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@
44

55
namespace Bywulf\Jigsawlutioner\Dto;
66

7-
use Bywulf\Jigsawlutioner\SideClassifier\BigWidthClassifier;
8-
use Bywulf\Jigsawlutioner\SideClassifier\CornerDistanceClassifier;
9-
use Bywulf\Jigsawlutioner\SideClassifier\DepthClassifier;
10-
use Bywulf\Jigsawlutioner\SideClassifier\DirectionClassifier;
11-
use Bywulf\Jigsawlutioner\SideClassifier\LineDistanceClassifier;
12-
use Bywulf\Jigsawlutioner\SideClassifier\SmallWidthClassifier;
137
use InvalidArgumentException;
148
use JsonSerializable;
159

@@ -111,19 +105,14 @@ public static function fromSerialized(string $serializedContent): self
111105
{
112106
$piece = unserialize(
113107
$serializedContent,
114-
['allowed_classes' => [
115-
DerivativePoint::class,
116-
Piece::class,
117-
Point::class,
118-
Side::class,
119-
SideMetadata::class,
120-
BigWidthClassifier::class,
121-
CornerDistanceClassifier::class,
122-
DepthClassifier::class,
123-
DirectionClassifier::class,
124-
SmallWidthClassifier::class,
125-
LineDistanceClassifier::class,
126-
]]
108+
['allowed_classes' => array_merge(
109+
[
110+
Piece::class,
111+
DerivativePoint::class,
112+
Point::class,
113+
],
114+
Side::getUnserializeClasses(),
115+
)]
127116
);
128117

129118
if (!$piece instanceof Piece) {

src/Dto/Side.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
namespace Bywulf\Jigsawlutioner\Dto;
66

77
use Bywulf\Jigsawlutioner\Exception\SideClassifierException;
8+
use Bywulf\Jigsawlutioner\SideClassifier\BigWidthClassifier;
9+
use Bywulf\Jigsawlutioner\SideClassifier\CornerDistanceClassifier;
10+
use Bywulf\Jigsawlutioner\SideClassifier\DepthClassifier;
811
use Bywulf\Jigsawlutioner\SideClassifier\DirectionClassifier;
12+
use Bywulf\Jigsawlutioner\SideClassifier\LineDistanceClassifier;
913
use Bywulf\Jigsawlutioner\SideClassifier\SideClassifierInterface;
14+
use Bywulf\Jigsawlutioner\SideClassifier\SmallWidthClassifier;
1015
use JsonSerializable;
1116

1217
class Side implements JsonSerializable
@@ -129,4 +134,23 @@ public function jsonSerialize(): array
129134
'endPoint' => $this->endPoint->jsonSerialize(),
130135
];
131136
}
137+
138+
/**
139+
* @return class-string[]
140+
*/
141+
public static function getUnserializeClasses(): array
142+
{
143+
return [
144+
Side::class,
145+
Point::class,
146+
DerivativePoint::class,
147+
SideMetadata::class,
148+
BigWidthClassifier::class,
149+
CornerDistanceClassifier::class,
150+
DepthClassifier::class,
151+
DirectionClassifier::class,
152+
SmallWidthClassifier::class,
153+
LineDistanceClassifier::class,
154+
];
155+
}
132156
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bywulf\Jigsawlutioner\Exception;
6+
7+
class PathServiceException extends JigsawlutionerException
8+
{
9+
}

src/Service/BorderFinder/ByWulfBorderFinder.php

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ public function findPieceBorder(
4545
GdImage $image,
4646
BorderFinderContextInterface $context
4747
): array {
48-
/** @noinspection PhpConditionAlreadyCheckedInspection */
4948
if (!$context instanceof ByWulfBorderFinderContext) {
5049
throw new InvalidArgumentException('Expected context of type ' . ByWulfBorderFinderContext::class . ', got ' . $context::class);
5150
}
@@ -87,11 +86,9 @@ public function findPieceBorder(
8786
throw new BorderParsingException('Piece is cut off');
8887
}
8988

90-
if ($context->getTransparentImage() !== null) {
91-
$this->createTransparentImage($context->getTransparentImage(), $pixelMap, $biggestObjectColor);
92-
}
93-
if ($context->getSmallTransparentImage() !== null) {
94-
$this->createSmallTransparentImage($context->getSmallTransparentImage(), $pixelMap, $biggestObjectColor);
89+
// Make given images transparent if wished
90+
foreach ($context->getTransparentImages() as $transparentImage) {
91+
$this->transparencifyImage($transparentImage, $pixelMap, $biggestObjectColor);
9592
}
9693

9794
return $this->pointParser->getOrderedBorderPoints($pixelMap, $biggestObjectColor);
@@ -100,9 +97,14 @@ public function findPieceBorder(
10097
/**
10198
* @throws BorderParsingException
10299
*/
103-
private function allocateColor(GdImage $image, int $red, int $green, int $blue): int
100+
private function allocateColor(GdImage $image, int $red, int $green, int $blue, ?int $alpha = null): int
104101
{
105-
$color = imagecolorallocate($image, $red, $green, $blue);
102+
if ($alpha !== null) {
103+
$color = imagecolorallocatealpha($image, $red, $green, $blue, $alpha);
104+
} else {
105+
$color = imagecolorallocate($image, $red, $green, $blue);
106+
}
107+
106108
if ($color === false) {
107109
throw new BorderParsingException('Could not allocate color ' . $red . '/' . $green . '/' . $blue . '.');
108110
}
@@ -148,39 +150,18 @@ private function hasBorderPixel(PixelMap $pixelMap, int $objectColor): bool
148150
/**
149151
* @throws BorderParsingException
150152
*/
151-
private function createTransparentImage(GdImage $transparentImage, PixelMap $pixelMap, int $opaqueColor): void
153+
private function transparencifyImage(GdImage $transparentImage, PixelMap $pixelMap, int $opaqueColor): void
152154
{
153-
$transparentColor = imagecolorallocatealpha($transparentImage, 255, 255, 255, 0);
154-
if ($transparentColor === false) {
155-
throw new BorderParsingException('Color could not be created.');
156-
}
155+
$transparentColor = $this->allocateColor($transparentImage, 255, 255, 255, 0);
157156

158157
imagecolortransparent($transparentImage, $transparentColor);
159158

160-
for ($y = 0; $y < imagesy($transparentImage); ++$y) {
161-
for ($x = 0; $x < imagesx($transparentImage); ++$x) {
162-
if ($pixelMap->getColor($x, $y) !== $opaqueColor) {
163-
imagesetpixel($transparentImage, $x, $y, $transparentColor);
164-
}
165-
}
166-
}
167-
}
168-
169-
/**
170-
* @throws BorderParsingException
171-
*/
172-
private function createSmallTransparentImage(GdImage $transparentImage, PixelMap $pixelMap, int $opaqueColor): void
173-
{
174-
$transparentColor = imagecolorallocatealpha($transparentImage, 255, 255, 255, 0);
175-
if ($transparentColor === false) {
176-
throw new BorderParsingException('Color could not be created.');
177-
}
178-
179-
imagecolortransparent($transparentImage, $transparentColor);
159+
$xFactor = $pixelMap->getWidth() / imagesx($transparentImage);
160+
$yFactor = $pixelMap->getHeight() / imagesy($transparentImage);
180161

181162
for ($y = 0; $y < imagesy($transparentImage); ++$y) {
182163
for ($x = 0; $x < imagesx($transparentImage); ++$x) {
183-
if ($pixelMap->getColor($x * 10, $y * 10) !== $opaqueColor) {
164+
if ($pixelMap->getColor((int) ($x * $xFactor), (int) ($y * $yFactor)) !== $opaqueColor) {
184165
imagesetpixel($transparentImage, $x, $y, $transparentColor);
185166
}
186167
}

src/Service/PathService.php

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Bywulf\Jigsawlutioner\Service;
66

77
use Bywulf\Jigsawlutioner\Dto\Point;
8+
use Bywulf\Jigsawlutioner\Exception\PathServiceException;
89

910
class PathService
1011
{
@@ -72,29 +73,37 @@ public function extendPointsByDistance(array $points, float $distance): array
7273
/**
7374
* @param Point[] $points
7475
*/
75-
public function getPointOnPolyline(array $points, int $index, float $length): Point
76+
public function getPointOnPolyline(array $points, int $fromPointIndex, float $length): Point
7677
{
78+
if (count($points) < 2) {
79+
throw new PathServiceException('At least two points must be given.');
80+
}
81+
82+
if (!isset($points[$fromPointIndex])) {
83+
throw new PathServiceException('Given index out of range of the given points.');
84+
}
85+
7786
$indexDirection = $length < 0 ? -1 : 1;
7887
$movedLength = 0;
7988
/** @noinspection CallableParameterUseCaseInTypeContextInspection */
8089
$length = abs($length);
8190
$point = null;
8291
while ($point === null) {
83-
$nextIndex = $index + $indexDirection;
92+
$nextIndex = $fromPointIndex + $indexDirection;
8493
$nextIndex = $nextIndex < 0 ? count($points) - 1 : $nextIndex;
8594
$nextIndex = $nextIndex >= count($points) ? 0 : $nextIndex;
86-
$lineLength = $this->pointService->getDistanceBetweenPoints($points[$index], $points[$nextIndex]);
95+
$lineLength = $this->pointService->getDistanceBetweenPoints($points[$fromPointIndex], $points[$nextIndex]);
8796

8897
if ($movedLength + $lineLength >= $length) {
8998
$offset = $length - $movedLength;
9099

91100
$point = new Point(
92-
$points[$index]->getX() + ($points[$nextIndex]->getX() - $points[$index]->getX()) / $lineLength * $offset,
93-
$points[$index]->getY() + ($points[$nextIndex]->getY() - $points[$index]->getY()) / $lineLength * $offset,
101+
$points[$fromPointIndex]->getX() + ($points[$nextIndex]->getX() - $points[$fromPointIndex]->getX()) / $lineLength * $offset,
102+
$points[$fromPointIndex]->getY() + ($points[$nextIndex]->getY() - $points[$fromPointIndex]->getY()) / $lineLength * $offset,
94103
);
95104
}
96105
$movedLength += $lineLength;
97-
$index = $nextIndex;
106+
$fromPointIndex = $nextIndex;
98107
}
99108

100109
return $point;

src/Service/SideFinder/ByWulfSideFinder.php

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -90,55 +90,35 @@ private function getDerivativesRating(array $activeDerivatives, array $borderPoi
9090
$this->logger?->debug('Looking at the following points: ', $activeDerivatives);
9191
$rating = 0;
9292

93-
// 1. Check that opposite sides are equally long
94-
$distance0 = $this->pointService->getDistanceBetweenPoints($activeDerivatives[0], $activeDerivatives[1]);
95-
$distance2 = $this->pointService->getDistanceBetweenPoints($activeDerivatives[2], $activeDerivatives[3]);
96-
$rating += $this->calculateRating(abs($distance0 - $distance2) / (0.4 * min($distance0, $distance2)));
97-
if (abs($distance0 - $distance2) > 0.6 * min($distance0, $distance2)) {
98-
$this->logger?->debug(' -> Distance of sides 0 and 2 more than 60% apart', [$distance0, $distance2]);
99-
100-
return null;
101-
}
102-
103-
$distance1 = $this->pointService->getDistanceBetweenPoints($activeDerivatives[1], $activeDerivatives[2]);
104-
$distance3 = $this->pointService->getDistanceBetweenPoints($activeDerivatives[3], $activeDerivatives[0]);
105-
$rating += $this->calculateRating(abs($distance1 - $distance3) / (0.4 * min($distance1, $distance3)));
106-
if (abs($distance1 - $distance3) > 0.6 * min($distance1, $distance3)) {
107-
$this->logger?->debug(' -> Distance of sides 1 and 3 more than 60% apart', [$distance1, $distance3]);
108-
109-
return null;
93+
$distances = [];
94+
for ($i = 0; $i < 4; ++$i) {
95+
$distances[$i] = $this->pointService->getDistanceBetweenPoints($activeDerivatives[$i], $activeDerivatives[($i + 1) % 4]);
11096
}
11197

112-
// 2. Check that opposite sides are equally rotated
113-
$rotation0 = $this->pointService->getRotation($activeDerivatives[0], $activeDerivatives[1]);
114-
$rotation2 = $this->pointService->getRotation($activeDerivatives[3], $activeDerivatives[2]);
115-
$rotationDiff0 = abs($this->pointService->normalizeRotation($rotation0 - $rotation2));
116-
$rating += $this->calculateRating($rotationDiff0 / 30);
117-
if ($rotationDiff0 > 40) {
118-
$this->logger?->debug(' -> Rotation of sides 0 and 2 more than 40° apart', [$rotation0, $rotation2]);
98+
// 1. Check that opposite sides are equally long
99+
$rating += $this->calculateRating(abs($distances[0] - $distances[2]) / (0.4 * min($distances[0], $distances[2])));
100+
if (abs($distances[0] - $distances[2]) > 0.6 * min($distances[0], $distances[2])) {
101+
$this->logger?->debug(' -> Distance of sides 0 and 2 more than 60% apart', [$distances[0], $distances[2]]);
119102

120103
return null;
121104
}
122105

123-
$rotation1 = $this->pointService->getRotation($activeDerivatives[1], $activeDerivatives[2]);
124-
$rotation3 = $this->pointService->getRotation($activeDerivatives[0], $activeDerivatives[3]);
125-
$rotationDiff1 = abs($this->pointService->normalizeRotation($rotation1 - $rotation3));
126-
$rating += $this->calculateRating($rotationDiff1 / 30);
127-
if ($rotationDiff1 > 40) {
128-
$this->logger?->debug(' -> Rotation of sides 1 and 3 more than 40° apart', [$rotation1, $rotation3]);
106+
$rating += $this->calculateRating(abs($distances[1] - $distances[3]) / (0.4 * min($distances[1], $distances[3])));
107+
if (abs($distances[1] - $distances[3]) > 0.6 * min($distances[1], $distances[3])) {
108+
$this->logger?->debug(' -> Distance of sides 1 and 3 more than 60% apart', [$distances[1], $distances[3]]);
129109

130110
return null;
131111
}
132112

133-
// 4. Check that the sides are in their length not too far away from another
134-
$rating += $this->calculateRating(abs(min($distance0, $distance2) - min($distance1, $distance3)) / (0.5 * min($distance0, $distance1, $distance2, $distance3)));
135-
if (abs(min($distance0, $distance2) - min($distance1, $distance3)) > 0.75 * min($distance0, $distance1, $distance2, $distance3)) {
136-
$this->logger?->debug(' -> Too narrow rectangle', [$distance0, $distance1, $distance2, $distance3]);
113+
// 2. Check that the sides are in their length not too far away from another
114+
$rating += $this->calculateRating(abs(min($distances[0], $distances[2]) - min($distances[1], $distances[3])) / (0.5 * min($distances)));
115+
if (abs(min($distances[0], $distances[2]) - min($distances[1], $distances[3])) > 0.75 * min($distances)) {
116+
$this->logger?->debug(' -> Too narrow rectangle', $distances);
137117

138118
return null;
139119
}
140120

141-
// 5. Check that the first 10% are straight to the side
121+
// 3. Check that the first 10% are straight to the side
142122
if (!$this->areLineStartsStraight($activeDerivatives, $borderPoints, $rating)) {
143123
return null;
144124
}
@@ -152,14 +132,16 @@ private function getDerivativesRating(array $activeDerivatives, array $borderPoi
152132
*/
153133
private function areLineStartsStraight(array $activeDerivatives, array $borderPoints, float &$rating): bool
154134
{
135+
$dividedCount = 100;
136+
155137
for ($i = 0; $i < 4; ++$i) {
156138
$sideDistance = $this->pointService->getDistanceBetweenPoints($activeDerivatives[$i], $activeDerivatives[($i + 1) % 4]);
157139
$extendedPoints = $this->pathService->extendPointsByCount(
158140
$this->getPointsBetweenIndexes($borderPoints, $activeDerivatives[$i]->getIndex(), $activeDerivatives[($i + 1) % 4]->getIndex()),
159-
100
141+
$dividedCount
160142
);
161143

162-
for ($j = 0; $j < 10; ++$j) {
144+
for ($j = 0; $j < $dividedCount * 0.1; ++$j) {
163145
$distanceToLine = $this->pointService->getDistanceToLine($extendedPoints[$j], $activeDerivatives[$i], $activeDerivatives[($i + 1) % 4]);
164146
$rating += $this->calculateRating($distanceToLine / (0.06 * $sideDistance)) * 0.1;
165147
if ($distanceToLine > 0.075 * $sideDistance) {
@@ -168,7 +150,7 @@ private function areLineStartsStraight(array $activeDerivatives, array $borderPo
168150
return false;
169151
}
170152
}
171-
for ($j = 90; $j < 100; ++$j) {
153+
for ($j = $dividedCount * 0.9; $j < $dividedCount; ++$j) {
172154
$distanceToLine = $this->pointService->getDistanceToLine($extendedPoints[$j], $activeDerivatives[$i], $activeDerivatives[($i + 1) % 4]);
173155
$rating += $this->calculateRating($distanceToLine / (0.06 * $sideDistance)) * 0.1;
174156
if ($distanceToLine > 0.075 * $sideDistance) {

tests/Dto/Context/ByWulfBorderFinderContextTest.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ public function testGetters(): void
1818
$transparentImage = imagecreatetruecolor(10, 10);
1919
$smallTransparentImage = imagecreatetruecolor(1, 1);
2020

21-
$context = new ByWulfBorderFinderContext($threshold, $transparentImage, $smallTransparentImage);
21+
$context = new ByWulfBorderFinderContext($threshold, [$transparentImage, $smallTransparentImage]);
2222

2323
$this->assertEquals($threshold, $context->getThreshold());
24-
$this->assertEquals($transparentImage, $context->getTransparentImage());
25-
$this->assertEquals($smallTransparentImage, $context->getSmallTransparentImage());
24+
$this->assertEquals([$transparentImage, $smallTransparentImage], $context->getTransparentImages());
2625
}
2726
}

tests/Dto/SideTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,9 @@ public function testJsonSerialize(): void
164164
],
165165
], $side->jsonSerialize());
166166
}
167+
168+
public function testGetUnserializeClasses(): void
169+
{
170+
$this->assertIsArray(Side::getUnserializeClasses());
171+
}
167172
}

0 commit comments

Comments
 (0)