You can try the Tan-Triggs algorithm. In OpenCV, I met its use to reduce the variability of light in face recognition, but in my task it, in my opinion, can help.

The characters are relatively clean, the areas of their styles are practically intact, which significantly reduces the complexity of further processing, which in the same OpenCV will be reduced to fairly simple operations.
The algorithm itself is quite fast and does not require any training:
Mat tan_triggs_preprocessing(InputArray src , float alpha = 0.1, float tau = 10.0, float gamma = 0.2 , int sigma0 = 1, int sigma1 = 2) { Mat X = src.getMat(); X.convertTo(X, CV_32FC1); Mat I; pow(X, gamma, I); // Calculate the DOG Image: { Mat gaussian0, gaussian1; // Kernel Size: int kernel_sz0 = (3*sigma0); int kernel_sz1 = (3*sigma1); // Make them odd for OpenCV: kernel_sz0 += ((kernel_sz0 % 2) == 0) ? 1 : 0; kernel_sz1 += ((kernel_sz1 % 2) == 0) ? 1 : 0; GaussianBlur(I, gaussian0, Size(kernel_sz0,kernel_sz0) , sigma0, sigma0, BORDER_CONSTANT); GaussianBlur(I, gaussian1, Size(kernel_sz1,kernel_sz1) , sigma1, sigma1, BORDER_CONSTANT); subtract(gaussian0, gaussian1, I); } { double meanI = 0.0; { Mat tmp; pow(abs(I), alpha, tmp); meanI = mean(tmp).val[0]; } I = I / pow(meanI, 1.0/alpha); } { double meanI = 0.0; { Mat tmp; pow(min(abs(I), tau), alpha, tmp); meanI = mean(tmp).val[0]; } I = I / pow(meanI, 1.0/alpha); } // Squash into the tanh: { for(int r = 0; r < I.rows; r++) { for(int c = 0; c < I.cols; c++) { I.at<float>(r,c) = tanh(I.at<float>(r,c) / tau); } } I = tau * I; } return I; }
It’s easy to use:
Mat src_mat = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE); Mat dst_mat = tan_triggs_preprocessing(src_mat); normalize(dst_mat, dst_mat, 0, 255, NORM_MINMAX, CV_8UC1); Mat res_mat = src_mat - dst_mat; imshow("TanTriggs Image", res_mat);