outputPath = $outputPath; // Carrega a imagem $this->srcImage = cv\imread($imagePath); if ($this->srcImage === null) { throw new Exception('Failed to load image'); } // Pipeline de processamento $this->detectDocument() ->correctPerspective() ->enhanceImage() ->saveResult(); return $this->outputPath; } private function detectDocument() { // Converte para escala de cinza $gray = cv\cvtColor($this->srcImage, cv\COLOR_BGR2GRAY); // Aplica blur para reduzir ruído $blur = cv\GaussianBlur($gray, new cv\Size(5, 5), 0); // Detecta bordas $edges = cv\Canny($blur, 75, 200); // Encontra contornos $contours = new cv\Mat(); $hierarchy = new cv\Mat(); cv\findContours($edges, $contours, $hierarchy, cv\RETR_LIST, cv\CHAIN_APPROX_SIMPLE); // Ordena contornos por área usort($contours, function($a, $b) { return cv\contourArea($b) - cv\contourArea($a); }); // Procura o contorno mais próximo de um documento foreach ($contours as $contour) { $peri = cv\arcLength($contour, true); $approx = cv\approxPolyDP($contour, 0.02 * $peri, true); if (count($approx) == 4) { $this->documentContour = $approx; break; } } return $this; } private function correctPerspective() { if (!isset($this->documentContour)) { return $this; } // Ordena os pontos para perspectiva $points = $this->orderPoints($this->documentContour); // Calcula dimensões do documento $width = max( sqrt(pow($points[1][0] - $points[0][0], 2) + pow($points[1][1] - $points[0][1], 2)), sqrt(pow($points[3][0] - $points[2][0], 2) + pow($points[3][1] - $points[2][1], 2)) ); $height = max( sqrt(pow($points[2][0] - $points[1][0], 2) + pow($points[2][1] - $points[1][1], 2)), sqrt(pow($points[3][0] - $points[0][0], 2) + pow($points[3][1] - $points[0][1], 2)) ); // Define pontos de destino $dst = cv\Mat::zeros([4, 2], CV_32F); $dst[0] = [0, 0]; $dst[1] = [$width - 1, 0]; $dst[2] = [$width - 1, $height - 1]; $dst[3] = [0, $height - 1]; // Aplica transformação de perspectiva $matrix = cv\getPerspectiveTransform($points, $dst); for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($image, $x, $y); $colors = imagecolorsforindex($image, $rgb); $value = ($colors['red'] + $colors['green'] + $colors['blue']) / 3; $total += $value; $count++; } } return $total / $count; } private function findDocumentBounds($binary) { $width = imagesx($binary); $height = imagesy($binary); $left = $width; $right = 0; $top = $height; $bottom = 0; for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($binary, $x, $y); if ($rgb == 0x000000) { // Pixel preto if ($x < $left) $left = $x; if ($x > $right) $right = $x; if ($y < $top) $top = $y; if ($y > $bottom) $bottom = $y; } } } if ($left < $right && $top < $bottom) { return ['left' => $left, 'right' => $right, 'top' => $top, 'bottom' => $bottom]; } return null; } 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; } }