Можно попробовать применить алгоритм Тана-Триггса (Tan-Triggs). В OpenCV я встречал его использование для снижения вариативности освещённости при распознавании лиц, но и в обозначенной задаче он, на мой взгляд, может помочь.

Символы получаются относительно чистыми, области их начертания практически целыми, что значительно снижает сложность дальнейшей обработки, которая в том же OpenCV сведётся к достаточно простым операциям.
Сам алгоритм довольно быстрый и не требует какого-либо обучения:
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;
}
Использовать просто:
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);