Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Skew
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 3
1190
0.00% covered (danger)
0.00%
0 / 1
 autoRotate
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
306
 rotatePixelMatrix
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 getNearestValue
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
182
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Image
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Image;
16
17/**
18 * Skew image
19 *
20 * @package phpOMS\Image
21 * @license OMS License 2.0
22 * @link    https://jingga.app
23 * @since   1.0.0
24 */
25final class Skew
26{
27    /**
28     * Automatically rotate image based on projection profile
29     *
30     * @param string $inPath    Binary input image (black/white)
31     * @param string $outPath   Output image
32     * @param int    $maxDegree Max degree to consider for rotation
33     * @param array  $start     Start coordinates for analysis (e.g. ignore top/border of image)
34     * @param array  $end       End coordinates for analysis (e.g. ignore bottom/border of image)
35     */
36    public static function autoRotate(string $inPath, string $outPath, int $maxDegree = 45, array $start = [], array $end = []) : void
37    {
38        $im = null;
39        if (\strripos($inPath, 'png') !== false) {
40            $im = \imagecreatefrompng($inPath);
41        } elseif (\strripos($inPath, 'jpg') !== false || \strripos($inPath, 'jpeg') !== false) {
42            $im = \imagecreatefromjpeg($inPath);
43        } else {
44            $im = \imagecreatefromgif($inPath);
45        }
46
47        if ($im === false) {
48            return;
49        }
50
51        $dim = [\imagesx($im), \imagesy($im)];
52
53        $start = [\max(0, $start[0] ?? 0), \max(0, $start[1] ?? 0)];
54        $end   = [\min($dim[0], $end[0] ?? $dim[0]), \min($dim[1], $end[1] ?? $dim[1])];
55
56        // Pixelmatrix [width][height]
57        // This is important since it makes the hist calculation further down easier
58        $imMatrix = [[]];
59
60        $avg = 0;
61
62        for ($i = $start[0]; $i < $end[0]; ++$i) {
63            for ($j = $start[1]; $j < $end[1]; ++$j) {
64                $imMatrix[$j - $start[1]][$i - $start[0]] = \imagecolorat($im, $i, $j) < 0.5 ? 1 : 0;
65                $avg                                     += $imMatrix[$j - $start[1]][$i - $start[0]];
66            }
67        }
68
69        $avg /= $start[1] - $end[1];
70
71        $dimImMatrix = [\count($imMatrix), \count($imMatrix[0])];
72        $bestScore   = 0;
73        $bestDegree  = 0;
74
75        for ($i = -$maxDegree; $i < $maxDegree; ++$i) {
76            if ($i === 0) {
77                continue;
78            }
79
80            $rotated = self::rotatePixelMatrix($imMatrix, $dimImMatrix, $i);
81            $hist    = [];
82
83            for ($j = 0; $j < $dimImMatrix[0]; ++$j) {
84                $hist[$j] = \array_sum($rotated[$j]);
85
86                // cleanup for score function
87                // we want to see how many lines are above avg. and how much they are above avg.
88                // a different score function may not need this line
89                $hist[$j] = $hist[$j] > $avg ? $hist[$j] : 0;
90            }
91
92            $score = \array_sum($hist);
93            if ($bestScore < $score) {
94                $bestScore  = $score;
95                $bestDegree = $i;
96            }
97        }
98
99        $im = \imagerotate($im, $bestDegree, 1);
100        if ($im === false) {
101            return;
102        }
103
104        if (\strripos($outPath, 'png') !== false) {
105            \imagepng($im, $outPath);
106        } elseif (\strripos($outPath, 'jpg') !== false || \strripos($outPath, 'jpeg') !== false) {
107            \imagejpeg($im, $outPath);
108        } else {
109            \imagegif($im, $outPath);
110        }
111
112        \imagedestroy($im);
113    }
114
115    /**
116     * Rotate the pixel matrix by a certain degree
117     *
118     * @param array $pixel Pixel matrix (0 index = y, 1 index = x)
119     * @param array $dim   Matrix dimension (0 index = y, 1 index = x)
120     * @param int   $deg   Degree to rotate
121     *
122     * @return array
123     *
124     * @since 1.0.0
125     */
126    public static function rotatePixelMatrix(array $pixel, array $dim, int $deg) : array
127    {
128        $rad = \deg2rad($deg);
129
130        $sin = \sin(-$rad);
131        $cos = \cos(-$rad);
132
133        $rotated = [[]];
134
135        $cXArr = [];
136        for ($j = 0; $j < $dim[1]; ++$j) {
137            $cXArr[] = $j - $dim[1] / 2.0; // center
138        }
139
140        for ($i = 0; $i < $dim[0]; ++$i) {
141            $cY = $i - $dim[0] / 2.0; // center
142
143            foreach ($cXArr as $j => $cX) {
144                $x = $cos * $cX + $sin * $cY + $dim[1] / 2.0;
145                $y = -$sin * $cX + $cos * $cY + $dim[0] / 2.0;
146
147                $rotated[$i][$j] = self::getNearestValue($pixel, $dim, $x, $y);
148            }
149        }
150
151        return $rotated;
152    }
153
154    /**
155     * Find the closes pixel based on floating points
156     *
157     * @param array $pixel Pixel matrix (0 index = y, 1 index = x)
158     * @param array $dim   Matrix dimension (0 index = y, 1 index = x)
159     * @param float $x     X coordinate
160     * @param float $y     Y coordinate
161     *
162     * @return int
163     *
164     * @since 1.0.0
165     */
166    private static function getNearestValue(array $pixel, array $dim, float $x, float $y) : int
167    {
168        $xLow  = ($x < 0) ? 0 : (($x > ($dim[1] - 1)) ? ($dim[1] - 1) : (int) $x);
169        $xHigh = ($xLow === $dim[1] - 1) ? $xLow : ($xLow + 1);
170
171        $yLow  = ($y < 0) ? 0 : (($y > ($dim[0] - 1)) ? ($dim[0] - 1) : (int) $y);
172        $yHigh = ($yLow === $dim[0] - 1) ? $yLow : ($yLow + 1);
173
174        $points = [
175            [$xLow, $yLow],
176            [$xLow, $yHigh],
177            [$xHigh, $yLow],
178            [$xHigh, $yHigh],
179        ];
180
181        $minDistance = \PHP_FLOAT_MAX;
182        $minValue    = 0;
183
184        foreach ($points as $point) {
185            $distance = ($point[0] - $x) * ($point[0] - $x) + ($point[1] - $y) * ($point[1] - $y);
186
187            if ($distance < $minDistance) {
188                $minDistance = $distance;
189
190                $minValue = $point[0] >= 0 && $point[0] < $dim[0] && $point[1] >= 0 && $point[1] < $dim[1]
191                    ? $pixel[$point[1]][$point[0]]
192                    : 0;
193            }
194        }
195
196        return $minValue;
197    }
198}