Apparent memory leak with lib-lept (leptonica) and/or lib-tesseract
I have an MVP which produces a perfect memory leak (100% memory leakage) due to failure to release destroyed memory.
The test procedure is as follows: construct and destruct a Pix image from a numpy image 1000s of times in a python loop running in a docker container. Meanwhile, record the docker container's memory usage. This wound up being more effective than valgrind at proving the leak, since valgrinding python3 was time consuming.
As a control, I set up a minimal C++ test of libtesseract and liblept which I also ran inside of a docker container. That is posted here as well and I'll be double checking momentarily, since I observed leakage primarily in the usage of libtesseract.
MVP python3 memory leak with graph of usage pattern (Y-axis: Gb, X-axis: Seconds):
import gc
import click
import cv2
import numpy
from cffi import FFI
from PIL import Image as ImageModule
from PIL.Image import Image
DEFAULT_DPI = 300
def read_img(img_file: str):
img = cv2.imread(img_file)
# favoring numpy's memory management over opencv-python
img_arr = numpy.copy(img)
del img
return img_arr
def cffi_memtest_pix(img: numpy.ndarray, cycles: int = 1):
"""Memory test the image cffi transaction with a standalone cffi interface.
Run this for an arbitrary number of cycles in a docker container while monitoring the image's memory to confirm
leakage.
Args:
img: the image to test
cycles: the number of times to load and delete images
"""
hdr = """
typedef struct Pix Pix;
char * getLeptonicaVersion();
Pix * pixRead(const char* filename);
Pix * pixCreate(int cols, int rows, int channels);
Pix * pixSetData(Pix* pix, unsigned int * buffer);
int pixSetResolution(Pix * pix, int xres, int yres);
void pixDestroy(Pix ** pix);
Pix * pixEndianByteSwapNew(Pix * pix);
"""
global DEFAULT_DPI
ffi = FFI(backend=None)
ffi.cdef(hdr)
lept = ffi.dlopen('lept')
for i in range(cycles):
pil: Image = ImageModule.fromarray(img).convert("RGBA")
cols, rows = pil.size
pil_img_buf = pil.tobytes("raw", "RGBA")
pix = lept.pixCreate(cols, rows, 32) # the handle '_' indicates ptr, cannot be manipulated directly
lept.pixSetData(pix, ffi.from_buffer("unsigned int[]", pil_img_buf))
lept.pixSetResolution(pix, DEFAULT_DPI, DEFAULT_DPI)
pix = lept.pixEndianByteSwapNew(pix) # no-op on big-endian, fixes the problem on little-endian
_pix = ffi.new("Pix **")
_pix[0] = pix
lept.pixDestroy(_pix)
ffi.release(_pix)
del pix
del _pix
gc.collect()
@click.command()
@click.argument('img', type=str)
@click.argument('cycles', type=int)
def run(img: str, cycles: int):
img_ndarr = read_img(img)
cffi_memtest_pix(img_ndarr, cycles)
if __name__ == "__main__":
run()
This code is run in a different python3 housing that provides an outer loop and a loaded image as input to this function. I provided an entry point via click.
Control variable: C++
#include <string>
#include "tesseract/baseapi.h"
#include "leptonica/allheaders.h"
int main()
{
// args
auto file = std::string("<your-img>");
int N = 1000;
char *outText;
for (int i = 0; i < N; i++){
tesseract::TessBaseAPI *api = new tesseract::TessBaseAPI();
// Initialize tesseract-ocr with English, without specifying tessdata path
if (api->Init(NULL, "eng")) {
fprintf(stderr, "Could not initialize tesseract.\n");
exit(1);
}
// Open input image with leptonica library
Pix *image = pixRead(file.c_str());
api->SetImage(image);
// Get OCR result
outText = api->GetUTF8Text();
printf("Nth {N=%d} OCR output:\n%s", i, std::string(outText).substr(0,10).c_str());
delete [] outText;
pixDestroy(&image);
// Destroy used object and release memory
api->End();
delete api;
}
return 0;
}
The C++ hello-world does not leak, although (again) double checking.
Alternatively, perhaps I am doing something wrong. However, the Pix deletion protocol in particular is not my code. I found this usage pattern when looking into using lept
with cffi
from python
, so it was an aspect of this leakage that I was confident was correct (or at least typical).
cffi @ vers 1.15.0