FlyBrain-Lab / src /tools /visual.py
timfromhcs's picture
FlyBrain v4.1.0 Space build (REAL_SUBGRAPH, CPU-only, honest backend)
3d46076 verified
Raw
History Blame Contribute Delete
3.75 kB
import os
import numpy as np
from PIL import Image
from typing import Dict, Any, Optional
from src.tools.base import ToolConnector
class ObserveVisualConnector(ToolConnector):
def __init__(self):
super().__init__(
name="observe_visual",
description="Observes and extracts 64-dimensional biophysical visual features from an image file or synthetic visual field.",
timeout_sec=10.0
)
@property
def input_schema(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"image_path": {"type": "string"},
"synthetic_target": {"type": "string"}
}
}
@property
def output_schema(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"features_vector": {"type": "array", "items": {"type": "number"}},
"mean_brightness": {"type": "number"},
"contrast": {"type": "number"},
"dominant_channel": {"type": "string"}
},
"required": ["features_vector", "mean_brightness", "contrast", "dominant_channel"]
}
def _execute(self, params: Dict[str, Any], execution_id: str) -> Dict[str, Any]:
img_path = params.get("image_path")
if img_path and os.path.exists(img_path):
img = Image.open(img_path).convert("RGB").resize((64, 64))
arr = np.array(img, dtype=np.float32) / 255.0
else:
# Generate synthetic sensory visual field based on synthetic_target
target = params.get("synthetic_target", "neutral")
arr = np.zeros((64, 64, 3), dtype=np.float32)
if "red" in target:
arr[:, :, 0] = 0.8
elif "green" in target:
arr[:, :, 1] = 0.8
elif "blue" in target:
arr[:, :, 2] = 0.8
elif "bright" in target:
arr[:] = 0.9
else:
# Gradient field
for y in range(64):
for x in range(64):
arr[y, x, 0] = x / 64.0
arr[y, x, 1] = y / 64.0
arr[y, x, 2] = 0.5
# Extract 64-d feature vector
# 16 values: 4x4 spatial grid of mean luminance
grid = arr.mean(axis=2).reshape(4, 16, 4, 16).mean(axis=(1, 3)).flatten()
# 16 values: color distribution histograms
hist_r, _ = np.histogram(arr[:, :, 0], bins=8, range=(0, 1))
hist_g, _ = np.histogram(arr[:, :, 1], bins=8, range=(0, 1))
# 16 values: horizontal gradients
grad_x = np.abs(np.diff(arr.mean(axis=2), axis=1))
grad_pool_x = grad_x.reshape(4, 16, 63).mean(axis=(1, 2))
grad_pool_pad = np.pad(grad_pool_x, (0, 12), mode="edge")
# 16 values: vertical gradients
grad_y = np.abs(np.diff(arr.mean(axis=2), axis=0))
grad_pool_y = grad_y.reshape(63, 4, 16).mean(axis=(0, 2))
grad_pool_y_pad = np.pad(grad_pool_y, (0, 12), mode="edge")
feat = np.concatenate([grid, hist_r / 4096.0, hist_g / 4096.0, grad_pool_pad[:16], grad_pool_y_pad[:16]])
feat = feat[:64].astype(np.float32)
mean_b = float(arr.mean())
contrast = float(arr.std())
channel_means = [arr[:, :, 0].mean(), arr[:, :, 1].mean(), arr[:, :, 2].mean()]
dom_idx = int(np.argmax(channel_means))
dominant_channel = ["red", "green", "blue"][dom_idx]
return {
"features_vector": feat.tolist(),
"mean_brightness": round(mean_b, 4),
"contrast": round(contrast, 4),
"dominant_channel": dominant_channel
}