mirror of
https://github.com/vladmandic/automatic
synced 2026-08-26 15:16:01 +02:00
refactor(vqa): convert to class-based singleton
Refactor VQA module from module-level globals to a VQA class singleton
pattern with self-contained per-model loading methods.
Changes:
- Add VQA class with model/processor state and detection data storage
- Extract load methods for clean model pre-loading via UI
- Interrogate to return string only; store detection data on instance
- Add vqa_draw.py for bounding box/point annotation utilities
Stub, further transfer of drawing functions to follow
- Update moondream3.py to store detection data on VQA singleton
- Update endpoints.py and ui_caption.py for new return type
This commit is contained in:
@@ -100,7 +100,7 @@ def post_vqa(req: models.ReqVQA):
|
||||
image = helpers.decode_base64_to_image(req.image)
|
||||
image = image.convert('RGB')
|
||||
from modules.interrogate import vqa
|
||||
answer, _ = vqa.interrogate(req.question, req.system, '', image, req.model)
|
||||
answer = vqa.interrogate(req.question, req.system, '', image, req.model)
|
||||
return models.ResVQA(answer=answer)
|
||||
|
||||
def post_unload_checkpoint():
|
||||
|
||||
@@ -320,7 +320,7 @@ def predict(question: str, image: Image.Image, repo: str, model_name: str = None
|
||||
**kwargs: Additional parameters (max_objects for detect, etc.)
|
||||
|
||||
Returns:
|
||||
Response string or tuple (text, annotated_image) for detect/point modes
|
||||
Response string (detection data stored on VQA singleton instance.last_detection_data)
|
||||
(or generator if stream=True for query/caption modes)
|
||||
"""
|
||||
debug(f'VQA interrogate: handler=moondream3 model_name="{model_name}" repo="{repo}" question="{question}" image_size={image.size if image else None} mode={mode} stream={stream}')
|
||||
@@ -386,7 +386,7 @@ def predict(question: str, image: Image.Image, repo: str, model_name: str = None
|
||||
debug(f'VQA interrogate: handler=moondream3 point_extracted_object="{object_name}"')
|
||||
result = point(image, object_name, repo)
|
||||
if result:
|
||||
# Handle multiple instances - return text and points for drawing
|
||||
# Handle multiple instances - return text and store points for drawing
|
||||
if len(result) == 1:
|
||||
text = f"Found at coordinates: ({result[0][0]:.3f}, {result[0][1]:.3f})"
|
||||
else:
|
||||
@@ -395,8 +395,11 @@ def predict(question: str, image: Image.Image, repo: str, model_name: str = None
|
||||
for i, (x, y) in enumerate(result, 1):
|
||||
lines.append(f" {i}. ({x:.3f}, {y:.3f})")
|
||||
text = '\n'.join(lines)
|
||||
return (text, {'points': result}) # Return text and points data
|
||||
return ("Object not found", None)
|
||||
# Store detection data on VQA singleton for annotation
|
||||
from modules.interrogate import vqa
|
||||
vqa.get_instance().last_detection_data = {'points': result}
|
||||
return text
|
||||
return "Object not found"
|
||||
elif mode == 'detect':
|
||||
# Extract object name from question - case insensitive
|
||||
object_name = question
|
||||
@@ -413,13 +416,16 @@ def predict(question: str, image: Image.Image, repo: str, model_name: str = None
|
||||
debug(f'VQA interrogate: handler=moondream3 detect_extracted_object="{object_name}"')
|
||||
|
||||
results = detect(image, object_name, repo, max_objects=kwargs.get('max_objects', 10))
|
||||
# Format as string for display and return detections for drawing
|
||||
# Format as string for display and store detections for drawing
|
||||
if results:
|
||||
lines = [f"{det['label']}: [{det['bbox'][0]:.3f}, {det['bbox'][1]:.3f}, {det['bbox'][2]:.3f}, {det['bbox'][3]:.3f}] (confidence: {det['confidence']:.2f})"
|
||||
for det in results]
|
||||
text = '\n'.join(lines)
|
||||
return (text, {'detections': results}) # Return text and detection data
|
||||
return ("No objects detected", None)
|
||||
# Store detection data on VQA singleton for annotation
|
||||
from modules.interrogate import vqa
|
||||
vqa.get_instance().last_detection_data = {'detections': results}
|
||||
return text
|
||||
return "No objects detected"
|
||||
else: # mode == 'query'
|
||||
if len(question) < 2:
|
||||
question = "Describe this image."
|
||||
|
||||
+1059
-1107
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
# VQA Image Annotation Utilities
|
||||
# Drawing functions for bounding boxes, points, and other visual annotations
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from modules import shared
|
||||
|
||||
|
||||
def draw_bounding_boxes(image: Image.Image, detections: list, points: list = None) -> Image.Image:
|
||||
"""
|
||||
Draw bounding boxes and/or points on an image.
|
||||
|
||||
Args:
|
||||
image: PIL Image to annotate
|
||||
detections: List of detection dicts with format:
|
||||
[{'label': str, 'bbox': [x1, y1, x2, y2], 'confidence': float}, ...]
|
||||
where coordinates are normalized 0-1
|
||||
points: Optional list of (x, y) tuples with normalized 0-1 coordinates
|
||||
|
||||
Returns:
|
||||
Annotated PIL Image with boxes and labels drawn, or None if no annotations
|
||||
"""
|
||||
if not detections and not points:
|
||||
return None
|
||||
|
||||
# Create a copy to avoid modifying original
|
||||
annotated = image.copy()
|
||||
draw = ImageDraw.Draw(annotated)
|
||||
width, height = image.size
|
||||
|
||||
# Try to load a font, fall back to default if unavailable
|
||||
try:
|
||||
font_size = max(12, int(min(width, height) * 0.02))
|
||||
font_path = shared.opts.font or "javascript/notosans-nerdfont-regular.ttf"
|
||||
font = ImageFont.truetype(font_path, size=font_size)
|
||||
except Exception:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# Draw bounding boxes
|
||||
if detections:
|
||||
colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF', '#FFA500', '#800080']
|
||||
for idx, det in enumerate(detections):
|
||||
bbox = det['bbox']
|
||||
label = det.get('label', 'object')
|
||||
confidence = det.get('confidence', 1.0)
|
||||
|
||||
# Convert normalized coordinates to pixel coordinates
|
||||
x1 = int(bbox[0] * width)
|
||||
y1 = int(bbox[1] * height)
|
||||
x2 = int(bbox[2] * width)
|
||||
y2 = int(bbox[3] * height)
|
||||
|
||||
# Choose color
|
||||
color = colors[idx % len(colors)]
|
||||
|
||||
# Draw box
|
||||
draw.rectangle([x1, y1, x2, y2], outline=color, width=max(2, int(min(width, height) * 0.003)))
|
||||
|
||||
# Draw label with background
|
||||
label_text = f"{label} {confidence:.2f}" if confidence < 1.0 else label
|
||||
bbox_font = draw.textbbox((x1, y1), label_text, font=font)
|
||||
text_width = bbox_font[2] - bbox_font[0]
|
||||
text_height = bbox_font[3] - bbox_font[1]
|
||||
draw.rectangle([x1, y1 - text_height - 4, x1 + text_width + 4, y1], fill=color)
|
||||
draw.text((x1 + 2, y1 - text_height - 2), label_text, fill='white', font=font)
|
||||
|
||||
# Draw points
|
||||
if points:
|
||||
point_radius = max(3, int(min(width, height) * 0.01))
|
||||
for px, py in points:
|
||||
x = int(px * width)
|
||||
y = int(py * height)
|
||||
# Draw point as a circle
|
||||
draw.ellipse([x - point_radius, y - point_radius, x + point_radius, y + point_radius],
|
||||
fill='#FF0000', outline='#FFFFFF', width=2)
|
||||
|
||||
return annotated
|
||||
@@ -4,15 +4,13 @@ from modules.interrogate import openclip
|
||||
|
||||
|
||||
def vlm_caption_wrapper(question, system_prompt, prompt, image, model_name, prefill, thinking_mode):
|
||||
"""Wrapper to handle tuple returns from vqa.interrogate with annotated images."""
|
||||
"""Wrapper for vqa.interrogate that handles annotated image display."""
|
||||
from modules.interrogate import vqa
|
||||
result = vqa.interrogate(question, system_prompt, prompt, image, model_name, prefill, thinking_mode)
|
||||
if isinstance(result, tuple):
|
||||
text, annotated_image = result
|
||||
if annotated_image is not None:
|
||||
return text, gr.update(value=annotated_image, visible=True)
|
||||
return text, gr.update(visible=False)
|
||||
return result, gr.update(visible=False)
|
||||
answer = vqa.interrogate(question, system_prompt, prompt, image, model_name, prefill, thinking_mode)
|
||||
annotated_image = vqa.get_last_annotated_image()
|
||||
if annotated_image is not None:
|
||||
return answer, gr.update(value=annotated_image, visible=True)
|
||||
return answer, gr.update(visible=False)
|
||||
|
||||
|
||||
def update_vlm_prompts_for_model(model_name):
|
||||
|
||||
Reference in New Issue
Block a user