Studio: render figure regions (vector + raster) for RAG captioning

page.get_images() only returns raster blobs embedded in the PDF's
resource dictionary, so vector schematics like Figure 1 — drawn purely
with paths/lines — were never extracted, and the VLM only ever saw
incidental embedded photos that happened to live near figures.

Replace the xref-based extraction with bbox rendering: union the
bounding rects of all vector drawings and raster image_info entries on
each page, expand a few points, and render the region with
get_pixmap(clip=bbox, matrix=2x). The captioner now receives the
actual figure — schematic arrows, box labels, legend text, and any
inset photos — and produces a caption that describes the figure as a
whole, not just one embedded sub-image.

Also sharpen the captioner prompt: explicitly tell the VLM the image
is a single figure cropped from a PDF page, and not to describe page
chrome or body paragraphs.
This commit is contained in:
Roland Tannous 2026-05-27 16:36:54 +04:00
commit 0be7ca39a4
2 changed files with 67 additions and 31 deletions

View file

@ -33,9 +33,14 @@ from loggers import get_logger
logger = get_logger(__name__)
_PROMPT = (
"Describe this figure in <=60 words. Focus on factual content "
"(axes, labels, captions, visible text, main objects). "
"Do not speculate beyond what is visible."
"This image is a region cropped from a PDF page that contains a "
"single figure (schematic, chart, diagram, table, photo, or "
"their combination). Describe the figure's structure and content "
"in <=80 words. Focus on factual visible content: axes, labels, "
"arrow labels, box labels, legends, visible text in the figure, "
"and what entities are connected to what. Do not speculate beyond "
"what is visible and do not describe the page header/footer or "
"body paragraphs."
)
_MAX_NEW_TOKENS = 200
# Downscale large images so the base64 payload stays manageable; the chat

View file

@ -58,45 +58,76 @@ def _extract_with_pymupdf(path: Path, want_images: bool) -> ParseResult:
doc.close()
# Pages smaller than this (in PDF points) are ignored as figure regions —
# bigger than a typical icon/glyph, smaller than a banner.
_MIN_FIGURE_PT = 60
# 2× scale renders at 144 dpi (PDF default is 72 dpi). Enough resolution for
# the captioner to read axis labels, arrow text, and inset photos.
_RENDER_SCALE = 2.0
# Expand the union bbox a few points so caption baselines / borders survive.
_FIGURE_MARGIN_PT = 8.0
def _extract_images_pymupdf(doc, pages: list[ParsedPage]) -> list[ParsedImage]:
"""Render each page's figure region (vector drawings + raster sub-images)
as a single PNG. Vector schematics like Figure 1 (no embedded raster)
are visible to the captioner only via rendering ``page.get_images``
misses them entirely. We union all non-text geometry on a page into
one bbox; for academic papers this typically maps 1:1 to "the figure
on this page".
"""
import pymupdf
captions_by_page: dict[int, str] = {
p.page_number: p.text for p in pages if p.page_number
}
out: list[ParsedImage] = []
for page_index in range(len(doc)):
page = doc[page_index]
page_number = page_index + 1
try:
image_list = doc[page_index].get_images(full = True)
rects: list[pymupdf.Rect] = []
for drawing in page.get_drawings() or []:
rect = drawing.get("rect")
if rect is not None:
rects.append(pymupdf.Rect(rect))
for info in page.get_image_info(xrefs = True) or []:
bbox = info.get("bbox")
if bbox is not None:
rects.append(pymupdf.Rect(bbox))
except Exception:
continue
for img_info in image_list:
xref = img_info[0]
try:
extracted = doc.extract_image(xref)
except Exception:
continue
image_bytes = extracted.get("image")
ext = (extracted.get("ext") or "png").lower()
mime = {
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"gif": "image/gif",
"webp": "image/webp",
"bmp": "image/bmp",
"tiff": "image/tiff",
}.get(ext, f"image/{ext}")
if not image_bytes:
continue
caption = (captions_by_page.get(page_number, "") or "")[:1500]
out.append(
ParsedImage(
image_bytes = image_bytes,
mime_type = mime,
page_number = page_number,
nearest_caption = caption,
)
if not rects:
continue
union = rects[0]
for r in rects[1:]:
union |= r
if union.width < _MIN_FIGURE_PT or union.height < _MIN_FIGURE_PT:
continue
# Expand and clip to page rect so we don't render past page edges.
union = pymupdf.Rect(
union.x0 - _FIGURE_MARGIN_PT,
union.y0 - _FIGURE_MARGIN_PT,
union.x1 + _FIGURE_MARGIN_PT,
union.y1 + _FIGURE_MARGIN_PT,
) & page.rect
try:
matrix = pymupdf.Matrix(_RENDER_SCALE, _RENDER_SCALE)
pix = page.get_pixmap(clip = union, matrix = matrix, alpha = False)
png_bytes = pix.tobytes("png")
except Exception:
continue
if not png_bytes:
continue
caption = (captions_by_page.get(page_number, "") or "")[:1500]
out.append(
ParsedImage(
image_bytes = png_bytes,
mime_type = "image/png",
page_number = page_number,
nearest_caption = caption,
)
)
return out