RED-7669: optimize OCR-module performance

* binarize images after reading
This commit is contained in:
Kilian Schuettler 2023-11-21 14:37:18 +01:00
parent 6f99664906
commit bb5b4a2fd8
11 changed files with 182 additions and 94 deletions

View File

@ -0,0 +1,27 @@
package com.knecon.fforesight.service.ocr.processor.model;
import java.awt.image.BufferedImage;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
import org.apache.pdfbox.util.Matrix;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
@Getter
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class ExtractedImage {
int pageNumber;
QuadPoint position;
int height;
int width;
BufferedImage image;
Matrix ctm;
int numberOnPage;
PDColorSpace colorSpace;
}

View File

@ -11,6 +11,8 @@ import org.apache.pdfbox.util.Matrix;
import com.knecon.fforesight.service.ocr.processor.service.threads.OCRThread; import com.knecon.fforesight.service.ocr.processor.service.threads.OCRThread;
import com.knecon.fforesight.service.ocr.processor.utils.ImageProcessingUtils; import com.knecon.fforesight.service.ocr.processor.utils.ImageProcessingUtils;
import com.pdftron.sdf.Obj; import com.pdftron.sdf.Obj;
import com.sun.jna.StringArray;
import com.sun.jna.ptr.PointerByReference;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
@ -56,6 +58,19 @@ public class ExtractedOcrImage implements OcrImage {
} }
public ExtractedOcrImage(ExtractedImage image, int targetDpi) {
this.pageNumber = image.getPageNumber();
this.numberOnPage = image.getNumberOnPage();
this.ctm = image.getCtm();
this.originalHeight = image.getImage().getHeight();
this.originalWidth = image.getImage().getWidth();
float imageDPI = Math.abs(image.getImage().getWidth() / (ctm.getScalingFactorX() / 72));
this.pix = binarize(image.getImage(), imageDPI, targetDpi);
this.height = pix.h;
this.width = pix.w;
}
@SneakyThrows @SneakyThrows
private Pix binarize(BufferedImage image, float imageDpi, int targetDpi) { private Pix binarize(BufferedImage image, float imageDpi, int targetDpi) {

View File

@ -2,10 +2,12 @@ package com.knecon.fforesight.service.ocr.processor.model;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import com.knecon.fforesight.service.ocr.processor.service.threads.OCRThread; import com.knecon.fforesight.service.ocr.processor.service.threads.OCRThread;
import com.knecon.fforesight.service.ocr.processor.utils.PdfDpiCalculator; import com.knecon.fforesight.service.ocr.processor.utils.PdfDpiCalculator;
import lombok.SneakyThrows;
import net.sourceforge.lept4j.Leptonica1; import net.sourceforge.lept4j.Leptonica1;
import net.sourceforge.lept4j.Pix; import net.sourceforge.lept4j.Pix;
import net.sourceforge.lept4j.util.LeptUtils; import net.sourceforge.lept4j.util.LeptUtils;
@ -62,6 +64,20 @@ public interface OcrImage {
} }
@SneakyThrows
default BufferedImage getBufferedImage() {
return LeptUtils.convertPixToImage(getPix());
}
@SneakyThrows
default BufferedImage getRotatedBufferedImage() {
return LeptUtils.convertPixToImage(getRotatedPix());
}
/** /**
* Retrieves the rotation degree of the OCR image. * Retrieves the rotation degree of the OCR image.
* *

View File

@ -97,4 +97,10 @@ public record QuadPoint(Point2D a, Point2D b, Point2D c, Point2D d) {
d().getY()); d().getY());
} }
public double size() {
return a().distance(b()) * a().distance(d());
}
} }

View File

@ -24,6 +24,7 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.util.Matrix; import org.apache.pdfbox.util.Matrix;
import com.knecon.fforesight.service.ocr.processor.model.ExtractedImage;
import com.knecon.fforesight.service.ocr.processor.model.ExtractedOcrImage; import com.knecon.fforesight.service.ocr.processor.model.ExtractedOcrImage;
import com.knecon.fforesight.service.ocr.processor.settings.OcrServiceSettings; import com.knecon.fforesight.service.ocr.processor.settings.OcrServiceSettings;
@ -34,7 +35,7 @@ import lombok.SneakyThrows;
public class ImageStreamEngine extends PDFStreamEngine { public class ImageStreamEngine extends PDFStreamEngine {
private ExtractedOcrImage currentImageOnPage; private ExtractedOcrImage currentImageOnPage;
private List<ExtractedOcrImage> imagesOnCurrentPage; private List<ExtractedImage> imagesOnCurrentPage;
private OcrServiceSettings settings; private OcrServiceSettings settings;
private int pageNum; private int pageNum;
@ -69,21 +70,14 @@ public class ImageStreamEngine extends PDFStreamEngine {
} }
Matrix imageCTM = getGraphicsState().getCurrentTransformationMatrix(); Matrix imageCTM = getGraphicsState().getCurrentTransformationMatrix();
if (imageXObject.getColorSpace() instanceof PDDeviceRGB) { this.imagesOnCurrentPage.add(new ExtractedImage(pageNum,
BufferedImage image = imageXObject.getImage(); imageXObject.getHeight(),
this.currentImageOnPage = new ExtractedOcrImage(pageNum, imagesOnCurrentPage.size(), image, imageCTM, settings.getDpi()); imageXObject.getWidth(),
} else if (imageXObject.getColorSpace() instanceof PDDeviceGray) { imageXObject.getImage(),
BufferedImage image = imageXObject.getImage(); imageCTM,
this.currentImageOnPage = new ExtractedOcrImage(pageNum, imagesOnCurrentPage.size(), image, imageCTM, settings.getDpi()); imagesOnCurrentPage.size(),
} else { imageXObject.getColorSpace()));
BufferedImage pdfImage = imageXObject.getImage();
BufferedImage image = new BufferedImage(pdfImage.getWidth(), pdfImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
Graphics g = image.getGraphics();
g.drawImage(pdfImage, 0, 0, null);
g.dispose();
this.currentImageOnPage = new ExtractedOcrImage(pageNum, imagesOnCurrentPage.size(), image, imageCTM, settings.getDpi());
}
this.imagesOnCurrentPage.add(this.currentImageOnPage);
//imagesOnPages.add(this.currentImageOnPage); //imagesOnPages.add(this.currentImageOnPage);
} else if (xobject instanceof PDFormXObject) { } else if (xobject instanceof PDFormXObject) {
PDFormXObject form = (PDFormXObject) xobject; PDFormXObject form = (PDFormXObject) xobject;

View File

@ -107,7 +107,7 @@ public class OCRService {
int numberOfOcrThreads = Math.min(settings.getOcrThreadCount(), document.getNumberOfPages()); int numberOfOcrThreads = Math.min(settings.getOcrThreadCount(), document.getNumberOfPages());
stats = new Statistics(numberOfExtractThreads, numberOfOcrThreads); stats = new Statistics(numberOfExtractThreads, numberOfOcrThreads);
BlockingQueue<OcrImage> ocrImageQueue = new ArrayBlockingQueue<>(numberOfOcrThreads); BlockingQueue<OcrImage> ocrImageQueue = new ArrayBlockingQueue<>(2 * numberOfOcrThreads);
OcrImageFactory ocrImageFactory = new OcrImageFactory(document, OcrImageFactory ocrImageFactory = new OcrImageFactory(document,
documentFile, documentFile,

View File

@ -5,10 +5,10 @@ import java.util.List;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import com.knecon.fforesight.service.ocr.processor.model.ExtractedImage;
import com.knecon.fforesight.service.ocr.processor.model.ExtractedOcrImage; import com.knecon.fforesight.service.ocr.processor.model.ExtractedOcrImage;
import com.knecon.fforesight.service.ocr.processor.model.OcrImage; import com.knecon.fforesight.service.ocr.processor.model.OcrImage;
import com.knecon.fforesight.service.ocr.processor.service.ImageStreamEngine; import com.knecon.fforesight.service.ocr.processor.service.ImageStreamEngine;
@ -26,6 +26,7 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class ImageExtractionThread extends Thread { public class ImageExtractionThread extends Thread {
static double FULL_PAGE_IMAGE_THRESHOLD = 0.98;
static double IMAGE_ALIGNMENT_THRESHOLD = 1; static double IMAGE_ALIGNMENT_THRESHOLD = 1;
int id; int id;
@ -40,6 +41,7 @@ public class ImageExtractionThread extends Thread {
BlockingQueue<OcrImage> imageOutputQueue; BlockingQueue<OcrImage> imageOutputQueue;
List<Integer> stitchedPageNumbers; List<Integer> stitchedPageNumbers;
@SneakyThrows @SneakyThrows
@Override @Override
public void run() { public void run() {
@ -48,20 +50,21 @@ public class ImageExtractionThread extends Thread {
for (Integer pageIndex : pageIndices) { for (Integer pageIndex : pageIndices) {
try (PDDocument document = Loader.loadPDF(documentFile)) { // load new PDDocument for thread safety, also keeps RAM usage low. try (PDDocument document = Loader.loadPDF(documentFile)) { // load new PDDocument for thread safety, also keeps RAM usage low.
timestamp = System.currentTimeMillis(); timestamp = System.currentTimeMillis();
List<ExtractedOcrImage> extractedOcrImages = getExtractedOcrImages(pageIndex, document); List<ExtractedImage> extractedImages = getExtractedOcrImages(pageIndex, document);
stats.increaseImageExtraction(id, System.currentTimeMillis() - timestamp); stats.increaseImageExtraction(id, System.currentTimeMillis() - timestamp);
if (extractedOcrImages.isEmpty()) { if (extractedImages.isEmpty()) {
logger.logPageSkipped(pageIndex); logger.logPageSkipped(pageIndex);
} }
if (checkForStitchedImages(extractedOcrImages)) { if (checkForStitchedImages(extractedImages, document.getPage(pageIndex - 1))) {
stitchedPageNumbers.add(pageIndex); stitchedPageNumbers.add(pageIndex);
logger.addImagesToProcess(pageIndex, 0); logger.addImagesToProcess(pageIndex, 0);
continue; continue;
} }
for (ExtractedOcrImage image : extractedOcrImages) { for (ExtractedImage image : extractedImages) {
imageOutputQueue.put(image); ExtractedOcrImage ocrImage = new ExtractedOcrImage(image, settings.getDpi());
imageOutputQueue.put(ocrImage);
logger.addImagesToProcess(image.getPageNumber(), image.getNumberOnPage()); logger.addImagesToProcess(image.getPageNumber(), image.getNumberOnPage());
} }
} }
@ -69,7 +72,7 @@ public class ImageExtractionThread extends Thread {
} }
private List<ExtractedOcrImage> getExtractedOcrImages(Integer pageIndex, PDDocument document) { private List<ExtractedImage> getExtractedOcrImages(Integer pageIndex, PDDocument document) {
PDPage page = document.getPage(pageIndex - 1); PDPage page = document.getPage(pageIndex - 1);
ImageStreamEngine imageStreamEngine = new ImageStreamEngine(settings); ImageStreamEngine imageStreamEngine = new ImageStreamEngine(settings);
@ -79,22 +82,22 @@ public class ImageExtractionThread extends Thread {
@SneakyThrows @SneakyThrows
private boolean checkForStitchedImages(List<ExtractedOcrImage> imagesOnCurrentPage) { private boolean checkForStitchedImages(List<ExtractedImage> imagesOnCurrentPage, PDPage page) {
if (imagesOnCurrentPage.size() <= 1) { if (imagesOnCurrentPage.isEmpty()) {
return false; return false;
} }
//checking for intersections or direct alignment of images for (ExtractedImage imageOnPage : imagesOnCurrentPage) {
ExtractedOcrImage[] imageOnPagesArray = new ExtractedOcrImage[imagesOnCurrentPage.size()]; if (imageOnPage.getImageCoordinatesInInitialUserSpace().size() > FULL_PAGE_IMAGE_THRESHOLD * page.getCropBox().getHeight() * page.getCropBox().getWidth()) {
int index = 0; return true;
for (ExtractedOcrImage imageOnPage : imagesOnCurrentPage) {
imageOnPagesArray[index] = imageOnPage;
index++;
} }
for (int j = 0; j < imageOnPagesArray.length; j++) { }
for (int i = j + 1; i < imageOnPagesArray.length; i++) {
if (imageOnPagesArray[j].getImageCoordinatesInInitialUserSpace().aligns(imageOnPagesArray[i].getImageCoordinatesInInitialUserSpace(), IMAGE_ALIGNMENT_THRESHOLD)) { //checking for intersections or direct alignment of images
for (int j = 0; j < imagesOnCurrentPage.size(); j++) {
for (int i = j + 1; i < imagesOnCurrentPage.size(); i++) {
if (imagesOnCurrentPage.get(j).getImageCoordinatesInInitialUserSpace().aligns(imagesOnCurrentPage.get(i).getImageCoordinatesInInitialUserSpace(), IMAGE_ALIGNMENT_THRESHOLD)) {
// TODO: see if we can stitch aligning images using BufferedImage and skip the gs conversion entirely // TODO: see if we can stitch aligning images using BufferedImage and skip the gs conversion entirely
return true; return true;
} }

View File

@ -1,6 +1,10 @@
package com.knecon.fforesight.service.ocr.processor.service.threads; package com.knecon.fforesight.service.ocr.processor.service.threads;
import static net.sourceforge.tess4j.ITessAPI.TRUE; import static net.sourceforge.tess4j.ITessAPI.TRUE;
import static net.sourceforge.tess4j.TessAPI1.TessBaseAPICreate;
import static net.sourceforge.tess4j.TessAPI1.TessBaseAPIInit1;
import static net.sourceforge.tess4j.TessAPI1.TessBaseAPISetPageSegMode;
import static net.sourceforge.tess4j.TessAPI1.TessBaseAPISetVariable;
import java.io.File; import java.io.File;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
@ -16,6 +20,7 @@ import com.knecon.fforesight.service.ocr.processor.model.OcrResult;
import com.knecon.fforesight.service.ocr.processor.service.OcrProgressLogger; import com.knecon.fforesight.service.ocr.processor.service.OcrProgressLogger;
import com.knecon.fforesight.service.ocr.processor.service.Statistics; import com.knecon.fforesight.service.ocr.processor.service.Statistics;
import com.knecon.fforesight.service.ocr.processor.utils.Tesseract2; import com.knecon.fforesight.service.ocr.processor.utils.Tesseract2;
import com.sun.jna.StringArray;
import com.sun.jna.ptr.PointerByReference; import com.sun.jna.ptr.PointerByReference;
import lombok.AccessLevel; import lombok.AccessLevel;
@ -42,8 +47,8 @@ public class OCRThread extends Thread {
OcrProgressLogger logger; OcrProgressLogger logger;
Statistics stats; Statistics stats;
OcrServiceSettings settings; OcrServiceSettings settings;
Tesseract2 instance;
ITessAPI.TessBaseAPI detectionScriptHandle; ITessAPI.TessBaseAPI detectionScriptHandle;
ITessAPI.TessBaseAPI tesseractHandle;
public OCRThread(int id, public OCRThread(int id,
@ -61,8 +66,8 @@ public class OCRThread extends Thread {
this.logger = logger; this.logger = logger;
this.stats = stats; this.stats = stats;
this.settings = settings; this.settings = settings;
this.instance = createInstance(settings);
this.detectionScriptHandle = initDetectionScriptHandle(); this.detectionScriptHandle = initDetectionScriptHandle();
this.tesseractHandle = initTesseractHandle(settings);
} }
@ -88,9 +93,9 @@ public class OCRThread extends Thread {
} }
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {
log.debug("Processed all Images, finishing."); log.debug("Processed all Images, finishing.");
}
TessAPI1.TessBaseAPIDelete(this.detectionScriptHandle); TessAPI1.TessBaseAPIDelete(this.detectionScriptHandle);
TessAPI1.TessBaseAPIDelete(this.tesseractHandle);
}
} }
@ -107,10 +112,8 @@ public class OCRThread extends Thread {
Pix rotatedPix = image.getRotatedPix(); Pix rotatedPix = image.getRotatedPix();
executeTesseract(psm, image.getDpi(), rotatedPix, tesseractOutputFileName); executeTesseract(psm, image.getDpi(), rotatedPix, tesseractOutputFileName);
synchronized (OCRThread.class) {
image.destroyPix(); image.destroyPix();
LeptUtils.disposePix(rotatedPix); LeptUtils.disposePix(rotatedPix);
}
results.add(OcrResult.create(image, tesseractOutputFileName)); results.add(OcrResult.create(image, tesseractOutputFileName));
logger.logImageFinished(image, psm); logger.logImageFinished(image, psm);
@ -145,22 +148,38 @@ public class OCRThread extends Thread {
orient_deg = orientationDegreeResultBuffer.get(); orient_deg = orientationDegreeResultBuffer.get();
} }
synchronized (OCRThread.class) {
TessAPI1.TessBaseAPIClear(detectionScriptHandle); TessAPI1.TessBaseAPIClear(detectionScriptHandle);
}
return orient_deg; return orient_deg;
} }
synchronized private static ITessAPI.TessBaseAPI initDetectionScriptHandle() { private static ITessAPI.TessBaseAPI initDetectionScriptHandle() {
ITessAPI.TessBaseAPI handle = TessAPI1.TessBaseAPICreate(); synchronized (OCRThread.class) {
ITessAPI.TessBaseAPI handle = TessBaseAPICreate();
String datapath = System.getenv("TESSDATA_PREFIX"); String datapath = System.getenv("TESSDATA_PREFIX");
// TessBaseAPISetVariable(handle, "debug_file", "/dev/null");
TessAPI1.TessBaseAPIInit3(handle, datapath, "osd"); TessAPI1.TessBaseAPIInit3(handle, datapath, "osd");
return handle; return handle;
} }
}
synchronized private static ITessAPI.TessBaseAPI initTesseractHandle(OcrServiceSettings settings) {
synchronized (OCRThread.class) {
ITessAPI.TessBaseAPI handle = TessBaseAPICreate();
String datapath = System.getenv("TESSDATA_PREFIX");
// TessBaseAPISetVariable(handle, "debug_file", "/dev/null");
TessBaseAPIInit1(handle, datapath, settings.getLanguages(), 1, new PointerByReference(), 0);
return handle;
}
}
@SneakyThrows @SneakyThrows
@ -173,19 +192,14 @@ public class OCRThread extends Thread {
Leptonica1.pixWrite(folder + "/pix_" + a[a.length - 1] + ".png", pix, 3); Leptonica1.pixWrite(folder + "/pix_" + a[a.length - 1] + ".png", pix, 3);
} }
instance.setVariable("user_defined_dpi", String.valueOf(dpi)); TessBaseAPISetPageSegMode(tesseractHandle, psm);
instance.setPageSegMode(psm);
instance.createDocumentsWithResults(pix, null, tesseractOutputFileName, List.of(ITesseract.RenderedFormat.HOCR), ITessAPI.TessPageIteratorLevel.RIL_BLOCK);
}
Tesseract2.createDocumentsWithResults(pix,
private static Tesseract2 createInstance(OcrServiceSettings settings) { null,
tesseractOutputFileName,
Tesseract2 instance = new Tesseract2(); List.of(ITesseract.RenderedFormat.HOCR),
instance.setVariable("debug_file", "/dev/null"); // remove warnings from std out ITessAPI.TessPageIteratorLevel.RIL_BLOCK,
instance.setOcrEngineMode(1); // set to LSTM based Engine tesseractHandle);
instance.setLanguage(settings.getLanguages());
return instance;
} }
} }

View File

@ -12,9 +12,9 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class OcrServiceSettings { public class OcrServiceSettings {
int ocrThreadCount = 4; // Number of OCR threads int ocrThreadCount = 16; // Number of OCR threads
int imageExtractThreadCount = 2; // Number of image extraction threads int imageExtractThreadCount = 5; // Number of image extraction threads
int gsProcessCount = 2; // Number of Ghostscript processes int gsProcessCount = 5; // Number of Ghostscript processes
int dpi = 300; // Target DPI for binarized images int dpi = 300; // Target DPI for binarized images
int psmOverride = -1; // Overrides the page segmentation mode if > 0 int psmOverride = -1; // Overrides the page segmentation mode if > 0
int minImageHeight = 20; // Minimum height for images to be processed int minImageHeight = 20; // Minimum height for images to be processed

View File

@ -1,45 +1,54 @@
package com.knecon.fforesight.service.ocr.processor.utils; package com.knecon.fforesight.service.ocr.processor.utils;
import static net.sourceforge.tess4j.ITesseract.DOCUMENT_TITLE;
import java.awt.Rectangle; import java.awt.Rectangle;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.knecon.fforesight.service.ocr.processor.service.threads.OCRThread;
import com.sun.jna.Pointer; import com.sun.jna.Pointer;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.sourceforge.lept4j.Pix; import net.sourceforge.lept4j.Pix;
import net.sourceforge.tess4j.ITessAPI;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.OCRResult; import net.sourceforge.tess4j.OCRResult;
import net.sourceforge.tess4j.TessAPI1; import net.sourceforge.tess4j.TessAPI1;
import net.sourceforge.tess4j.Tesseract1;
import net.sourceforge.tess4j.TesseractException;
import net.sourceforge.tess4j.Word; import net.sourceforge.tess4j.Word;
@Slf4j @Slf4j
/** /**
* Overriden version only so I can use Tesseract1 with Pixs instead of BufferedImages. All Functions are copied and then the BufferedImage -> Pix conversion deleted. * Overriden version only so I can use Tesseract1 with Pixs instead of BufferedImages. All Functions are copied and then the BufferedImage -> Pix conversion deleted.
*/ */
public class Tesseract2 extends Tesseract1 { @UtilityClass
public class Tesseract2 extends TessAPI1 {
private int createDocuments(Pix pix, String filename, ITessAPI.TessBaseAPI handle, ITessAPI.TessResultRenderer renderer) {
private int createDocuments(Pix pix, String filename, TessResultRenderer renderer) { String title = TessBaseAPIGetStringVariable(handle, DOCUMENT_TITLE);
String title = TessBaseAPIGetStringVariable(getHandle(), DOCUMENT_TITLE);
TessResultRendererBeginDocument(renderer, title); TessResultRendererBeginDocument(renderer, title);
int result = TessBaseAPIProcessPage(getHandle(), pix, 0, filename, null, 0, renderer); int result = TessBaseAPIProcessPage(handle, pix, 0, filename, null, 0, renderer);
TessResultRendererEndDocument(renderer); TessResultRendererEndDocument(renderer);
// if (result == ITessAPI.FALSE) { // if (result == ITessAPI.FALSE) {
// throw new TesseractException("Error during processing page."); // throw new TesseractException("Error during processing page.");
// } // }
return TessBaseAPIMeanTextConf(getHandle()); return TessBaseAPIMeanTextConf(handle);
} }
public OCRResult createDocumentsWithResults(Pix bi, String filename, String outputbase, List<RenderedFormat> formats, int pageIteratorLevel) throws TesseractException { public OCRResult createDocumentsWithResults(Pix bi,
String filename,
String outputbase,
List<ITesseract.RenderedFormat> formats,
int pageIteratorLevel,
ITessAPI.TessBaseAPI handle) {
List<OCRResult> results = createDocumentsWithResults(new Pix[]{bi}, new String[]{filename}, new String[]{outputbase}, formats, pageIteratorLevel); List<OCRResult> results = createDocumentsWithResults(new Pix[]{bi}, new String[]{filename}, new String[]{outputbase}, formats, pageIteratorLevel, handle);
if (!results.isEmpty()) { if (!results.isEmpty()) {
return results.get(0); return results.get(0);
} else { } else {
@ -48,24 +57,26 @@ public class Tesseract2 extends Tesseract1 {
} }
public List<OCRResult> createDocumentsWithResults(Pix[] pixs, String[] filenames, String[] outputbases, List<RenderedFormat> formats, int pageIteratorLevel) { public List<OCRResult> createDocumentsWithResults(Pix[] pixs,
String[] filenames,
String[] outputbases,
List<ITesseract.RenderedFormat> formats,
int pageIteratorLevel,
ITessAPI.TessBaseAPI handle) {
if (pixs.length != filenames.length || pixs.length != outputbases.length) { if (pixs.length != filenames.length || pixs.length != outputbases.length) {
throw new RuntimeException("The three arrays must match in length."); throw new RuntimeException("The three arrays must match in length.");
} }
init();
setVariables();
List<OCRResult> results = new ArrayList<OCRResult>(); List<OCRResult> results = new ArrayList<OCRResult>();
try { try {
for (int i = 0; i < pixs.length; i++) { for (int i = 0; i < pixs.length; i++) {
try { try {
TessResultRenderer renderer = createRenderers(outputbases[i], formats); ITessAPI.TessResultRenderer renderer = createRenderers(outputbases[i], formats);
int meanTextConfidence = createDocuments(pixs[i], filenames[i], renderer); int meanTextConfidence = createDocuments(pixs[i], filenames[i], handle, renderer);
TessDeleteResultRenderer(renderer); TessDeleteResultRenderer(renderer);
List<Word> words = meanTextConfidence > 0 ? getRecognizedWords(pageIteratorLevel) : new ArrayList<Word>(); List<Word> words = meanTextConfidence > 0 ? getRecognizedWords(pageIteratorLevel, handle) : new ArrayList<Word>();
results.add(new OCRResult(meanTextConfidence, words)); results.add(new OCRResult(meanTextConfidence, words));
} catch (Exception e) { } catch (Exception e) {
// skip the problematic image file // skip the problematic image file
@ -73,20 +84,22 @@ public class Tesseract2 extends Tesseract1 {
} }
} }
} finally { } finally {
dispose(); synchronized (OCRThread.class) {
TessAPI1.TessBaseAPIClear(handle);
}
} }
return results; return results;
} }
private List<Word> getRecognizedWords(int pageIteratorLevel) { private List<Word> getRecognizedWords(int pageIteratorLevel, ITessAPI.TessBaseAPI handle) {
List<Word> words = new ArrayList<>(); List<Word> words = new ArrayList<>();
try { try {
TessResultIterator ri = TessBaseAPIGetIterator(getHandle()); ITessAPI.TessResultIterator ri = TessBaseAPIGetIterator(handle);
TessPageIterator pi = TessResultIteratorGetPageIterator(ri); ITessAPI.TessPageIterator pi = TessResultIteratorGetPageIterator(ri);
TessPageIteratorBegin(pi); TessPageIteratorBegin(pi);
do { do {
@ -119,11 +132,11 @@ public class Tesseract2 extends Tesseract1 {
} }
private TessResultRenderer createRenderers(String outputbase, List<RenderedFormat> formats) { private ITessAPI.TessResultRenderer createRenderers(String outputbase, List<ITesseract.RenderedFormat> formats) {
TessResultRenderer renderer = null; ITessAPI.TessResultRenderer renderer = null;
for (RenderedFormat format : formats) { for (ITesseract.RenderedFormat format : formats) {
switch (format) { switch (format) {
case HOCR: case HOCR:

View File

@ -31,7 +31,7 @@ import io.micrometer.prometheus.PrometheusMeterRegistry;
import io.micrometer.prometheus.PrometheusTimer; import io.micrometer.prometheus.PrometheusTimer;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@Disabled // Ghostscript/Tesseract/Leptonica is not available on build server. If you want to run the test install these dependencies. See README.md for help. //@Disabled // Ghostscript/Tesseract/Leptonica is not available on build server. If you want to run the test install these dependencies. See README.md for help.
@SpringBootTest() @SpringBootTest()
public class OcrServiceIntegrationTest extends AbstractTest { public class OcrServiceIntegrationTest extends AbstractTest {
@ -64,7 +64,7 @@ public class OcrServiceIntegrationTest extends AbstractTest {
@SneakyThrows @SneakyThrows
public void testOcr() { public void testOcr() {
String text = testOCR("files/2009-1048395_50pages_tables.pdf"); String text = testOCR("files/VV-352892.pdf");
} }
@ -139,7 +139,7 @@ public class OcrServiceIntegrationTest extends AbstractTest {
String dir = "/home/kschuettler/Dokumente/TestFiles/syn-dm-testfiles/"; String dir = "/home/kschuettler/Dokumente/TestFiles/syn-dm-testfiles/";
List<File> foundFiles = Files.walk(Path.of(dir)) List<File> foundFiles = Files.walk(Path.of(dir))
.sorted(Comparator.comparingLong(this::getFileSize)) // .sorted(Comparator.comparingLong(this::getFileSize))
.map(Path::toFile) .map(Path::toFile)
.filter(file -> file.getName().endsWith(".pdf")) .filter(file -> file.getName().endsWith(".pdf"))
.peek(System.out::println) .peek(System.out::println)
@ -162,7 +162,7 @@ public class OcrServiceIntegrationTest extends AbstractTest {
@SneakyThrows @SneakyThrows
public void testOcrForSpecificFile() { public void testOcrForSpecificFile() {
testOCRForFile(new File("/home/kschuettler/Dokumente/TestFiles/syn-dm-testfiles/F.2. A16003E - Acute Inhalation Study.pdf")); testOCRForFile(new File("/home/kschuettler/Dokumente/TestFiles/syn-dm-testfiles/Item 17_Toxicidade Inalatoria.pdf"));
// testOCRForFile(new File("/home/kschuettler/Dokumente/TestFiles/syn-dm-testfiles/A23220A - 404 - Skin Irritation in vivo.pdf")); // testOCRForFile(new File("/home/kschuettler/Dokumente/TestFiles/syn-dm-testfiles/A23220A - 404 - Skin Irritation in vivo.pdf"));
// testOCRForFile(new File("/home/kschuettler/Dokumente/TestFiles/OcrMassTest/G.1.2 - 1768300_MMNA_A13617AV_report.pdf")); // testOCRForFile(new File("/home/kschuettler/Dokumente/TestFiles/OcrMassTest/G.1.2 - 1768300_MMNA_A13617AV_report.pdf"));
// testOCRForFile(new File("/home/kschuettler/Dokumente/TestFiles/OcrMassTest/SOLICITA_VICTRATO-GOLD-II_Item 17_Toxicidade Inalatoria Aguda.pdf")); // testOCRForFile(new File("/home/kschuettler/Dokumente/TestFiles/OcrMassTest/SOLICITA_VICTRATO-GOLD-II_Item 17_Toxicidade Inalatoria Aguda.pdf"));