From 70debcfc58d47154b4f0741ab0860fa9701b09a8 Mon Sep 17 00:00:00 2001 From: Orzu Ionut Date: Wed, 23 Jun 2021 11:12:45 +0300 Subject: [PATCH] Performance analyzer. Apply OCR on images in digital documents --- app/Console/Commands/AnalyzePerformance.php | 85 ++++++++++++++ app/Ingest/Convertor.php | 16 +-- app/Ingest/DocumentHandler.php | 11 +- app/Ingest/DocxConvertor.php | 45 ++++---- app/Ingest/OCR.php | 9 +- app/Ingest/Office.php | 62 +++++++++++ app/Ingest/OtherConvertor.php | 18 +-- app/Ingest/PDFConvertor.php | 104 +++++++++--------- app/Jobs/IngestDocuments.php | 66 +++++++++-- resources/python/dewarp/page_dewarp.py | 7 +- .../python/ocr/localize_text_tesseract.py | 29 +++++ resources/python/ocr/logo.jpg | Bin 0 -> 19798 bytes resources/python/ocr/results/.gitignore | 2 + 13 files changed, 332 insertions(+), 122 deletions(-) create mode 100644 app/Console/Commands/AnalyzePerformance.php create mode 100644 app/Ingest/Office.php create mode 100644 resources/python/ocr/localize_text_tesseract.py create mode 100644 resources/python/ocr/logo.jpg create mode 100644 resources/python/ocr/results/.gitignore diff --git a/app/Console/Commands/AnalyzePerformance.php b/app/Console/Commands/AnalyzePerformance.php new file mode 100644 index 0000000..d6141a2 --- /dev/null +++ b/app/Console/Commands/AnalyzePerformance.php @@ -0,0 +1,85 @@ +argument('path'); + + if ( ! is_dir($directoryPath)) { + $this->error('The path is invalid: not a directory.'); + + return; + } + + $redis = Redis::connection(); + + $redis->set('analyze_performance_time', Carbon::now()->format('U')); + $redis->set('analyze_performance_path', $directoryPath); + + $allFiles = $this->getDirContents($directoryPath); + + $redis->set('analyze_performance_remaining_files', count($allFiles)); + + foreach ($allFiles as $index => $file) { + $handler = new DocumentHandler($index, new UploadedFile($file, "File {$index}"), false); + + $handler->handle(); + } + + $this->info('Processing... When it\'s done the results will be added to the \'ingest_analyze_performance.txt\' file in the directory you have provided.'); + } + + protected function getDirContents($dir, &$results = array()) + { + $files = scandir($dir); + + foreach ($files as $key => $value) { + $path = realpath($dir . DIRECTORY_SEPARATOR . $value); + + if (!is_dir($path)) { + $results[] = $path; + } else if ($value != "." && $value != "..") { + $this->getDirContents($path, $results); + } + } + + return $results; + } +} diff --git a/app/Ingest/Convertor.php b/app/Ingest/Convertor.php index 8a204af..522cbee 100644 --- a/app/Ingest/Convertor.php +++ b/app/Ingest/Convertor.php @@ -45,22 +45,16 @@ class Convertor private function convertToHtml() { - (new Process(['export HOME=' . env('USER_HOME_PATH')]))->run(); + $office = new Office(); - $process = new Process([ - 'soffice', - '--headless', - '--convert-to', + $success = $office->run( 'html:HTML:EmbedImages', $this->storage->path($this->path), - '--outdir', $this->storage->path('contracts') - ]); - - $process->run(); + ); - if (!$process->isSuccessful()) { - throw new ProcessFailedException($process); + if (! $success) { + throw new \Exception('Something went wrong while tried converting to HTML for file: ' . $this->path); } $this->storage->delete($this->path); diff --git a/app/Ingest/DocumentHandler.php b/app/Ingest/DocumentHandler.php index 38e4dfe..6d69f12 100644 --- a/app/Ingest/DocumentHandler.php +++ b/app/Ingest/DocumentHandler.php @@ -9,6 +9,7 @@ class DocumentHandler { protected $id; protected $document; + protected $fromRequest; const DOCX_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; const DOC_MIME_TYPE = 'application/msword'; @@ -34,18 +35,18 @@ class DocumentHandler self::PLAIN_TEXT_TYPE => 'txt', ]; - public function __construct($id, $document) + public function __construct($id, $document, $fromRequest = true) { $this->id = $id; $this->document = $document; + $this->fromRequest = $fromRequest; } public function handle() { $storage = Storage::disk('local'); - $file = request()->file('document'); - $mimeType = $file->getClientMimeType(); + $mimeType = $this->document->getClientMimeType(); if (!array_key_exists($mimeType, $this->supportedFiles)) { throw new \Exception('File not supported.'); @@ -55,8 +56,8 @@ class DocumentHandler $id = str_replace(' ', '_', $this->id); - $path = $storage->putFileAs("contracts/$id", $file, "document.$type"); + $path = $storage->putFileAs("contracts/$id", $this->document, "document.$type"); - IngestDocuments::dispatch($this->id, $path, $type); + IngestDocuments::dispatch($this->id, $path, $type, $this->fromRequest); } } diff --git a/app/Ingest/DocxConvertor.php b/app/Ingest/DocxConvertor.php index 9940453..35c862f 100644 --- a/app/Ingest/DocxConvertor.php +++ b/app/Ingest/DocxConvertor.php @@ -9,33 +9,33 @@ class DocxConvertor extends AbstractConvertor { public function execute() { -// $this->convertToText(); -// -// $convertor = new TextConvertor($this->storage, "$this->directoryPath/document.txt"); -// -// $convertor->execute(); + $this->convertToPdfWithLibreOffice(); - $this->convertToPDF(); + $pdfFilePath = "$this->directoryPath/document.pdf"; - $convertor = new PDFConvertor($this->storage, "$this->directoryPath/document.pdf"); + if ( ! $this->storage->exists($pdfFilePath)) { + throw new \Exception('Failed to convert to PDF: ' . $pdfFilePath); + } + + $convertor = new PDFConvertor($this->storage, $pdfFilePath); $convertor->execute(); } - protected function convertToText() + protected function convertToPDF() { (new Process(['export HOME=' . env('USER_HOME_PATH')]))->run(); $process = new Process([ - 'soffice', - '--headless', - '--convert-to', - 'txt', + 'unoconv', + '-f', + 'pdf', +// '-c=socket,host=localhost,port=' . (2000 + rand(2, 7)) . ';urp;StarOffice.ComponentContext', $this->storage->path($this->path), - '--outdir', - $this->storage->path($this->directoryPath) ]); + $process->setTimeout(10); + $process->run(); if (!$process->isSuccessful()) { @@ -45,21 +45,18 @@ class DocxConvertor extends AbstractConvertor $this->deleteOriginalDocument(); } - protected function convertToPDF() + protected function convertToPdfWithLibreOffice() { - (new Process(['export HOME=' . env('USER_HOME_PATH')]))->run(); + $office = new Office(); - $process = new Process([ - 'unoconv', - '-f', + $success = $office->run( 'pdf', $this->storage->path($this->path), - ]); - - $process->run(); + $this->storage->path($this->directoryPath) + ); - if (!$process->isSuccessful()) { - throw new ProcessFailedException($process); + if (! $success) { + throw new \Exception('Failed when converting from DOCX to PDF for file: ' . $this->path); } $this->deleteOriginalDocument(); diff --git a/app/Ingest/OCR.php b/app/Ingest/OCR.php index ecb8ad9..5c9483b 100644 --- a/app/Ingest/OCR.php +++ b/app/Ingest/OCR.php @@ -52,13 +52,10 @@ class OCR $directory = pathinfo($this->path, PATHINFO_DIRNAME); $newPath = "$directory/$filePath"; - $moved = File::move(base_path($filePath), $newPath); - - if ( ! $moved) { - throw new \Exception('Something went wrong while moving file.'); + // The file may not be created by the library for various reasons, including if it does not have text. + if (File::exists($newPath)) { + $this->path = $newPath; } - - $this->path = $newPath; } protected function applyDeskew() diff --git a/app/Ingest/Office.php b/app/Ingest/Office.php new file mode 100644 index 0000000..4581dfd --- /dev/null +++ b/app/Ingest/Office.php @@ -0,0 +1,62 @@ +id = uniqid(); + $this->directory = 'soffice-dir-' . $this->id; + + (new Process(['export HOME=' . env('USER_HOME_PATH')]))->run(); + } + + public function run($convertTo, $filePath, $directoryPath) + { + $this->makeTemporaryDirectory(); + + $success = $this->runConversion($convertTo, $filePath, $directoryPath); + + // @TODO Does not work at the moment. +// $this->removeTemporaryDirectory(); + + return $success; + } + + protected function runConversion($convertTo, $filePath, $directoryPath) + { + $process = new Process([ + 'soffice', + '--accept="pipe,name=soffice-pipe-' . $this->id . ';urp;StarOffice.ServiceMananger"', + '-env:UserInstallation=file:///tmp/' . $this->directory, + '--headless', + '--convert-to', + $convertTo, + $filePath, + '--outdir', + $directoryPath + ]); + + $process->setTimeout(10); + + $process->run(); + + return $process->isSuccessful(); + } + + protected function makeTemporaryDirectory() + { + (new Process(['mkdir /tmp/' . $this->directory]))->run(); + } + + protected function removeTemporaryDirectory() + { + (new Process(['rm -rf /tmp/' . $this->directory]))->run(); + } +} diff --git a/app/Ingest/OtherConvertor.php b/app/Ingest/OtherConvertor.php index 53f6839..3ab91bb 100644 --- a/app/Ingest/OtherConvertor.php +++ b/app/Ingest/OtherConvertor.php @@ -24,25 +24,19 @@ class OtherConvertor extends AbstractConvertor */ private function convertToDocx() { - (new Process(['export HOME=' . env('USER_HOME_PATH')]))->run(); - /** * Convert doc,dot,rtf,odt to docx */ - $process = new Process([ - 'soffice', - '--headless', - '--convert-to', + $office = new Office(); + + $success = $office->run( 'docx', $this->storage->path($this->path), - '--outdir', $this->storage->path($this->directoryPath) - ]); - - $process->run(); + ); - if (!$process->isSuccessful()) { - throw new ProcessFailedException($process); + if (! $success) { + throw new \Exception('Something went wrong while tried converting to DOCX for file: ' . $this->path); } $this->deleteOriginalDocument(); diff --git a/app/Ingest/PDFConvertor.php b/app/Ingest/PDFConvertor.php index 7abde61..c3a8a5b 100644 --- a/app/Ingest/PDFConvertor.php +++ b/app/Ingest/PDFConvertor.php @@ -12,47 +12,13 @@ class PDFConvertor extends AbstractConvertor { // $this->prepareForConvertPDF(); - $result = $this->getFileContents(); + $contents = $this->getFileContents(); - if ( ! $result['has_images'] && ! $result['has_text']) { + if ( ! $contents) { throw new \Exception('Cannot get pdf file contents.'); } - if ($result['has_text']) { - $mdContents = ''; - - foreach ($result['htmls'] as $html) { - $converter = new HtmlConverter(); - $converter->getConfig()->setOption('strip_tags', true); - - $contents = $converter->convert($html); - - $mdContents = $mdContents . "\n\n" . $contents; - } - - $this->storage->put("$this->directoryPath/document.md", $mdContents); - - return; - } - - // Only contains images. - $imagesContent = ''; - $files = $this->storage->allFiles($this->path); - - foreach ($files as $file) { - // Only get the image files from the directory, it may contain some empty html files too. - - // @TODO Only OCR images with text and delete them afterwards, the remaining ignore and keep. - if (in_array(pathinfo($file, PATHINFO_EXTENSION), ['jpg', 'png'])) { - $ocr = new OCR($this->storage->path($file)); - - $imagesContent = $imagesContent . $ocr->execute(); - - $this->storage->delete($file); - } - } - - $this->storage->put("$this->directoryPath/document.md", $imagesContent); + $this->storage->put("$this->directoryPath/document.md", $contents); } protected function getFileContents() @@ -115,13 +81,14 @@ class PDFConvertor extends AbstractConvertor ksort($orderedList[$pageNumber]); } - $htmls = []; $hasImages = false; $hasText = false; $imagesCount = 0; $imagesInFooter = true; + $mdContents = ''; + try { foreach ($orderedList as $page) { $html = ''; @@ -132,19 +99,38 @@ class PDFConvertor extends AbstractConvertor foreach ($items as $p) { if ($p->getName() == 'image') { - $hasImages = true; + $basePath = $this->storage->path(''); + $imageFilePath = str_replace($basePath, '', $p['src']); - $imagesCount += 1; - $caption = "Fig. $imagesCount"; + $textContents = $this->applyOCR($imageFilePath); - $imageHTML = $this->handleImage($p, $caption); + if ($textContents) { + if ($html) { + $mdContents = $mdContents . $this->convertHtmlToMD($html) . "\n"; - if ( ! $imagesInFooter) { - $html = $html . $imageHTML; + $html = ''; + } + + $mdContents = $mdContents . $textContents . "\n"; + + $this->storage->delete($imageFilePath); + + $hasText = true; } else { - $html = $html . "

$caption

"; + $hasImages = true; + + $imagesCount += 1; + $caption = "Fig. $imagesCount"; - $footerImages[] = $imageHTML; + $imageHTML = $this->handleImage($p, $caption); + + if ( ! $imagesInFooter) { + $html = $html . $imageHTML; + } else { + $html = $html . "

$caption

"; + + $footerImages[] = $imageHTML; + } } } @@ -161,11 +147,10 @@ class PDFConvertor extends AbstractConvertor if ($imagesInFooter) { foreach ($footerImages as $index => $footerImage) { $html = $html . '

' . $footerImage . '

'; -// $html = $html . '

Fig. ' . ($index + 1) . '

'; } } - $htmls[] = '' . $html . ''; + $mdContents = $mdContents . $this->convertHtmlToMD($html) . "\n\n"; } } catch (\Exception $exception) { $this->storage->deleteDirectory($this->directoryPath); @@ -183,11 +168,7 @@ class PDFConvertor extends AbstractConvertor $this->storage->delete($xmlFilePath); } - return [ - 'has_images' => $hasImages, - 'has_text' => $hasText, - 'htmls' => $htmls, - ]; + return $mdContents; } protected function handleImage($p, $caption) @@ -254,6 +235,23 @@ class PDFConvertor extends AbstractConvertor return 'span'; } + protected function applyOCR($path) + { + $ocr = new OCR($this->storage->path($path)); + + return $ocr->execute(); + } + + protected function convertHtmlToMD($contents) + { + $html = '' . $contents . ''; + + $converter = new HtmlConverter(); + $converter->getConfig()->setOption('strip_tags', true); + + return $converter->convert($html); + } + protected function prepareForConvertPDF() { (new Process(['export HOME=' . env('USER_HOME_PATH')]))->run(); diff --git a/app/Jobs/IngestDocuments.php b/app/Jobs/IngestDocuments.php index 1c08f87..cb654f2 100644 --- a/app/Jobs/IngestDocuments.php +++ b/app/Jobs/IngestDocuments.php @@ -11,7 +11,9 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; class IngestDocuments implements ShouldQueue @@ -21,6 +23,7 @@ class IngestDocuments implements ShouldQueue protected $id; private $path; protected $type; + protected $fromRequest; /** * @var \Illuminate\Contracts\Filesystem\Filesystem @@ -53,12 +56,14 @@ class IngestDocuments implements ShouldQueue * @param $id * @param string $path * @param $type + * @param $fromRequest */ - public function __construct($id, string $path, $type) + public function __construct($id, string $path, $type, $fromRequest) { $this->id = $id; $this->path = $path; $this->type = $type; + $this->fromRequest = $fromRequest; $this->storage = Storage::disk('local'); $this->parserDocx = new ParseDocx(); @@ -86,7 +91,17 @@ class IngestDocuments implements ShouldQueue return; } - SendToCore::dispatch($this->id, pathinfo($this->path, PATHINFO_DIRNAME)); + $directoryPath = pathinfo($this->path, PATHINFO_DIRNAME); + + if ($this->fromRequest) { + SendToCore::dispatch($this->id, $directoryPath); + + return; + } + + $this->storage->deleteDirectory($directoryPath); + + $this->updateAnalyzer(); } public function failed() @@ -95,13 +110,48 @@ class IngestDocuments implements ShouldQueue $this->storage = Storage::disk('local'); } - Log::error('Ingest documents failed.'); + Log::error('Ingest documents failed. ' . $this->path); + + $directoryPath = pathinfo($this->path, PATHINFO_DIRNAME); -// // @TODO Delete docx, txt and md files. -// if ($this->storage->exists($this->path)) { -// $this->storage->delete($this->path); -// } + if ($this->fromRequest) { + SendToCore::dispatch($this->id, $directoryPath, true); + + return; + } + + $this->storage->deleteDirectory($directoryPath); + + $this->updateAnalyzer(true); + } + + protected function updateAnalyzer($failed = false) + { + $redis = Redis::connection(); + + if ($failed) { + $redis->set('analyze_performance_error', '1'); + } + + $remainingFiles = $redis->get('analyze_performance_remaining_files'); + $remainingFiles -= 1; + + if ($remainingFiles === 0) { + $startedAt = $redis->get('analyze_performance_time'); + $endedAt = Carbon::now()->format('U'); + $directoryPath = $redis->get('analyze_performance_path'); + + $data = 'Time elapsed in seconds: ' . ($endedAt - $startedAt) . "\n"; + + if ($failed) { + $data = $data . 'Something went wrong while processing the files.'; + } + + file_put_contents($directoryPath . '/ingest_analyze_performance.txt', $data); + + return; + } - SendToCore::dispatch($this->id, pathinfo($this->path, PATHINFO_DIRNAME), true); + $redis->set('analyze_performance_remaining_files', $remainingFiles); } } diff --git a/resources/python/dewarp/page_dewarp.py b/resources/python/dewarp/page_dewarp.py index 222df32..c0b1e7e 100755 --- a/resources/python/dewarp/page_dewarp.py +++ b/resources/python/dewarp/page_dewarp.py @@ -785,7 +785,7 @@ def get_page_dims(corners, rough_dims, params): return dims -def remap_image(name, img, small, page_dims, params): +def remap_image(name, dirname, img, small, page_dims, params): height = 0.5 * page_dims[1] * OUTPUT_ZOOM * img.shape[0] height = round_nearest_multiple(height, REMAP_DECIMATE) @@ -833,7 +833,7 @@ def remap_image(name, img, small, page_dims, params): pil_image = pil_image.convert('1') threshfile = name + '_thresh.png' - pil_image.save(threshfile, dpi=(OUTPUT_DPI, OUTPUT_DPI)) + pil_image.save(dirname + '/' + threshfile, dpi=(OUTPUT_DPI, OUTPUT_DPI)) if DEBUG_LEVEL >= 1: height = small.shape[0] @@ -861,6 +861,7 @@ def main(): img = cv2.imread(imgfile) small = resize_to_screen(img) basename = os.path.basename(imgfile) + dirname = os.path.dirname(imgfile) name, _ = os.path.splitext(basename) print('loaded', basename, 'with size', imgsize(img), end=' ') @@ -907,7 +908,7 @@ def main(): page_dims = get_page_dims(corners, rough_dims, params) - outfile = remap_image(name, img, small, page_dims, params) + outfile = remap_image(name, dirname, img, small, page_dims, params) outfiles.append(outfile) diff --git a/resources/python/ocr/localize_text_tesseract.py b/resources/python/ocr/localize_text_tesseract.py new file mode 100644 index 0000000..63a162b --- /dev/null +++ b/resources/python/ocr/localize_text_tesseract.py @@ -0,0 +1,29 @@ +import cv2 + +image = cv2.imread("logo.jpg", 1) + +img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + +cv2.threshold(img, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU, img) +cv2.bitwise_not(img, img) + +rect_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 5)) + +img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, rect_kernel) +contours, hier = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) + +if len(contours) != 0: + ROI_number = 0 + for c in contours: + x,y,w,h = cv2.boundingRect(c) + + # Depends on text size, so the greater the value the less objects we get. + if (h > 50): + cv2.rectangle(image, (x,y), (x+w,y+h), (0,0,255), 1) + + ROI = image[y:y+h, x:x+w] + cv2.imwrite('results/ROI_{}.png'.format(ROI_number), ROI) + ROI_number += 1 + +cv2.imshow("Result", image) +cv2.waitKey(0) diff --git a/resources/python/ocr/logo.jpg b/resources/python/ocr/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..90b41ddea5b5aebce43bcd1a98f940a3eb618a03 GIT binary patch literal 19798 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