Add buffered crop UI and API controls
This commit is contained in:
33
app/main.py
33
app/main.py
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import FastAPI, File, HTTPException, UploadFile
|
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
||||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
@@ -33,30 +33,45 @@ def index():
|
|||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-4">
|
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-4">
|
||||||
<input id="file" type="file" accept="image/*" class="block w-full rounded-lg border border-slate-700 bg-slate-950 p-3" />
|
<input id="file" type="file" accept="image/*" class="block w-full rounded-lg border border-slate-700 bg-slate-950 p-3" />
|
||||||
|
<label class="mt-4 block text-sm text-slate-400">Buffer ratio</label>
|
||||||
|
<input id="buffer_ratio" type="number" step="0.05" min="0" max="0.5" value="0.15" class="block w-full rounded-lg border border-slate-700 bg-slate-950 p-3" />
|
||||||
<button id="go" class="mt-4 rounded-lg bg-cyan-500 px-4 py-2 font-semibold text-slate-950">Process</button>
|
<button id="go" class="mt-4 rounded-lg bg-cyan-500 px-4 py-2 font-semibold text-slate-950">Process</button>
|
||||||
<pre id="meta" class="mt-4 whitespace-pre-wrap rounded-lg bg-slate-950 p-3 text-xs text-slate-300"></pre>
|
<pre id="meta" class="mt-4 whitespace-pre-wrap rounded-lg bg-slate-950 p-3 text-xs text-slate-300"></pre>
|
||||||
</section>
|
</section>
|
||||||
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-4">
|
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-4">
|
||||||
<div class="mb-3 text-sm font-semibold text-slate-400">Result</div>
|
<div class="mb-3 text-sm font-semibold text-slate-400">Result</div>
|
||||||
<img id="result" class="hidden w-full rounded-xl border border-slate-800" />
|
<div class="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 text-xs uppercase tracking-wide text-slate-500">Crop</div>
|
||||||
|
<img id="crop" class="hidden w-full rounded-xl border border-slate-800" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 text-xs uppercase tracking-wide text-slate-500">Annotated source</div>
|
||||||
|
<img id="annotated" class="hidden w-full rounded-xl border border-slate-800" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
const file = document.getElementById('file');
|
const file = document.getElementById('file');
|
||||||
const go = document.getElementById('go');
|
const go = document.getElementById('go');
|
||||||
const result = document.getElementById('result');
|
const crop = document.getElementById('crop');
|
||||||
|
const annotated = document.getElementById('annotated');
|
||||||
const meta = document.getElementById('meta');
|
const meta = document.getElementById('meta');
|
||||||
go.onclick = async () => {
|
go.onclick = async () => {
|
||||||
if (!file.files.length) return;
|
if (!file.files.length) return;
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('file', file.files[0]);
|
form.append('file', file.files[0]);
|
||||||
|
form.append('buffer_ratio', document.getElementById('buffer_ratio').value);
|
||||||
meta.textContent = 'Working...';
|
meta.textContent = 'Working...';
|
||||||
const resp = await fetch('/api/focus', { method: 'POST', body: form });
|
const resp = await fetch('/api/focus', { method: 'POST', body: form });
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
meta.textContent = JSON.stringify(data, null, 2);
|
meta.textContent = JSON.stringify(data, null, 2);
|
||||||
result.src = data.crop_data_url;
|
crop.src = data.crop_data_url;
|
||||||
result.classList.remove('hidden');
|
annotated.src = data.annotated_data_url;
|
||||||
|
crop.classList.remove('hidden');
|
||||||
|
annotated.classList.remove('hidden');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
@@ -66,23 +81,23 @@ def index():
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/api/focus")
|
@app.post("/api/focus")
|
||||||
async def focus(file: UploadFile = File(...)):
|
async def focus(file: UploadFile = File(...), buffer_ratio: float = Form(0.15)):
|
||||||
from app.vision import process_image
|
from app.vision import process_image
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = await file.read()
|
payload = await file.read()
|
||||||
return process_image(payload, file.filename or "upload")
|
return process_image(payload, file.filename or "upload", buffer_ratio=buffer_ratio)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/focus/image")
|
@app.post("/api/focus/image")
|
||||||
async def focus_image(file: UploadFile = File(...)):
|
async def focus_image(file: UploadFile = File(...), buffer_ratio: float = Form(0.15)):
|
||||||
from app.vision import process_image
|
from app.vision import process_image
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = await file.read()
|
payload = await file.read()
|
||||||
result = process_image(payload, file.filename or "upload")
|
result = process_image(payload, file.filename or "upload", buffer_ratio=buffer_ratio)
|
||||||
return StreamingResponse(result["crop_bytes_io"], media_type=result["mime_type"])
|
return StreamingResponse(result["crop_bytes_io"], media_type=result["mime_type"])
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ def detect_salient_object(image: np.ndarray) -> BBox | None:
|
|||||||
|
|
||||||
def square_bbox(bbox: BBox, image_shape: tuple[int, int, int], buffer_ratio: float = 0.15) -> BBox:
|
def square_bbox(bbox: BBox, image_shape: tuple[int, int, int], buffer_ratio: float = 0.15) -> BBox:
|
||||||
image_h, image_w = image_shape[:2]
|
image_h, image_w = image_shape[:2]
|
||||||
|
buffer_ratio = max(0.0, min(buffer_ratio, 0.5))
|
||||||
side = int(round(max(bbox.w, bbox.h) * (1 + buffer_ratio * 2)))
|
side = int(round(max(bbox.w, bbox.h) * (1 + buffer_ratio * 2)))
|
||||||
side = max(1, min(side, image_w, image_h))
|
side = max(1, min(side, image_w, image_h))
|
||||||
|
|
||||||
@@ -126,10 +127,10 @@ def _data_url(image_bytes: bytes, mime_type: str) -> str:
|
|||||||
return f"data:{mime_type};base64,{base64.b64encode(image_bytes).decode('ascii')}"
|
return f"data:{mime_type};base64,{base64.b64encode(image_bytes).decode('ascii')}"
|
||||||
|
|
||||||
|
|
||||||
def process_image(image_bytes: bytes, filename: str) -> dict[str, Any]:
|
def process_image(image_bytes: bytes, filename: str, buffer_ratio: float = 0.15) -> dict[str, Any]:
|
||||||
image = decode_image(image_bytes)
|
image = decode_image(image_bytes)
|
||||||
bbox, method = select_primary_bbox(image)
|
bbox, method = select_primary_bbox(image)
|
||||||
square = square_bbox(bbox, image.shape)
|
square = square_bbox(bbox, image.shape, buffer_ratio=buffer_ratio)
|
||||||
crop = crop_image(image, square)
|
crop = crop_image(image, square)
|
||||||
annotated = draw_square(image, square)
|
annotated = draw_square(image, square)
|
||||||
|
|
||||||
@@ -139,6 +140,7 @@ def process_image(image_bytes: bytes, filename: str) -> dict[str, Any]:
|
|||||||
return {
|
return {
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"method": method,
|
"method": method,
|
||||||
|
"buffer_ratio": buffer_ratio,
|
||||||
"detected_bbox": bbox.__dict__,
|
"detected_bbox": bbox.__dict__,
|
||||||
"square_bbox": square.__dict__,
|
"square_bbox": square.__dict__,
|
||||||
"source_size": {"width": int(image.shape[1]), "height": int(image.shape[0])},
|
"source_size": {"width": int(image.shape[1]), "height": int(image.shape[0])},
|
||||||
|
|||||||
Reference in New Issue
Block a user