feat(klausur): Handschrift entfernen + Klausur-HTR implementiert
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 26s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m49s
CI / test-python-agent-core (push) Successful in 14s
CI / test-nodejs-website (push) Successful in 15s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 26s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m49s
CI / test-python-agent-core (push) Successful in 14s
CI / test-nodejs-website (push) Successful in 15s
Feature 1: Handschrift entfernen via OCR-Pipeline Session
- services/handwriting_detection.py: _detect_pencil() + target_ink Parameter
("all" | "colored" | "pencil") für gezielte Tinten-Erkennung
- ocr_pipeline_session_store.py: clean_png + handwriting_removal_meta Spalten
(idempotentes ALTER TABLE in init_ocr_pipeline_tables)
- ocr_pipeline_api.py: POST /sessions/{id}/remove-handwriting Endpoint
+ "clean" zu valid_types für Image-Serving hinzugefügt
Feature 2: Klausur-HTR (Hochwertige Handschriftenerkennung)
- handwriting_htr_api.py: Neuer Router /api/v1/htr/recognize + /recognize-session
Primary: qwen2.5vl:32b via Ollama, Fallback: trocr-large-handwritten
- services/trocr_service.py: size Parameter (base | large) für get_trocr_model()
+ run_trocr_ocr() - unterstützt jetzt trocr-large-handwritten
- main.py: HTR Router registriert
Config:
- docker-compose.yml: OLLAMA_HTR_MODEL, HTR_FALLBACK_MODEL
- .env.example: HTR Env-Vars dokumentiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -168,6 +168,13 @@ class RowGroundTruthRequest(BaseModel):
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class RemoveHandwritingRequest(BaseModel):
|
||||
method: str = "auto" # "auto" | "telea" | "ns"
|
||||
target_ink: str = "all" # "all" | "colored" | "pencil"
|
||||
dilation: int = 2 # mask dilation iterations (0-5)
|
||||
use_source: str = "auto" # "original" | "deskewed" | "auto"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Session Management Endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -309,7 +316,7 @@ async def delete_session(session_id: str):
|
||||
@router.get("/sessions/{session_id}/image/{image_type}")
|
||||
async def get_image(session_id: str, image_type: str):
|
||||
"""Serve session images: original, deskewed, dewarped, binarized, columns-overlay, or rows-overlay."""
|
||||
valid_types = {"original", "deskewed", "dewarped", "binarized", "columns-overlay", "rows-overlay", "words-overlay"}
|
||||
valid_types = {"original", "deskewed", "dewarped", "binarized", "columns-overlay", "rows-overlay", "words-overlay", "clean"}
|
||||
if image_type not in valid_types:
|
||||
raise HTTPException(status_code=400, detail=f"Unknown image type: {image_type}")
|
||||
|
||||
@@ -1906,3 +1913,90 @@ async def _get_words_overlay(session_id: str) -> Response:
|
||||
raise HTTPException(status_code=500, detail="Failed to encode overlay image")
|
||||
|
||||
return Response(content=result_png.tobytes(), media_type="image/png")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Handwriting Removal Endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.post("/sessions/{session_id}/remove-handwriting")
|
||||
async def remove_handwriting_endpoint(session_id: str, req: RemoveHandwritingRequest):
|
||||
"""
|
||||
Remove handwriting from a session image using inpainting.
|
||||
|
||||
Steps:
|
||||
1. Load source image (auto → deskewed if available, else original)
|
||||
2. Detect handwriting mask (filtered by target_ink)
|
||||
3. Dilate mask to cover stroke edges
|
||||
4. Inpaint the image
|
||||
5. Store result as clean_png in the session
|
||||
|
||||
Returns metadata including the URL to fetch the clean image.
|
||||
"""
|
||||
import time as _time
|
||||
t0 = _time.monotonic()
|
||||
|
||||
from services.handwriting_detection import detect_handwriting
|
||||
from services.inpainting_service import inpaint_image, dilate_mask as _dilate_mask, InpaintingMethod, image_to_png
|
||||
|
||||
session = await get_session_db(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
|
||||
|
||||
# 1. Determine source image
|
||||
source = req.use_source
|
||||
if source == "auto":
|
||||
deskewed = await get_session_image(session_id, "deskewed")
|
||||
source = "deskewed" if deskewed else "original"
|
||||
|
||||
image_bytes = await get_session_image(session_id, source)
|
||||
if not image_bytes:
|
||||
raise HTTPException(status_code=404, detail=f"Source image '{source}' not available")
|
||||
|
||||
# 2. Detect handwriting mask
|
||||
detection = detect_handwriting(image_bytes, target_ink=req.target_ink)
|
||||
|
||||
# 3. Convert mask to PNG bytes and dilate
|
||||
import io
|
||||
from PIL import Image as _PILImage
|
||||
mask_img = _PILImage.fromarray(detection.mask)
|
||||
mask_buf = io.BytesIO()
|
||||
mask_img.save(mask_buf, format="PNG")
|
||||
mask_bytes = mask_buf.getvalue()
|
||||
|
||||
if req.dilation > 0:
|
||||
mask_bytes = _dilate_mask(mask_bytes, iterations=req.dilation)
|
||||
|
||||
# 4. Inpaint
|
||||
method_map = {
|
||||
"telea": InpaintingMethod.OPENCV_TELEA,
|
||||
"ns": InpaintingMethod.OPENCV_NS,
|
||||
"auto": InpaintingMethod.AUTO,
|
||||
}
|
||||
inpaint_method = method_map.get(req.method, InpaintingMethod.AUTO)
|
||||
|
||||
result = inpaint_image(image_bytes, mask_bytes, method=inpaint_method)
|
||||
if not result.success:
|
||||
raise HTTPException(status_code=500, detail="Inpainting failed")
|
||||
|
||||
elapsed_ms = int((_time.monotonic() - t0) * 1000)
|
||||
|
||||
meta = {
|
||||
"method_used": result.method_used.value if hasattr(result.method_used, "value") else str(result.method_used),
|
||||
"handwriting_ratio": round(detection.handwriting_ratio, 4),
|
||||
"detection_confidence": round(detection.confidence, 4),
|
||||
"target_ink": req.target_ink,
|
||||
"dilation": req.dilation,
|
||||
"source_image": source,
|
||||
"processing_time_ms": elapsed_ms,
|
||||
}
|
||||
|
||||
# 5. Persist clean image (convert BGR ndarray → PNG bytes)
|
||||
clean_png_bytes = image_to_png(result.image)
|
||||
await update_session_db(session_id, clean_png=clean_png_bytes, handwriting_removal_meta=meta)
|
||||
|
||||
return {
|
||||
**meta,
|
||||
"image_url": f"/api/v1/ocr-pipeline/sessions/{session_id}/image/clean",
|
||||
"session_id": session_id,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user