imgboost-ai 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- imgboost/__init__.py +10 -0
- imgboost/cli.py +169 -0
- imgboost/core.py +162 -0
- imgboost/processors/__init__.py +9 -0
- imgboost/processors/document.py +269 -0
- imgboost/processors/filters.py +257 -0
- imgboost/processors/superres.py +252 -0
- imgboost/utils/__init__.py +7 -0
- imgboost/utils/image_io.py +231 -0
- imgboost_ai-0.1.0.dist-info/METADATA +387 -0
- imgboost_ai-0.1.0.dist-info/RECORD +15 -0
- imgboost_ai-0.1.0.dist-info/WHEEL +5 -0
- imgboost_ai-0.1.0.dist-info/entry_points.txt +2 -0
- imgboost_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
- imgboost_ai-0.1.0.dist-info/top_level.txt +1 -0
imgboost/__init__.py
ADDED
imgboost/cli.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from .core import Engine
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
@click.version_option(version='0.1.0')
|
|
9
|
+
def cli():
|
|
10
|
+
"""
|
|
11
|
+
🚀 ImgBoost - Melhore a qualidade de imagens via CLI
|
|
12
|
+
|
|
13
|
+
Modos disponíveis:
|
|
14
|
+
- general: Melhoria geral (denoise + contrast + sharpen)
|
|
15
|
+
- text: Otimizado para documentos e texto
|
|
16
|
+
- dark: Otimizado para interfaces escuras (WhatsApp, etc)
|
|
17
|
+
- superres: Super-resolução com IA (upscale 2x)
|
|
18
|
+
"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cli.command()
|
|
23
|
+
@click.argument('input_path', type=click.Path(exists=True))
|
|
24
|
+
@click.option('--output', '-o', default='output', help='Pasta de saída para imagens processadas.')
|
|
25
|
+
@click.option('--mode', '-m',
|
|
26
|
+
type=click.Choice(['general', 'text', 'dark', 'superres'], case_sensitive=False),
|
|
27
|
+
default='general',
|
|
28
|
+
help='Modo de processamento.')
|
|
29
|
+
@click.option('--upscale', '-u', is_flag=True, help='Aplicar Super-Resolução com IA (2x).')
|
|
30
|
+
@click.option('--quality', '-q', type=int, default=95, help='Qualidade JPEG de saída (1-100).')
|
|
31
|
+
@click.option('--format', '-f', type=click.Choice(['auto', 'png', 'jpg', 'jpeg']),
|
|
32
|
+
default='auto', help='Formato de saída.')
|
|
33
|
+
@click.option('--denoise-strength', '-d', type=int, default=10, help='Força do denoise (0-30).')
|
|
34
|
+
@click.option('--contrast', '-c', type=float, default=3.0, help='Intensidade do CLAHE (1.0-5.0).')
|
|
35
|
+
@click.option('--sharpen', '-s', is_flag=True, help='Aplicar sharpening extra.')
|
|
36
|
+
@click.option('--verbose', '-v', is_flag=True, help='Mostrar informações detalhadas.')
|
|
37
|
+
def process(input_path, output, mode, upscale, quality, format, denoise_strength,
|
|
38
|
+
contrast, sharpen, verbose):
|
|
39
|
+
"""
|
|
40
|
+
Processar uma imagem ou diretório inteiro.
|
|
41
|
+
|
|
42
|
+
Exemplos:
|
|
43
|
+
|
|
44
|
+
imgboost process foto.jpg --mode text
|
|
45
|
+
|
|
46
|
+
imgboost process ./fotos --mode dark --upscale
|
|
47
|
+
|
|
48
|
+
imgboost process documento.png -m text -o melhorado
|
|
49
|
+
"""
|
|
50
|
+
# Criar engine com configurações
|
|
51
|
+
engine = Engine(
|
|
52
|
+
output_dir=output,
|
|
53
|
+
quality=quality,
|
|
54
|
+
output_format=format,
|
|
55
|
+
denoise_strength=denoise_strength,
|
|
56
|
+
contrast_clip=contrast,
|
|
57
|
+
extra_sharpen=sharpen,
|
|
58
|
+
verbose=verbose
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Verificar se é diretório ou arquivo único
|
|
62
|
+
input_path = Path(input_path)
|
|
63
|
+
|
|
64
|
+
if input_path.is_dir():
|
|
65
|
+
# Processar todos os arquivos de imagem no diretório
|
|
66
|
+
image_extensions = {'.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.webp'}
|
|
67
|
+
files = [f for f in input_path.iterdir()
|
|
68
|
+
if f.suffix.lower() in image_extensions]
|
|
69
|
+
|
|
70
|
+
if not files:
|
|
71
|
+
click.echo("❌ Nenhuma imagem encontrada no diretório.", err=True)
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
click.echo(f"📂 Encontradas {len(files)} imagens para processar\n")
|
|
75
|
+
|
|
76
|
+
with click.progressbar(files,
|
|
77
|
+
label='🔧 Processando imagens',
|
|
78
|
+
show_eta=True) as bar:
|
|
79
|
+
for file in bar:
|
|
80
|
+
try:
|
|
81
|
+
engine.run(str(file), mode, upscale)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
if verbose:
|
|
84
|
+
click.echo(f"\n❌ Erro em {file.name}: {e}", err=True)
|
|
85
|
+
|
|
86
|
+
click.echo(f"\n✅ Processamento concluído! Imagens salvas em: {output}/")
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
# Processar arquivo único
|
|
90
|
+
if verbose:
|
|
91
|
+
click.echo(f"🚀 Processando: {input_path.name}")
|
|
92
|
+
click.echo(f"📋 Modo: {mode}")
|
|
93
|
+
click.echo(f"🎯 Super-resolução: {'Sim' if upscale else 'Não'}\n")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
output_path = engine.run(str(input_path), mode, upscale)
|
|
97
|
+
click.echo(f"✅ Imagem processada com sucesso!")
|
|
98
|
+
click.echo(f"💾 Salva em: {output_path}")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
click.echo(f"❌ Erro ao processar: {e}", err=True)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@cli.command()
|
|
104
|
+
@click.argument('input_path', type=click.Path(exists=True))
|
|
105
|
+
@click.option('--output', '-o', default='output_batch', help='Pasta de saída.')
|
|
106
|
+
@click.option('--workers', '-w', type=int, default=4, help='Número de processos paralelos.')
|
|
107
|
+
def batch(input_path, output, workers):
|
|
108
|
+
"""
|
|
109
|
+
Processar múltiplas imagens em paralelo (mais rápido).
|
|
110
|
+
|
|
111
|
+
Exemplo:
|
|
112
|
+
imgboost batch ./fotos --workers 8
|
|
113
|
+
"""
|
|
114
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
115
|
+
from functools import partial
|
|
116
|
+
|
|
117
|
+
input_path = Path(input_path)
|
|
118
|
+
|
|
119
|
+
if not input_path.is_dir():
|
|
120
|
+
click.echo("❌ O caminho deve ser um diretório.", err=True)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
image_extensions = {'.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.webp'}
|
|
124
|
+
files = [str(f) for f in input_path.iterdir()
|
|
125
|
+
if f.suffix.lower() in image_extensions]
|
|
126
|
+
|
|
127
|
+
if not files:
|
|
128
|
+
click.echo("❌ Nenhuma imagem encontrada.", err=True)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
click.echo(f"📂 {len(files)} imagens encontradas")
|
|
132
|
+
click.echo(f"⚙️ Usando {workers} workers\n")
|
|
133
|
+
|
|
134
|
+
engine = Engine(output_dir=output)
|
|
135
|
+
process_func = partial(engine.run, mode='general', upscale=False)
|
|
136
|
+
|
|
137
|
+
with ProcessPoolExecutor(max_workers=workers) as executor:
|
|
138
|
+
results = list(tqdm(
|
|
139
|
+
executor.map(process_func, files),
|
|
140
|
+
total=len(files),
|
|
141
|
+
desc="Processando"
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
click.echo(f"\n✅ Processamento em lote concluído!")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@cli.command()
|
|
148
|
+
def info():
|
|
149
|
+
"""Mostrar informações sobre a biblioteca e dependências."""
|
|
150
|
+
import cv2
|
|
151
|
+
import numpy as np
|
|
152
|
+
import torch
|
|
153
|
+
|
|
154
|
+
click.echo("📦 ImgBoost v0.1.0")
|
|
155
|
+
click.echo("\n🔧 Dependências:")
|
|
156
|
+
click.echo(f" • OpenCV: {cv2.__version__}")
|
|
157
|
+
click.echo(f" • NumPy: {np.__version__}")
|
|
158
|
+
click.echo(f" • PyTorch: {torch.__version__}")
|
|
159
|
+
click.echo(f" • CUDA disponível: {'Sim ✅' if torch.cuda.is_available() else 'Não ❌'}")
|
|
160
|
+
click.echo("\n📚 Modos disponíveis:")
|
|
161
|
+
click.echo(" • general - Melhoria geral de qualidade")
|
|
162
|
+
click.echo(" • text - Otimizado para documentos e OCR")
|
|
163
|
+
click.echo(" • dark - Otimizado para UIs escuras")
|
|
164
|
+
click.echo(" • superres - Super-resolução com IA")
|
|
165
|
+
click.echo("\n💡 Use 'imgboost process --help' para mais informações")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == '__main__':
|
|
169
|
+
cli()
|
imgboost/core.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import cv2
|
|
4
|
+
from .processors.filters import ImageFilters
|
|
5
|
+
from .processors.document import DocumentProcessor
|
|
6
|
+
from .processors.superres import SuperResolution
|
|
7
|
+
from .utils.image_io import ImageIO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Engine:
|
|
11
|
+
"""
|
|
12
|
+
Orquestrador principal do pipeline de processamento de imagens.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, output_dir='output', quality=95, output_format='auto',
|
|
16
|
+
denoise_strength=10, contrast_clip=3.0, extra_sharpen=False,
|
|
17
|
+
verbose=False):
|
|
18
|
+
"""
|
|
19
|
+
Inicializa o engine de processamento.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
output_dir: Diretório de saída
|
|
23
|
+
quality: Qualidade JPEG (1-100)
|
|
24
|
+
output_format: Formato de saída ('auto', 'png', 'jpg')
|
|
25
|
+
denoise_strength: Força do denoise (0-30)
|
|
26
|
+
contrast_clip: Intensidade do CLAHE (1.0-5.0)
|
|
27
|
+
extra_sharpen: Aplicar sharpening adicional
|
|
28
|
+
verbose: Modo verboso
|
|
29
|
+
"""
|
|
30
|
+
self.output_dir = Path(output_dir)
|
|
31
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
self.quality = quality
|
|
34
|
+
self.output_format = output_format
|
|
35
|
+
self.denoise_strength = denoise_strength
|
|
36
|
+
self.contrast_clip = contrast_clip
|
|
37
|
+
self.extra_sharpen = extra_sharpen
|
|
38
|
+
self.verbose = verbose
|
|
39
|
+
|
|
40
|
+
# Inicializar processadores
|
|
41
|
+
self.filters = ImageFilters(
|
|
42
|
+
denoise_strength=denoise_strength,
|
|
43
|
+
contrast_clip=contrast_clip
|
|
44
|
+
)
|
|
45
|
+
self.doc_processor = DocumentProcessor()
|
|
46
|
+
self.superres = SuperResolution()
|
|
47
|
+
self.io = ImageIO()
|
|
48
|
+
|
|
49
|
+
def run(self, input_path, mode='general', upscale=False):
|
|
50
|
+
"""
|
|
51
|
+
Executa o pipeline de processamento.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
input_path: Caminho da imagem de entrada
|
|
55
|
+
mode: Modo de processamento ('general', 'text', 'dark', 'superres')
|
|
56
|
+
upscale: Aplicar super-resolução
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Caminho da imagem de saída
|
|
60
|
+
"""
|
|
61
|
+
if self.verbose:
|
|
62
|
+
print(f"[DEBUG] Carregando: {input_path}")
|
|
63
|
+
|
|
64
|
+
# Carregar imagem
|
|
65
|
+
img = self.io.load(input_path)
|
|
66
|
+
|
|
67
|
+
if img is None:
|
|
68
|
+
raise ValueError(f"Não foi possível carregar a imagem: {input_path}")
|
|
69
|
+
|
|
70
|
+
# Aplicar processamento baseado no modo
|
|
71
|
+
if mode == 'general':
|
|
72
|
+
img = self._process_general(img)
|
|
73
|
+
elif mode == 'text':
|
|
74
|
+
img = self._process_text(img)
|
|
75
|
+
elif mode == 'dark':
|
|
76
|
+
img = self._process_dark(img)
|
|
77
|
+
elif mode == 'superres':
|
|
78
|
+
img = self._process_superres(img)
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError(f"Modo desconhecido: {mode}")
|
|
81
|
+
|
|
82
|
+
# Aplicar super-resolução se solicitado
|
|
83
|
+
if upscale and mode != 'superres':
|
|
84
|
+
if self.verbose:
|
|
85
|
+
print("[DEBUG] Aplicando super-resolução...")
|
|
86
|
+
img = self.superres.upscale(img)
|
|
87
|
+
|
|
88
|
+
# Sharpening extra se solicitado
|
|
89
|
+
if self.extra_sharpen:
|
|
90
|
+
if self.verbose:
|
|
91
|
+
print("[DEBUG] Aplicando sharpen extra...")
|
|
92
|
+
img = self.filters.sharpen(img, strength=1.5)
|
|
93
|
+
|
|
94
|
+
# Salvar imagem
|
|
95
|
+
output_path = self._get_output_path(input_path, mode, upscale)
|
|
96
|
+
|
|
97
|
+
if self.verbose:
|
|
98
|
+
print(f"[DEBUG] Salvando em: {output_path}")
|
|
99
|
+
|
|
100
|
+
self.io.save(img, str(output_path), quality=self.quality)
|
|
101
|
+
|
|
102
|
+
return output_path
|
|
103
|
+
|
|
104
|
+
def _process_general(self, img):
|
|
105
|
+
"""Pipeline de melhoria geral."""
|
|
106
|
+
img = self.filters.denoise(img)
|
|
107
|
+
img = self.filters.enhance_contrast(img)
|
|
108
|
+
img = self.filters.sharpen(img)
|
|
109
|
+
return img
|
|
110
|
+
|
|
111
|
+
def _process_text(self, img):
|
|
112
|
+
"""Pipeline otimizado para texto e documentos."""
|
|
113
|
+
# Melhorias básicas
|
|
114
|
+
img = self.filters.denoise(img)
|
|
115
|
+
img = self.filters.enhance_contrast(img)
|
|
116
|
+
|
|
117
|
+
# Processamento específico para texto
|
|
118
|
+
img = self.doc_processor.enhance_text(img)
|
|
119
|
+
|
|
120
|
+
return img
|
|
121
|
+
|
|
122
|
+
def _process_dark(self, img):
|
|
123
|
+
"""Pipeline otimizado para interfaces escuras."""
|
|
124
|
+
# Processamento específico para UIs escuras
|
|
125
|
+
img = self.filters.denoise(img, strength=self.denoise_strength * 0.7)
|
|
126
|
+
img = self.filters.enhance_dark_ui(img)
|
|
127
|
+
img = self.filters.sharpen(img, strength=0.8)
|
|
128
|
+
|
|
129
|
+
return img
|
|
130
|
+
|
|
131
|
+
def _process_superres(self, img):
|
|
132
|
+
"""Pipeline com foco em super-resolução."""
|
|
133
|
+
# Limpeza básica antes do upscale
|
|
134
|
+
img = self.filters.denoise(img, strength=self.denoise_strength * 1.2)
|
|
135
|
+
|
|
136
|
+
# Super-resolução
|
|
137
|
+
img = self.superres.upscale(img)
|
|
138
|
+
|
|
139
|
+
# Refinamento pós-upscale
|
|
140
|
+
img = self.filters.enhance_contrast(img)
|
|
141
|
+
img = self.filters.sharpen(img, strength=0.5)
|
|
142
|
+
|
|
143
|
+
return img
|
|
144
|
+
|
|
145
|
+
def _get_output_path(self, input_path, mode, upscale):
|
|
146
|
+
"""Gera o caminho de saída baseado nas configurações."""
|
|
147
|
+
input_path = Path(input_path)
|
|
148
|
+
|
|
149
|
+
# Determinar extensão de saída
|
|
150
|
+
if self.output_format == 'auto':
|
|
151
|
+
ext = input_path.suffix
|
|
152
|
+
else:
|
|
153
|
+
ext = f'.{self.output_format}'
|
|
154
|
+
|
|
155
|
+
# Nome do arquivo com sufixos
|
|
156
|
+
suffix = f"_{mode}"
|
|
157
|
+
if upscale:
|
|
158
|
+
suffix += "_2x"
|
|
159
|
+
|
|
160
|
+
output_name = f"{input_path.stem}{suffix}{ext}"
|
|
161
|
+
|
|
162
|
+
return self.output_dir / output_name
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DocumentProcessor:
|
|
6
|
+
"""
|
|
7
|
+
Processador especializado para documentos, textos e OCR.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def enhance_text(self, img):
|
|
14
|
+
"""
|
|
15
|
+
Melhora imagem para leitura de texto.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
img: Imagem BGR
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Imagem otimizada para texto
|
|
22
|
+
"""
|
|
23
|
+
# Se já for grayscale, usar direto
|
|
24
|
+
if len(img.shape) == 2:
|
|
25
|
+
gray = img
|
|
26
|
+
else:
|
|
27
|
+
# Converter para grayscale
|
|
28
|
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
29
|
+
|
|
30
|
+
# Denoise
|
|
31
|
+
gray = cv2.fastNlMeansDenoising(gray, None, h=10, templateWindowSize=7, searchWindowSize=21)
|
|
32
|
+
|
|
33
|
+
# Melhorar contraste com CLAHE
|
|
34
|
+
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
|
35
|
+
gray = clahe.apply(gray)
|
|
36
|
+
|
|
37
|
+
# Retornar como BGR para consistência
|
|
38
|
+
return cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
|
|
39
|
+
|
|
40
|
+
def binarize(self, img, method='adaptive'):
|
|
41
|
+
"""
|
|
42
|
+
Binariza a imagem para OCR.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
img: Imagem BGR
|
|
46
|
+
method: Método de binarização ('adaptive', 'otsu', 'simple')
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Imagem binarizada
|
|
50
|
+
"""
|
|
51
|
+
# Converter para grayscale se necessário
|
|
52
|
+
if len(img.shape) == 3:
|
|
53
|
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
54
|
+
else:
|
|
55
|
+
gray = img
|
|
56
|
+
|
|
57
|
+
# Denoise primeiro
|
|
58
|
+
gray = cv2.fastNlMeansDenoising(gray, None, h=10)
|
|
59
|
+
|
|
60
|
+
if method == 'adaptive':
|
|
61
|
+
# Binarização adaptativa - melhor para iluminação não-uniforme
|
|
62
|
+
binary = cv2.adaptiveThreshold(
|
|
63
|
+
gray, 255,
|
|
64
|
+
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
|
65
|
+
cv2.THRESH_BINARY,
|
|
66
|
+
11, 2
|
|
67
|
+
)
|
|
68
|
+
elif method == 'otsu':
|
|
69
|
+
# Método de Otsu - automático
|
|
70
|
+
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
|
71
|
+
else:
|
|
72
|
+
# Threshold simples
|
|
73
|
+
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
|
|
74
|
+
|
|
75
|
+
return binary
|
|
76
|
+
|
|
77
|
+
def deskew(self, img):
|
|
78
|
+
"""
|
|
79
|
+
Corrige inclinação do documento.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
img: Imagem BGR
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Imagem com inclinação corrigida
|
|
86
|
+
"""
|
|
87
|
+
# Converter para grayscale
|
|
88
|
+
if len(img.shape) == 3:
|
|
89
|
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
90
|
+
else:
|
|
91
|
+
gray = img
|
|
92
|
+
|
|
93
|
+
# Binarizar
|
|
94
|
+
binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
|
|
95
|
+
|
|
96
|
+
# Detectar ângulo de rotação
|
|
97
|
+
coords = np.column_stack(np.where(binary > 0))
|
|
98
|
+
angle = cv2.minAreaRect(coords)[-1]
|
|
99
|
+
|
|
100
|
+
# Ajustar ângulo
|
|
101
|
+
if angle < -45:
|
|
102
|
+
angle = -(90 + angle)
|
|
103
|
+
else:
|
|
104
|
+
angle = -angle
|
|
105
|
+
|
|
106
|
+
# Rotacionar imagem
|
|
107
|
+
(h, w) = img.shape[:2]
|
|
108
|
+
center = (w // 2, h // 2)
|
|
109
|
+
M = cv2.getRotationMatrix2D(center, angle, 1.0)
|
|
110
|
+
|
|
111
|
+
if len(img.shape) == 3:
|
|
112
|
+
rotated = cv2.warpAffine(
|
|
113
|
+
img, M, (w, h),
|
|
114
|
+
flags=cv2.INTER_CUBIC,
|
|
115
|
+
borderMode=cv2.BORDER_REPLICATE
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
rotated = cv2.warpAffine(
|
|
119
|
+
img, M, (w, h),
|
|
120
|
+
flags=cv2.INTER_CUBIC,
|
|
121
|
+
borderMode=cv2.BORDER_REPLICATE
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return rotated
|
|
125
|
+
|
|
126
|
+
def remove_noise(self, img):
|
|
127
|
+
"""
|
|
128
|
+
Remove ruído usando morfologia.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
img: Imagem binária
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Imagem limpa
|
|
135
|
+
"""
|
|
136
|
+
# Kernel para operações morfológicas
|
|
137
|
+
kernel = np.ones((2, 2), np.uint8)
|
|
138
|
+
|
|
139
|
+
# Opening: remove ruído pequeno
|
|
140
|
+
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel, iterations=1)
|
|
141
|
+
|
|
142
|
+
# Closing: fecha buracos pequenos
|
|
143
|
+
closing = cv2.morphologyEx(opening, cv2.MORPH_CLOSE, kernel, iterations=1)
|
|
144
|
+
|
|
145
|
+
return closing
|
|
146
|
+
|
|
147
|
+
def enhance_for_ocr(self, img):
|
|
148
|
+
"""
|
|
149
|
+
Pipeline completo de otimização para OCR.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
img: Imagem BGR
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Imagem otimizada para OCR
|
|
156
|
+
"""
|
|
157
|
+
# Converter para grayscale
|
|
158
|
+
if len(img.shape) == 3:
|
|
159
|
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
160
|
+
else:
|
|
161
|
+
gray = img
|
|
162
|
+
|
|
163
|
+
# 1. Denoise
|
|
164
|
+
gray = cv2.fastNlMeansDenoising(gray, None, h=10)
|
|
165
|
+
|
|
166
|
+
# 2. Aumentar contraste
|
|
167
|
+
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
|
168
|
+
gray = clahe.apply(gray)
|
|
169
|
+
|
|
170
|
+
# 3. Binarizar
|
|
171
|
+
binary = cv2.adaptiveThreshold(
|
|
172
|
+
gray, 255,
|
|
173
|
+
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
|
174
|
+
cv2.THRESH_BINARY,
|
|
175
|
+
11, 2
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# 4. Limpar ruído
|
|
179
|
+
binary = self.remove_noise(binary)
|
|
180
|
+
|
|
181
|
+
# 5. Leve sharpen
|
|
182
|
+
kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
|
|
183
|
+
sharpened = cv2.filter2D(binary, -1, kernel)
|
|
184
|
+
|
|
185
|
+
return sharpened
|
|
186
|
+
|
|
187
|
+
def remove_shadows(self, img):
|
|
188
|
+
"""
|
|
189
|
+
Remove sombras de documentos fotografados.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
img: Imagem BGR
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Imagem sem sombras
|
|
196
|
+
"""
|
|
197
|
+
# Converter para LAB
|
|
198
|
+
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
|
|
199
|
+
l, a, b = cv2.split(lab)
|
|
200
|
+
|
|
201
|
+
# Aplicar blur para estimar iluminação
|
|
202
|
+
blur = cv2.GaussianBlur(l, (0, 0), sigmaX=3, sigmaY=3)
|
|
203
|
+
|
|
204
|
+
# Normalizar
|
|
205
|
+
l = cv2.divide(l, blur, scale=255)
|
|
206
|
+
|
|
207
|
+
# Recombinar
|
|
208
|
+
lab = cv2.merge([l, a, b])
|
|
209
|
+
img = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
|
|
210
|
+
|
|
211
|
+
return img
|
|
212
|
+
|
|
213
|
+
def increase_dpi(self, img, target_dpi=300, current_dpi=72):
|
|
214
|
+
"""
|
|
215
|
+
Aumenta DPI da imagem para impressão/OCR.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
img: Imagem BGR
|
|
219
|
+
target_dpi: DPI desejado
|
|
220
|
+
current_dpi: DPI atual
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Imagem com DPI aumentado
|
|
224
|
+
"""
|
|
225
|
+
scale = target_dpi / current_dpi
|
|
226
|
+
|
|
227
|
+
width = int(img.shape[1] * scale)
|
|
228
|
+
height = int(img.shape[0] * scale)
|
|
229
|
+
|
|
230
|
+
# Usar INTER_CUBIC ou INTER_LANCZOS4 para upscaling
|
|
231
|
+
resized = cv2.resize(img, (width, height), interpolation=cv2.INTER_LANCZOS4)
|
|
232
|
+
|
|
233
|
+
return resized
|
|
234
|
+
|
|
235
|
+
def detect_text_regions(self, img):
|
|
236
|
+
"""
|
|
237
|
+
Detecta regiões de texto na imagem.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
img: Imagem BGR
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Lista de bounding boxes (x, y, w, h)
|
|
244
|
+
"""
|
|
245
|
+
# Converter para grayscale
|
|
246
|
+
if len(img.shape) == 3:
|
|
247
|
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
248
|
+
else:
|
|
249
|
+
gray = img
|
|
250
|
+
|
|
251
|
+
# Binarizar
|
|
252
|
+
binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
|
|
253
|
+
|
|
254
|
+
# Operações morfológicas para conectar texto
|
|
255
|
+
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 5))
|
|
256
|
+
dilated = cv2.dilate(binary, kernel, iterations=2)
|
|
257
|
+
|
|
258
|
+
# Encontrar contornos
|
|
259
|
+
contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
260
|
+
|
|
261
|
+
# Extrair bounding boxes
|
|
262
|
+
boxes = []
|
|
263
|
+
for contour in contours:
|
|
264
|
+
x, y, w, h = cv2.boundingRect(contour)
|
|
265
|
+
# Filtrar regiões muito pequenas
|
|
266
|
+
if w > 50 and h > 20:
|
|
267
|
+
boxes.append((x, y, w, h))
|
|
268
|
+
|
|
269
|
+
return boxes
|