outputPath = $outputPath; // Detecta o tipo da imagem e carrega $imageInfo = getimagesize($imagePath); if ($imageInfo === false) { throw new Exception('Failed to load image'); } switch ($imageInfo[2]) { case IMAGETYPE_JPEG: $this->srcImage = imagecreatefromjpeg($imagePath); break; case IMAGETYPE_PNG: $this->srcImage = imagecreatefrompng($imagePath); break; default: throw new Exception('Unsupported image type'); } if ($this->srcImage === false) { throw new Exception('Failed to create image resource'); } // Novo pipeline de processamento mais completo $this->detectAndCropDocument() ->convertToGrayscale() ->enhanceContrast() ->sharpenImage() ->removeNoise() ->adjustBrightnessContrast() ->normalizeBackground() ->saveResult(); return $this->outputPath; } private function detectAndCropDocument() { $width = imagesx($this->srcImage); $height = imagesy($this->srcImage); // Cria cópia para processamento $work = imagecreatetruecolor($width, $height); imagecopy($work, $this->srcImage, 0, 0, 0, 0, $width, $height); // Pré-processamento para melhorar detecção imagefilter($work, IMG_FILTER_GAUSSIAN_BLUR); imagefilter($work, IMG_FILTER_GRAYSCALE); // Detecta bordas usando Sobel $edges = $this->detectEdgesSobel($work); // Encontra pontos de contorno $contourPoints = []; for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($edges, $x, $y); if ($rgb > 0) { $contourPoints[] = [$x, $y]; } } } // Encontra os 4 cantos do documento $corners = $this->findCornerPoints($contourPoints); if ($corners) { // Aplica transformação perspectiva $this->processedImage = $this->cropAndTransform($this->srcImage, $corners); } else { $this->processedImage = $this->srcImage; } imagedestroy($work); imagedestroy($edges); return $this; } private function detectEdgesSobel($image) { $width = imagesx($image); $height = imagesy($image); $edges = imagecreatetruecolor($width, $height); // Kernels Sobel $sobelX = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]; $sobelY = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]]; for ($x = 1; $x < $width-1; $x++) { for ($y = 1; $y < $height-1; $y++) { $pixelX = $pixelY = 0; // Aplica kernels for ($i = -1; $i <= 1; $i++) { for ($j = -1; $j <= 1; $j++) { $rgb = imagecolorat($image, $x + $i, $y + $j); $colors = imagecolorsforindex($image, $rgb); $gray = $colors['red']; $pixelX += $gray * $sobelX[$i+1][$j+1]; $pixelY += $gray * $sobelY[$i+1][$j+1]; } } // Magnitude do gradiente $magnitude = min(255, sqrt($pixelX * $pixelX + $pixelY * $pixelY)); // Aplica threshold para reduzir ruído $magnitude = ($magnitude > 50) ? 255 : 0; $color = imagecolorallocate($edges, $magnitude, $magnitude, $magnitude); imagesetpixel($edges, $x, $y, $color); } } return $edges; } private function findDocumentBounds($binary) { $width = imagesx($binary); $height = imagesy($binary); $left = $width; $right = 0; $top = $height; $bottom = 0; // Encontra os limites do documento for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($binary, $x, $y); if ($rgb == 0) { // Pixel preto $left = min($left, $x); $right = max($right, $x); $top = min($top, $y); $bottom = max($bottom, $y); } } } if ($left < $right && $top < $bottom) { return [ 'left' => $left, 'right' => $right, 'top' => $top, 'bottom' => $bottom ]; } return null; } private function isEdgePoint($img, $x, $y) { $threshold = 30; // Verifica pixels vizinhos $center = $this->getPixelIntensity($img, $x, $y); $left = $this->getPixelIntensity($img, $x - 1, $y); $right = $this->getPixelIntensity($img, $x + 1, $y); $top = $this->getPixelIntensity($img, $x, $y - 1); $bottom = $this->getPixelIntensity($img, $x, $y + 1); return (abs($center - $left) > $threshold || abs($center - $right) > $threshold || abs($center - $top) > $threshold || abs($center - $bottom) > $threshold); } private function getPixelIntensity($img, $x, $y) { if ($x < 0 || $y < 0 || $x >= imagesx($img) || $y >= imagesy($img)) { return 0; } $rgb = imagecolorat($img, $x, $y); $colors = imagecolorsforindex($img, $rgb); return ($colors['red'] + $colors['green'] + $colors['blue']) / 3; } private function findCornerPoints($points) { // Encontra os pontos mais extremos $topLeft = null; $topRight = null; $bottomLeft = null; $bottomRight = null; $minSum = PHP_FLOAT_MAX; $maxSum = PHP_FLOAT_MIN; $minDiff = PHP_FLOAT_MAX; $maxDiff = PHP_FLOAT_MIN; foreach ($points as $point) { $sum = $point[0] + $point[1]; $diff = $point[0] - $point[1]; if ($sum < $minSum) { $minSum = $sum; $topLeft = $point; } if ($sum > $maxSum) { $maxSum = $sum; $bottomRight = $point; } if ($diff < $minDiff) { $minDiff = $diff; $bottomLeft = $point; } if ($diff > $maxDiff) { $maxDiff = $diff; $topRight = $point; } } if ($topLeft && $topRight && $bottomLeft && $bottomRight) { return [$topLeft, $topRight, $bottomRight, $bottomLeft]; } return null; } private function cropAndTransform($img, $corners) { $width = abs($corners[1][0] - $corners[0][0]); $height = abs($corners[3][1] - $corners[0][1]); $dest = imagecreatetruecolor($width, $height); // Preserva transparência imagealphablending($dest, false); imagesavealpha($dest, true); // Define pontos de destino $dest_points = [ 0, 0, $width - 1, 0, $width - 1, $height - 1, 0, $height - 1 ]; // Define pontos de origem $src_points = []; foreach ($corners as $corner) { $src_points[] = $corner[0]; $src_points[] = $corner[1]; } // Aplica transformação perspectiva imagecopyresampled( $dest, $img, 0, 0, min($src_points[0], $src_points[6]), min($src_points[1], $src_points[3]), $width, $height, max($src_points[2], $src_points[4]) - min($src_points[0], $src_points[6]), max($src_points[5], $src_points[7]) - min($src_points[1], $src_points[3]) ); return $dest; } private function convertToGrayscale() { if (!isset($this->processedImage)) { $this->processedImage = $this->srcImage; } imagefilter($this->processedImage, IMG_FILTER_GRAYSCALE); return $this; } private function enhanceContrast() { // Aplica equalização de histograma adaptativa $width = imagesx($this->processedImage); $height = imagesy($this->processedImage); // Divide a imagem em blocos e aplica equalização local $blockSize = 32; for ($x = 0; $x < $width; $x += $blockSize) { for ($y = 0; $y < $height; $y += $blockSize) { $this->equalizeHistogramBlock($x, $y, min($blockSize, $width - $x), min($blockSize, $height - $y)); } } return $this; } private function equalizeHistogramBlock($startX, $startY, $blockWidth, $blockHeight) { $histogram = array_fill(0, 256, 0); $total = 0; // Calcula histograma do bloco for ($x = $startX; $x < $startX + $blockWidth; $x++) { for ($y = $startY; $y < $startY + $blockHeight; $y++) { $rgb = imagecolorat($this->processedImage, $x, $y); $colors = imagecolorsforindex($this->processedImage, $rgb); $gray = $colors['red']; // Já está em escala de cinza $histogram[$gray]++; $total++; } } // Calcula LUT de equalização $lut = array(); $sum = 0; for ($i = 0; $i < 256; $i++) { $sum += $histogram[$i]; $lut[$i] = round(($sum * 255) / $total); } // Aplica equalização for ($x = $startX; $x < $startX + $blockWidth; $x++) { for ($y = $startY; $y < $startY + $blockHeight; $y++) { $rgb = imagecolorat($this->processedImage, $x, $y); $colors = imagecolorsforindex($this->processedImage, $rgb); $newGray = $lut[$colors['red']]; $color = imagecolorallocate($this->processedImage, $newGray, $newGray, $newGray); imagesetpixel($this->processedImage, $x, $y, $color); } } } private function sharpenImage() { // Matriz de convolução para sharpening $sharpenMatrix = [ [-1, -1, -1], [-1, 9, -1], [-1, -1, -1] ]; $this->applyConvolution($sharpenMatrix); return $this; } private function applyConvolution($matrix) { $width = imagesx($this->processedImage); $height = imagesy($this->processedImage); $temp = imagecreatetruecolor($width, $height); imagecopy($temp, $this->processedImage, 0, 0, 0, 0, $width, $height); $matrixSize = count($matrix); $halfSize = floor($matrixSize / 2); for ($x = $halfSize; $x < $width - $halfSize; $x++) { for ($y = $halfSize; $y < $height - $halfSize; $y++) { $r = $g = $b = 0; for ($i = -$halfSize; $i <= $halfSize; $i++) { for ($j = -$halfSize; $j <= $halfSize; $j++) { $rgb = imagecolorat($temp, $x + $i, $y + $j); $colors = imagecolorsforindex($temp, $rgb); $weight = $matrix[$i + $halfSize][$j + $halfSize]; $r += $colors['red'] * $weight; $g += $colors['green'] * $weight; $b += $colors['blue'] * $weight; } } $r = min(255, max(0, $r)); $g = min(255, max(0, $g)); $b = min(255, max(0, $b)); $color = imagecolorallocate($this->processedImage, $r, $g, $b); imagesetpixel($this->processedImage, $x, $y, $color); } } imagedestroy($temp); return $this; } private function removeNoise() { // Aplica filtro mediano $temp = imagecreatetruecolor(imagesx($this->processedImage), imagesy($this->processedImage)); imagecopy($temp, $this->processedImage, 0, 0, 0, 0, imagesx($this->processedImage), imagesy($this->processedImage)); for ($x = 1; $x < imagesx($this->processedImage) - 1; $x++) { for ($y = 1; $y < imagesy($this->processedImage) - 1; $y++) { $values = array(); // Coleta valores dos pixels vizinhos for ($i = -1; $i <= 1; $i++) { for ($j = -1; $j <= 1; $j++) { $rgb = imagecolorat($temp, $x + $i, $y + $j); $colors = imagecolorsforindex($temp, $rgb); $values[] = $colors['red']; } } sort($values); $median = $values[4]; // Valor mediano $color = imagecolorallocate($this->processedImage, $median, $median, $median); imagesetpixel($this->processedImage, $x, $y, $color); } } imagedestroy($temp); return $this; } private function adjustBrightnessContrast() { // Ajusta brilho automaticamente imagefilter($this->processedImage, IMG_FILTER_BRIGHTNESS, 10); imagefilter($this->processedImage, IMG_FILTER_CONTRAST, -10); return $this; } private function normalizeBackground() { $width = imagesx($this->processedImage); $height = imagesy($this->processedImage); // Detecta cor do fundo (assume que os cantos são fundo) $cornerColors = array(); $cornerColors[] = imagecolorat($this->processedImage, 0, 0); $cornerColors[] = imagecolorat($this->processedImage, $width-1, 0); $cornerColors[] = imagecolorat($this->processedImage, 0, $height-1); $cornerColors[] = imagecolorat($this->processedImage, $width-1, $height-1); // Calcula cor média do fundo $avgBackground = 255; // Assumimos fundo branco // Normaliza fundo for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($this->processedImage, $x, $y); $colors = imagecolorsforindex($this->processedImage, $rgb); if ($colors['red'] > $avgBackground - 30) { // Se for próximo do fundo $color = imagecolorallocate($this->processedImage, 255, 255, 255); imagesetpixel($this->processedImage, $x, $y, $color); } } } return $this; } private function saveResult() { if (!isset($this->processedImage)) { throw new Exception('No processed image to save'); } $extension = strtolower(pathinfo($this->outputPath, PATHINFO_EXTENSION)); switch ($extension) { case 'jpg': case 'jpeg': imagejpeg($this->processedImage, $this->outputPath, 90); break; case 'png': imagepng($this->processedImage, $this->outputPath, 9); break; default: throw new Exception('Unsupported output image type'); } if (is_resource($this->srcImage)) { imagedestroy($this->srcImage); } if (is_resource($this->processedImage)) { imagedestroy($this->processedImage); } return $this; } private function getAdaptiveThreshold($image) { $width = imagesx($image); $height = imagesy($image); $blockSize = 40; // Tamanho do bloco para threshold adaptativo $C = 10; // Constante de ajuste // Cria imagem para o resultado $result = imagecreatetruecolor($width, $height); // Para cada bloco for ($x = 0; $x < $width; $x += $blockSize) { for ($y = 0; $y < $height; $y += $blockSize) { // Calcula média do bloco $sum = 0; $count = 0; $blockW = min($blockSize, $width - $x); $blockH = min($blockSize, $height - $y); for ($bx = 0; $bx < $blockW; $bx++) { for ($by = 0; $by < $blockH; $by++) { $rgb = imagecolorat($image, $x + $bx, $y + $by); $colors = imagecolorsforindex($image, $rgb); $gray = $colors['red']; // Já está em escala de cinza $sum += $gray; $count++; } } // Calcula threshold local $threshold = ($count > 0) ? ($sum / $count) - $C : 128; // Aplica threshold ao bloco for ($bx = 0; $bx < $blockW; $bx++) { for ($by = 0; $by < $blockH; $by++) { $rgb = imagecolorat($image, $x + $bx, $y + $by); $colors = imagecolorsforindex($image, $rgb); $gray = $colors['red']; if ($gray > $threshold) { $color = imagecolorallocate($result, 255, 255, 255); } else { $color = imagecolorallocate($result, 0, 0, 0); } imagesetpixel($result, $x + $bx, $color); } } } } return $threshold; } }