Referência dos módulos e contratos do pacote astroframe. Para utilização
prática, ver USO.md.
Convenções globais
- Imagens: numpy
np.ndarrayBGR (convenção OpenCV),uint8,(H, W, 3). Entradas em escala de cinza(H, W)e RGBA(H, W, 4)(interpretado RGBA) são normalizadas para BGR automaticamente. - Configuração:
AstroFrameConfig; qualquer função de processamento aceita-o opcionalmente (usa os valores por omissão se omitido).
Dataclasses com os parâmetros (ver USO.md para a tabela campo a campo):
CLAHEConfig,DenoiseConfig,UnsharpConfigStabilizerConfig,PolishConfig,FeedbackConfig,LuckyConfig,StackingConfigTuningConfig,AIConfig— auto-tuning e redes neuronais pequenas (v0.7.0)AstroFrameConfig— raiz com um campo por subconfiguração.
Métodos de AstroFrameConfig:
cfg.to_dict() -> dict
cfg.to_yaml(path) -> None
cfg.from_yaml(path) -> AstroFrameConfig # classmethodfrom_yaml valida os tipos e avisa (logging) sobre chaves desconhecidas e
tipos inesperados, sem falhar.
@dataclass(frozen=True)
class DiskDetection:
cx: int
cy: int # centro detetado nas coordenadas da imagem de origem
radius: int # raio ajustado após recorte/re-escala
find_all_disks(image, config=None) -> list[DiskDetection]
find_disk_center(image, config=None) -> DiskDetection | None
center_and_stabilize(image, config=None) -> tuple[np.ndarray, DiskDetection | None]
class AntiJitterStabilizer(config=None, alpha=None): ...find_all_disks— dois passes deHoughCircles(o segundo comminDist1/4 do normal, para círculos interiores ao astro maior) + fallback por contornos; até 5 discos, ordenados por raio decrescente. Dedup apenas de círculos do mesmo bordo (centros próximos E raios quase-iguais, tolerância 12% do raio), e rejeição de círculos-ghost: um candidato quase totalmente dentro de um disco já aceite (≥90% da área) com contraste fraco face ao anel à sua volta é descartado (_is_occluded_artifact). Aceita BGR ou escala de cinza. Comai.disk_filter > 0.0os candidatos são pontuados pelo classificador CNN (ai.cnn.DiskFilter) e os que ficam abaixo do limiar de confiança são descartados — a lista nunca é esvaziada.find_disk_center— primeiro elemento defind_all_disks; HoughCircles + fallback por contornos + refinamento por centroide de intensidade. Em frames ≥1200 px a deteção corre em meia-resolução.center_and_stabilize— translada o frame para centrar o disco e recorta as bordas pretas (stabilizer.auto_crop), devolvendo o raio ajustado. Sem disco detetado devolve a imagem inalterada eNone.AntiJitterStabilizer.stabilize(frame) -> (frame, DiskDetection | None)— estado interno: EMA do centroide (jitter_alpha) e reutilização do último deslocamento válido em frames sem deteção (last_detection— propriedade com o último disco detetado, usada pelo vídeo para polimento/preview). Comai.lstm_trajectoryo próximo centroide é previsto (extrapolação linear- refinamento LSTM opcional,
ai.lstm.TrajectoryPredictor) em vez de ficar congelado nos frames sem deteção.
- refinamento LSTM opcional,
polish_image(image, detection, config=None) -> np.ndarray- Polimento por astros: deteta todos os discos (
find_all_disks), separa discos secundários (centro dentro do astro maior; ex.: a Lua diante do Sol) de reflexos da lente (centro fora), realça cada astro individualmente (_astro_boost: esticamento local de contraste +polish.brightness; silhuetas escuras e uniformes — como um disco secundário concêntrico (ex.: a Lua diante do Sol) — são preservadas intactas) e remonta sem costuras por blend de máscaras com feather (_band_mask+_astro_region): a linha de recortecorona_scale × raiodilui o anel até ao fundo e as sobreposições entre astros são a média suave dos realces. O fundo é a média do fundo original (background_fill) ou preto puro (black_background); reflexos (raio ≥reflection_min_radiuspx) são preenchidos com o fundo seremove_reflections. Sem deteção devolve a imagem inalterada.
clahe_enhance(image, config) -> np.ndarray
denoise(image, config) -> np.ndarray
unsharp_mask(image, config) -> np.ndarray
enhance_image(image, config=None, use_denoise=True) -> np.ndarray- Ordem: CLAHE no canal L do LAB →
fastNlMeansDenoisingColored→ unsharp. Comai.cnn_enhanceum passo residual CNN (remoção aprendida de ruído/smearing,ai.cnn.ResidualEnhancer) corre a seguir ao unsharp (canal L do LAB, tiles 64×64 com overlap; v0.7.0). use_denoise=Falseomite o passo mais lento (usado pelo--fast).
@dataclass
class ProcessResult:
original: np.ndarray
stabilized: np.ndarray
enhanced: np.ndarray # estabilizado + CLAHE + denoise + unsharp + polimento
enhanced_raw: np.ndarray # o mesmo, sem polimento (base da avaliação)
detection: DiskDetection | None
process_image(image, config=None) -> ProcessResult
process_path(path, config=None) -> ProcessResult # ValueError se ilegívelclass FrameReader(path):
.fps -> float
.frame_count -> int # 0 quando desconhecido
.size -> tuple[int, int] # (largura, altura)
.close() / context manager
iterável: frames BGR
# ValueError se o vídeo não abrirsharpness(frame, config=None) -> float # variância do Laplaciano
estimate_sharpness_threshold(scores, percentile=25.0) -> float
select_sharp_frames(frames, config=None, minimum=None) -> list[(idx, frame, score)]- Ordem de limiar:
minimum→config.lucky.min_sharpness→ percentil estimado da própria sequência.
stack_frames(frames, stacking=None) -> np.ndarray # ValueError se vazio ou shapes diferentes
select_best(frames, n_best, config=None) -> list[np.ndarray]stack_frames: mediana (use_median=True) ou média; avisa sobre memória acima de 1080p com muitos frames.
Leitura de metadados e sugestões de parâmetros (implementação própria, MIT — inspirada na mesma ideia do MetadataExplorer, sem código copiado).
@dataclass(frozen=True)
class MediaMetadata:
path: str | None
kind: str # "image" | "video"
width: int | None
height: int | None
aspect_ratio: float | None # largura / altura
fps: float | None
frame_count: int | None
duration: float | None # segundos
codec: str | None
bitrate: int | None # bits/segundo
format_name: str | None
iso: int | None # sensibilidade ISO (EXIF)
exposure_time: float | None # segundos
focal_length: float | None # mm
aperture: float | None # f/ número
camera_make: str | None
camera_model: str | None
captured_at: str | None # data/hora EXIF
raw: dict # tudo o que foi lido (source→chave→valor)
extract_metadata(path) -> MediaMetadata- Vídeo: cascata ffprobe (se instalado; codec/bitrate/duração/formato) → OpenCV (resolução/fps/frames — sempre disponível).
- Imagem: EXIF via PIL (ISO, exposição, abertura, distância focal, câmara, data).
ValueErrorse o caminho não existir;kind="unknown"com o que der para ler se nem ffprobe nem OpenCV abrirem o ficheiro.aspect_ratioarredondado a 3 casas (0.0 →None); o texto de apresentação (ex.5616×3744 · 3:2) éaspect_textemextractor(16:9, 3:2, 4:3, 1:1, quadrado ou mudança decimal).
suggest_config(meta: MediaMetadata) -> AstroFrameConfig
summary_fields(meta: MediaMetadata) -> dict[str, str]- Heurísticas: raios de deteção proporcionais à resolução (
min = 8%do semieixo menor,max = 45%);denoise.hescalado pelo ISO (2 + ISO/1600*4, limitado a[2, 15], usado por defeito se o config não o defina) comunsharp0.4/0.6; em vídeos muito comprimidos (< 0.1 bit/pixel) o denoise é reduzido ~30% (menos risco de "plastificar"). summary_fieldsdevolve o dicionário exibido no painel "Proporção / qualidade / sugestões" da interface.
Calibração da deteção contra exemplos (fotos/vídeos), com ground truth manual.
@dataclass(frozen=True)
class SampleRef:
kind: str # "image" | "video"
path: Path # caminho absoluto do ficheiro
frame: int | None # índice de frame (None para imagens)
key: str # "path_relativo#frame" (chave estável no store)
label: str # "IMG path" / "VID path #frame" (interface)
scan_samples(root, frames_per_video=8) -> list[SampleRef]
sample_video_frames(frame_count, n=8) -> list[int]
load_frame(sample) -> np.ndarray # BGR (imagem ou frame amostrado)
item_key(relpath, frame=None) -> str
item_label(kind, relpath, frame=None) -> str- Varrimento recursivo da pasta; imagens (jpg/jpeg/png/bmp/tif/tiff/webp) entram tal como estão e vídeos (mp4/avi/mov/mkv/m4v) contribuem com N frames equidistantes e determinísticos (meios-intervalos — reproduzível na validação). Vídeos ilegíveis são ignorados com aviso.
load_framelê a imagem viacv2.imreadou o frame do vídeo viaFrameReader.frame_at(index)(novo — procuraCAP_PROP_POS_FRAMES; erros levantamValueError).
@dataclass
class CalibrationItem:
path: str # caminho relativo à pasta de amostras
kind: str
frame: int | None
width: int
height: int
circles: list[DiskDetection] = [] # ground truth manual
class CalibrationStore(path): # JSON v1 (samples/calibration.json)
.load() -> None # idempotente; ilegível/versão -> vazio
.save() -> None
.upsert_item(key, item) -> None # grava logo
.get_item(key) -> CalibrationItem | Nonecircles_to_layers(image_rgb, circles) -> {"background": ..., "layers": [...]}
layers_to_circles(layers) -> list[DiskDetection]circles_to_layersconstrói o valor dogr.ImageEditor: o fundo + uma camada RGBA por círculo (disco translúcido + bordo opaco). As camadas são arrastáveis na interface → mover um círculo = arrastar a camada; pintar por cima adiciona; a borracha remove.layers_to_circlesconverte o que o utilizador desenhou em círculos — um por componente conexa de cada camada (duas pinturas separadas na mesma camada = dois círculos); aceita camadas com alpha ou RGB.
circle_iou(a, b) -> float # interseção/união 0–1
match_circles(manual, detected, iou_threshold=0.5)
-> (pairs: list[(i, j)], unmatched_manual: set, unmatched_detected: set)
@dataclass
class ItemReport: # por amostra
label, n_manual, n_detected, n_matched,
n_false_negatives, n_false_positives,
mean_iou, mean_center_error, mean_radius_error_pct # None sem pares
@dataclass
class CalibrationReport: # agregado
items, total_* , recall, precision,
mean_iou, mean_center_error, mean_radius_error_pct,
score: float | None # 0–100 = 0.4·recall + 0.3·precisão + 0.3·IoU
validate_item(label, manual, detected) -> ItemReport # erro de raio com sinal (%)
validate_all([(label, manual, detected), ...]) -> CalibrationReport
suggest_parameters(report, config=None) -> list[str] # sugestões PT- Correspondência greedy por IoU decrescente (limiar 0.5): manual↔deteção.
build_app(config=None) -> gr.Blocks
run(config_path=None, host="127.0.0.1", port=7860, share=False, inbrowser=True) -> None
def inspect_video_upload(video_path, db=None, config=None) -> tuple[str, dict, dict, dict, dict, dict]
def process_video(video_path, export=False, denoise_h=None, ...) -> Generator[tuple]
def process_image_input(image, clip_limit=None, denoise_h=None, ..., db=None, config=None) -> tuple[np.ndarray, np.ndarray, np.ndarray, str, dict, str]
def manual_feedback(state, stars, db=None, config=None) -> tuple[str, str]
def run_autotune_tab(samples_dir, budget, params, anneal, register, config=None) -> Generator[tuple[str, dict | None]]- A UI converte RGB→BGR na entrada e BGR→RGB nas saídas (funções
_to_pipeline/_from_pipeline); os valores e o pipeline são partilhados com a CLI. - Três separadores: Imagem (entrada, estabilizado, processado, zoom, avaliação, sliders, avaliação manual + log de aprendizagem), Vídeo (upload, painel de metadados, sliders pré-preenchidos, processamento ao vivo com discos desenhados, avaliação automática e manual, exportação opcional) e Auto-tune (pasta de samples, orçamento em segundos, parâmetros e registo no banco → progresso, relatório e configuração resultante).
inspect_video_uploadchamameta.extractor+meta.suggest+apply_learned(avaliações anteriores do mesmo perfil de câmara e deltas do auto-tuning) e devolve, respetivamente: HTML do resumo (proporção/qualidade/sugestões), os metadados crus e osupdate()dos sliders.run_autotune_tabé um gerador (consumido pelogr.Progress.track): devolve pares(mensagem, config_dict_ou_None)— primeiro o aviso de pasta em falta (se aplicável), depois o progresso e por fim o relatório com a configuração otimizada; chamaai.tuner.run_autotunee regista no banco quandoregister=True.process_videoé um gerador (consumido pelogr.Progress.trackdo Gradio); a cada frame devolve:(live_rgb, preview_rgb, out_video_path_ou_None, status, progress, rating_html, run_state, log_html)—liveé o frame original em tempo real com os discos detetados (_draw_disks: verde = astro maior, amarelo = discos secundários, vermelho = reflexos — separados por_split_disks, que usa o centro do disco vs. raio do astro maior),previewé o resultado final mostrado apenas em frames espaçados (_preview_every), os outros campos comNone/fração no meio do passe. Sem disco detetado em nenhum frame, o resultado final sai sem polimento e a avaliação é calculada sem deteção (aviso no estado). Seexport=True, escreve o vídeo completo com polimento (.mp4, codecmp4v, sem áudio) e devolve o caminho no último frame.process_image_inputdevolve(estabilizado, processado, zoom, html da avaliação, estado, log de aprendizagem); o estado (perfil, avaliação, parâmetros) alimenta omanual_feedbackque grava a avaliação em estrelas e reporta o ajuste aprendido no log.run()aceitainbrowserpara abrir o navegador automaticamente; o ponto de entrada único equivalente épython main.pyna raiz do repositório.
build_calibration_app(samples_dir="samples", config=None, store=None) -> gr.Blocks
run(samples_dir="samples", config_path=None, host="127.0.0.1", port=7860, share=False, inbrowser=True) -> None
def load_item_payload(key, samples_dir, config=None, store=None) -> (dict, str)
def auto_detect_payload(key, samples_dir, config=None) -> (dict, str)
def save_item_circles(editor_value, key, samples_dir, store=None) -> str
def validate_all_report(samples_dir, config=None, store=None) -> (rows, summary_html, suggestions_html)- Layout: dropdown de amostras +
gr.ImageEditor(camadas RGBA por círculo, pincel/borracha) + botões "Deteção automática" / "Guardar ajustes" / "Validar todas as amostras" → tabela por amostra, resumo global (score 0–100, recall, precisão, IoU, erros) e sugestões de parâmetros. load_item_payloaddá prioridade ao ground truth guardado; sem ele usa a deteção automática como ponto de partida.save_item_circlesconverte as camadas do editor em círculos e grava no store.validate_all_reportpercorre todas as amostras (imagens + frames amostrados dos vídeos).- Ponto de entrada equivalente:
python calibrate.pyna raiz do repositório ouastroframe calibrate(CLI).
main(argv=None) -> int # ponto de entrada do script `astroframe`
build_parser() -> argparse.ArgumentParser
process_images(paths, output_dir, config) -> tuple[int, int] # (sucessos, falhas)
process_video(path, output, config, mode, stack_n, fast) -> str # caminho de saída- Subcomandos:
serve,process,video(--mode stabilize|enhance|stack,--fast),config-template,calibrate(interface de calibração) eautotune(auto-tuning:--samples DIR,--budget N,--seed N,--no-anneal,--params nome1,nome2,--profile NOME,--export FILE,--reset). process_imagescontinua após falhas individuais e levantaRuntimeErrorse nada for processado.mode="stack"centraliza os frames antes de empilhar. Exportação de vídeo não copia áudio (limitado pelo OpenCV).
Camada de IA: auto-tuning e redes neuronais pequenas com núcleo NumPy
puro (o PyTorch, obrigatório desde a v0.9.0, alimenta apenas o RIFE).
Módulos: params
(registry de parâmetros ajustáveis), tuner (auto-tuning), lstm
(aprendizagem temporal), cnn (melhoria residual + filtro de deteção),
feedback (aprendizagem por avaliação), score (avaliação automática) e
rife (interpolação).
Segurança: tudo está desligado por omissão (tuning.enabled=false,
ai.*); um modelo em falta ou corrompido degrada silenciosamente e nunca
bloqueia a pipeline.
class RifeInterpolator(repo, source="github", model_name="IFNet", device=None):
.available() -> bool # stateless: False se PyTorch não instalado
.interpolate(frame_a, frame_b, n_interp=1) -> list[np.ndarray]- Usado pela CLI
astroframe video --interp N. Aceita BGR; devolven_interpframes intermédios em BGR. A interface do modelo depende do repositório RIFE usado (o_inferinternal é o ponto a ajustar entre versões); o CLI avisa e continua sem interpolação se o modelo não carregar.
Fonte única de verdade dos intervalos seguros, passos de otimização e
deltas de treino de todos os parâmetros ajustáveis da pipeline (antes
espalhados por validator.py e feedback.py). Todo o valor aprendido passa
por clamp_value — os clamps são sempre aplicados via registry.
@dataclass(frozen=True)
class ParamSpec:
path: str # "stabilizer.param2"
low: float; high: float # intervalo seguro (clamp)
step: float # passo inicial da subida de colina
dtype: type # int | float (ints são arredondados)
odd: bool # True: tem de ficar ímpar (kernels Gaussianos)
group: str # detect | geometry | enhance | stack | polish | score | meta
costly: bool # True: avaliação lenta (denoising)
punish: float; reward: float # deltas de treino do validator (grupo "detect")
PARAM_SPECS: dict[str, ParamSpec] # caminhos de parâmetros registados
FEEDBACK_PARAMS: tuple[str, ...] # os 5 parâmetros visuais do feedback por estrelas
specs(group=None) -> list[ParamSpec] # ordem de declaração; opcionalmente por grupo
spec(path) -> ParamSpec
spec_by_name(name) -> ParamSpec
bounds(path) -> tuple[float, float] # intervalo seguro
step(path) -> float # passo inicial de otimização
clamp_value(path, value) -> int | float # clamps + arredonda ints + força ímpar
get_param(config, path) -> float
set_param(config, path, value) -> None # sem clamp
apply_deltas(config, deltas) -> AstroFrameConfig # cópia com clamps aplicados
deltas_dict(config, paths) -> dict[str, float] # deltas vs. valores por omissão
default_punish_deltas() / default_reward_deltas() -> dict # grupo "detect"- Grupos:
detect(os 7 pesos do validator:param2,param1,dp,gaussian_kernel_size,gaussian_sigma,occluded_ratio,occluded_ring),geometry(max_radius,max_disks,jitter_alpha),enhance(CLAHE, denoise, unsharp, lucky imaging),stack,polish,scoreemeta. clamp_valueé o único ponto por onde passa qualquer valor aprendido — a estabilidade do treino depende de nunca falhar.
Otimiza os parâmetros de deteção/melhoria contra as amostras de calibração através de uma pesquisa determinística com orçamento de tempo.
@dataclass
class TuneReport:
objective: float # 0–1 (deteção 0.6 · estrelas 0.4 por omissão)
stars: float # 0–5 sobre as amostras melhoradas
detection: float | None # 0.4·recall + 0.3·precisão + 0.3·IoU; None sem ground truth
recall: float; precision: float; mean_iou: float
elapsed_s: float; n_items: int; n_scored: int
to_dict() -> dict
@dataclass
class TuneResult:
config: AstroFrameConfig # melhor configuração encontrada
deltas: dict[str, float] # ajustes vs. a base
base: AstroFrameConfig
report: TuneReport
evaluations: int
lines: list[str] # relatório humano (parâmetro · base → ajustado)
to_dict() -> dict
class ProxyEval(samples_dir, work_scale=0.5, frames_per_sample=3,
detection_weight=0.6, seed=42):
.evaluate(config) -> TuneReport # em cache pelos parâmetros efetivos
.clear_cache() -> None
class BoundedHillClimb(specs, budget_s=60.0, seed=42, anneal=True,
patience=3, improve_eps=1e-4):
.optimize(evaluate, base, start_deltas=None) -> TuneResult
run_autotune(samples_dir, config=None, budget_s=60.0, seed=42, anneal=True,
params_filter=None, export_path=None, profile=DEFAULT_PROFILE,
db=None, work_scale=0.5, frames_per_sample=3,
detection_weight=0.6) -> TuneResult
export_trained_config(config, deltas, report, path) -> Path
tuning_table_lines(result) -> list[str]- Avaliação por proxy (
ProxyEval): corre a pipeline nas imagens de calibração (pastasamples/+ ground truth emLogs/train/calibration.json), mede o IoU médio entre os discos detetados (Hough) e os esperados, com penalizações por discos extra/em falta, e pontua alguns frames melhorados com estrelas. Os frames são reduzidos para ~480p (escala de trabalho máxima 0.5, nunca ampliados); os relatórios ficam em cache pelos valores efetivos dos parâmetros — a otimização só reavalia o que mudou. - Pesquisa (
BoundedHillClimb): passes de +passo/−passo por parâmetro sobre o registry (aceita o melhor se melhorar ≥improve_eps), momentum (duplica o passo após 2 aceites consecutivos na mesma direção), falhas reduzem o passo a metade (mínimo passo/8); comanneal=Truecandidatos piores são aceites com probabilidadeexp(−Δ/T), T a decair por passe — escapa a mínimos locais sem sair dos intervalos seguros do registry. Parâmetros caros (denoising) só são tentados nos passes pares. Determinístico (seedfixa) com orçamento de tempo (budget_s). run_autotunepode ser pré-semeado com previsões LSTM (_lstm_seed: deltas doai.lstm.LSTMTunersobre o histórico do perfil, mantidos apenas quando melhoram o objetivo do proxy) e regista o resultado no banco de feedback (tabelatuning) quando recebe umdboufeedback.enabledestá ativo. A CLI (astroframe autotune) e o separador Auto-tune chamam-na.export_trained_configescreve um JSON com osparamsefetivos, osdeltas, oreportdo proxy e uma secçãostabilizer(compatível com o export antigo); a CLI escreveLogs/train/trained_config.jsonpor omissão.
Célula LSTM de uma camada implementada à mão em NumPy puro (forward + backward com backprop-through-time, portas i/f/o/g vetorizadas — sem novas dependências), usada por dois preditores.
torch_available() -> bool # True se PyTorch estiver instalado (usado pelo RIFE)
class LSTMCell(n_in, n_hidden, rng=None):
.forward(x_seq, h0=None) -> (h, cache) # (T, n_in) → h, cache
.forward_full(x_seq) -> np.ndarray # saídas de todos os timesteps
.backward(x_seq, cache, dh_next=None) -> dict
.save(path) -> Path
.load(path) -> LSTMCell | None
@dataclass
class FitHistory:
epochs: int; final_loss: float; best_loss: float; best_epoch: int
class LSTMTuner(n_hidden=24, seed=42):
.fit(history, epochs=200, lr=0.05, seq_len=8, val_fraction=0.2,
patience=6) -> FitHistory
.predict_next_delta(history, seq_len=8) -> dict[str, float] # {} sem dados
.save(path=None) -> Path # Logs/weights/lstm.npz
.load(path=None) -> LSTMTuner | None
class TrajectoryPredictor(maxlen=8, use_lstm=False, model_path=None):
.push(cx, cy) -> None; .clear() -> None; len() -> int
.predict() -> tuple[float, float] | None # None sem histórico suficiente
train_trajectory_model(trajectories, path=None, seed=42, epochs=60) -> Path
trajectory_model_path(path=None) -> PathLSTMTuner— treinado offline (gradiente descendente em batch completo, 20% de validação, early stop) sobre o histórico de feedback (estrelas + métricas, 9 características por execução, janelas deslizantes) e prevê os deltas de parâmetros para a próxima execução (os 5 parâmetros visuais do feedback). O auto-tuning (ai.tuner.run_autotune) usa esta previsão como pré-semente; sem histórico ou convergência suficientes devolve{}— a pesquisa parte da base.TrajectoryPredictor— prevê o próximo centroide do disco a partir das últimas deteções: regressão linear (mínimos quadrados sobre o histórico) como base, com um refinamento LSTM opcional (célula 2→8, treinada em trajetórias sintéticas portrain_trajectory_model) quandouse_lstm=Truee existe um modelo compatível. Usado peloAntiJitterStabilizercomai.lstm_trajectory; sem histórico devolveNone— nada muda.- Os modelos são guardados como
.npzversionados (Logs/weights/lstm.npzpor omissão); um ficheiro corrompido ou com a versão errada carrega comoNone(fallback silencioso).
Rede convolucional pequena em NumPy puro (2× conv 3×3 + ReLU + pooling + cabeça) com duas cabeças permutáveis, treino offline determinístico (seed fixa) e gradientes verificados por diferenças finitas.
@dataclass
class FitReport:
epochs: int; final_loss: float; best_loss: float; best_epoch: int
class SmallCNN(mode="residual", k=8, seed=42, n_in=1):
.forward(x) -> (out, cache) # x: (N, 1, H, W)
.predict_class(x) -> np.ndarray # P(disco) por patch (modo classify)
.backward_residual / .backward_classify(grad, cache) -> dict
.save(path) -> Path
.load(path) -> SmallCNN | None
fit_residual(pairs, model=None, epochs=40, lr=0.05, batch_size=8,
val_fraction=0.2, seed=42) -> tuple[SmallCNN, FitReport]
fit_classifier(positives, negatives, model=None, epochs=60, lr=0.05,
batch_size=8, val_fraction=0.2, seed=42) -> tuple[SmallCNN, FitReport]
class ResidualEnhancer(model=None, model_path=None):
.available -> bool
.apply(image_bgr) -> np.ndarray # inalterada sem modelo
class DiskFilter(model=None, model_path=None):
.available -> bool
.patch(image, cx, cy, radius) -> np.ndarray # patch cinza, 2× raio → 48×48
.confidence(image, cx, cy, radius) -> float # P(disco); 0.5 sem modelo
.filter_disks(disks, image, threshold) -> list # nunca esvazia a listafit_residualtreina a cabeça residual em pares(entrada, alvo): aprender = y − x(MSE, 20% de validação, early stop patience 5). OResidualEnhancertreinado é aplicado porenhance_imagea seguir ao passo unsharp quandoai.cnn_enhance: o residual é somado ao canal L do LAB em tiles 64×64 com overlap (as cores preservam-se); sem modelo a imagem sai inalterada.fit_classifiertreina a cabeça disco/ruído em patches positivos (disco) e negativos (cross-entropy). ODiskFilterpontua cada deteção (confidence) e ofind_all_disksdescarta os candidatos abaixo deai.disk_filter(0–1) — nunca esvazia a lista detetada.- Modelos:
Logs/weights/enhancer_cnn.npzeLogs/weights/disk_filter.npz(.npzversionados; corrompidos ou com a versão errada → fallback silencioso).
@dataclass
class StarRating:
stars: float # 0.0–5.0 (peso das métricas = 1)
score: float # 0.0–1.0 não ponderado
metrics: dict[str, float] # noise | contrast | size | corona; 0 (mau) a 1 (bom)
explanation: str # texto humano com o porquê
score_image(image, detection=None, config=None) -> StarRating
package_rating(original, stabilized, detection, config=None) -> StarRating
score_from_stars(stars, metrics=None) -> StarRating # para testes/externalizaçãonoise= variância do Laplaciano (sem ruído → 1),contrast= relação percentil 99/50 da luminância,size= raio do disco vs. frame,corona= brilho médio do anel coroa (1–2× raio) vs. disco.score_imagefunciona sem deteção (métricas de ruído/contraste apenas).
@dataclass(frozen=True)
class ConfigNudge:
clip_limit: dict # {multiplicador, offset} ex.: {'m': 1.0, 'b': 0.0}
denoise: dict # ex.: {'h': {'m': 1.0, 'b': 1.5}}
unsharp: dict # {'amount': {...}, 'sigma': {'m': 0.7, 'b': 1.2}}
polish: dict # {'brightness': {...}}
explanation: dict[str, str] # texto por parâmetro (porquê do ajuste)
judicial_override: bool # True: a avaliação má impõe logo a correção
factor: float # escala global da correção (feedback.learning_rate)
@dataclass(frozen=True)
class RunRecord:
id: int; profile: str; kind: str; params: dict; metrics: dict
stars: float; source: str; at: str; modified: dict | None
def profile_for(kind, width, height) -> str # ex.: "video@5616x3744"
def format_profile(profile) -> str # "5616×3744 · vídeo" (interface)
def record_run(db, kind, profile, config, params, rating, source="cli") -> RunRecord
def recent_nudges(db, profile, limit=5) -> list[RunRecord]
def apply_learned(config, profile, db=None) -> AstroFrameConfig
def _learning_db(config, db=None) -> FeedbackDB | None # feedback.enabled?
def _learning_log_html(profile, db) -> str # histórico (interface)
class FeedbackDB(path=None): # SQLite com locking retry (WAL)
.history(profile, limit=50, base=None) -> list[RunRecord]
.latest_ids(profile, limit=5) -> list[int]
.nudges(profile_runs, nudge_params, factor) -> ConfigNudge # regras
.store_run(kind, profile, config, params, stars, source, metrics) -> RunRecord
.apply_nudge(config, nudge) -> AstroFrameConfig
.add_tuning(profile, base_params, deltas, report, source="autotune") -> None
.history_all(limit=32) -> list[dict] # histórico de tuning (tabela tuning)
.recent_tuning(profile=None, limit=1) -> list[dict[str, float]] # deltas
.reset_tuning(profile=None) -> int # limpa o histórico de tuning- Banco SQLite em
Logs/logs/system/feedback.db(ou$ASTROFRAME_FEEDBACK_DB); criado ao primeiro uso, com retry em base bloqueada ehistory_limitpor perfil. Além da tabelaruns(avaliações), existe a tabelatuning(resultados do auto-tuning: base_params, deltas, relatório e fonte). apply_learnedsoma os nudges por estrelas e os deltas do auto-tuning (tabelatuning), sempre com clamp do registry unificado; sem nada aprendido devolve a configuração original inalterada (as regras do feedback por estrelas continuam como antes: avaliações boas e consistentes suavizam o ajuste (user_weight), avaliações más aplicam denoise extra com ruído (métricas >1.0), coroa fraca aumenta o brilho do polimento, disco pequeno reduz os raios do detetor; os valores são limitados aos intervalos válidos).FeedbackDB.default_path() -> Path,.path -> Path,.close().