O processamento de imagens binárias para a extração de expressões matemáticas envolve desafios significativos, especialmente quando os dados contêm ruído aleatório e caracteres rotacionados. O objetivo é converter uma matriz 01 em uma expressão computável, como (3 * 3) - 4, lidando com operadoers e parênteses.
1. Filtragem de Ruído e Pré-processamento
Ruídos isolados em matrizes de pixels geralmente se manifestam como pequenos grupos de pixels desconectados da estrutura principal dos caracteres. Uma técnica eficiente para eliminá-los é o algoritmo de preenchimento por inundação (flood-fill) para identificar componentes conectados. Se um componente possuir uma área (contagem de pixels) inferior a um limite (por exemplo, 100 pixels), ele é classificado como ruído e removido.
void processarComponentes(int altura, int largura) {
contagemGlobal = 1;
memset(visitado, false, sizeof(visitado));
for (int i = 0; i < altura; ++i) {
for (int j = 0; j < largura; ++j) {
if (gradeOriginal[i][j] == '#' && !visitado[i][j]) {
tempPixels.clear();
AreaComponente info = {0, 0, 10000, 10000, 0};
executarDFS(i, j, info);
if (info.totalPixels >= 100) {
listaCaracteres[contagemGlobal++] = info;
} else {
for (auto& p : tempPixels) {
gradeOriginal[p.first][p.second] = '.';
}
}
}
}
}
}
2. Segmentação de Símbolos
Após a limpeza, cada componente conectado isolado representa um potencial caractere. Para segmentá-los, calculamos o bounding box (caixa delimitadora) de cada componente, identificando as coordenadas extremas (mínimo e máximo para X e Y). Isso permite isolar a região de interesse (ROI) de cada símbolo para análise individual.
void analisarLimites(int x, int y, AreaComponente &info) {
visitado[x][y] = true;
tempPixels.push_back({x, y});
info.totalPixels++;
info.maxX = max(info.maxX, x);
info.minX = min(info.minX, x);
info.maxY = max(info.maxY, y);
info.minY = min(info.minY, y);
for (int d = 0; d < 8; ++d) {
int nx = x + dx[d], ny = y + dy[d];
if (posicaoValida(nx, ny) && gradeOriginal[nx][ny] == '#' && !visitado[nx][ny]) {
analisarLimites(nx, ny, info);
}
}
}
3. Extração de Características e Classificação
Para reconhecer o caractere dentro de cada caixa delimitadora, utilizamos uma matriz de densidade de pixels. Este método é resiliente a variações de escala, embora sensível a rotações acentuadas.
3.1 Matriz de Densidade (Feature Matrix)
Dividimos a caixa delimitadora em uma grade de N x N sub-regiões (ex: 5x5). Para cada célula da grade, calculamos a proporção de pixels ativos ('#') em relação ao total de pixels daquela sub-região. O resultado é um vetor de características que descreve a forma do símbolo.
void extrairVetorCaracteristicas(int id, int divisao) {
double stepX = (double)(info[id].maxX - info[id].minX + 1) / divisao;
double stepY = (double)(info[id].maxY - info[id].minY + 1) / divisao;
for (int i = 0; i < divisao; ++i) {
for (int j = 0; j < divisao; ++j) {
int pixelsAtivos = 0, totalCelulas = 0;
int startX = info[id].minX + i * stepX;
int endX = info[id].minX + (i + 1) * stepX;
int startY = info[id].minY + j * stepY;
int endY = info[id].minY + (j + 1) * stepY;
for (int r = startX; r < endX; ++r) {
for (int c = startY; c < endY; ++c) {
totalCelulas++;
if (gradeOriginal[r][c] == '#') pixelsAtivos++;
}
}
mapaCaracteristicas[id][i][j] = totalCelulas > 0 ? (double)pixelsAtivos / totalCelulas : 0;
}
}
}
3.2 Cálculo de Similaridade de Cosseno
Para classificar o símbolo, comparamos seu vetor de características com vetores de modelos (templates) pré-carregados. A similaridade de cosseno é frequentemente superior à distância euclidiana neste contexto, pois foca na orientação do vetor de características.
double calcularSimilaridade(int idCaractere, int idModelo, int n) {
double produtoEscalar = 0, normaA = 0, normaB = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
double vA = mapaCaracteristicas[idCaractere][i][j];
double vB = modelos[idModelo][i][j];
produtoEscalar += vA * vB;
normaA += vA * vA;
normaB += vB * vB;
}
}
return produtoEscalar / (sqrt(normaA) * sqrt(normaB));
}
4. Refinamento de Operadores
Símbolos simples como +, -, * e / podem ser confundidos com partes de números em baixas resoluções. Uma estratégia eficaz é realizar uma segunda passagem de reconhecimento com uma grade mais fina (ex: 7x7 ou 10x10). Se o segundo reconhecimento apontar para um operador com alta confiança, priorizamos esse resultado sobre o reconhecimento inicial de dígitos.
Após a identificação de todos os símbolos e sua ordenação horizontal (baseada no minY de cada caixa), a sequência de caracteres resultante pode ser enviada para um parser de expressões matemáticas (como o algoritmo shunting-yard) para obter o valor numérico final.