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 ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ ImgBoost - Biblioteca CLI para melhorar qualidade de imagens
3
+ """
4
+
5
+ __version__ = '0.1.0'
6
+ __author__ = 'ImgBoost Team'
7
+
8
+ from .core import Engine
9
+
10
+ __all__ = ['Engine']
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,9 @@
1
+ """
2
+ Módulo de processadores de imagem.
3
+ """
4
+
5
+ from .filters import ImageFilters
6
+ from .document import DocumentProcessor
7
+ from .superres import SuperResolution
8
+
9
+ __all__ = ['ImageFilters', 'DocumentProcessor', 'SuperResolution']
@@ -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