Spaces:
Sleeping
Sleeping
File size: 10,670 Bytes
0d8d0b4 a3feb0e 0d8d0b4 a3feb0e 0d8d0b4 a3feb0e 0d8d0b4 a3feb0e 0d8d0b4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | import cv2
import imagesize
import itertools
import templates
import numpy as np
from pathlib import Path
from utils import mask_to_svg
from jinja2 import Environment, FileSystemLoader
# def get_center_point(mask):
# M = cv2.moments(mask)
# if M["m00"] == 0:
# return None # empty mask
# cx = M["m10"] / M["m00"]
# cy = M["m01"] / M["m00"]
# return {"x": cx, "y": cy}
def get_area(mask):
return int(np.sum(mask > 0))
from scipy.ndimage import binary_erosion
def mask_border(mask: np.ndarray) -> np.ndarray:
"""Return border pixels of a binary mask."""
eroded = binary_erosion(mask, structure=np.ones((3, 3)))
return mask & ~eroded
def get_left_point(mask):
border = mask_border(mask)
ys, xs = np.where(border > 0)
if xs.size == 0:
return None
min_x = xs.min()
max_x = xs.max()
offset = int((max_x - min_x) // 20) # dynamic offset by mask width
# select left-ish border pixels
candidate_mask = xs <= min_x + offset
xs = xs[candidate_mask]
ys = ys[candidate_mask]
if xs.size == 0:
return None
# desired vertical middle
target_y = (ys.min() + ys.max()) / 2
# find the candidate whose y is closest to target
idx = np.argmin(np.abs(ys - target_y))
chosen_x = xs[idx]
chosen_y = ys[idx]
return {"x": int(chosen_x), "y": int(chosen_y)}
def get_right_point(mask):
border = mask_border(mask)
ys, xs = np.where(border > 0)
if xs.size == 0:
return None
min_x = xs.min()
max_x = xs.max()
offset = int((max_x - min_x) // 20) # dynamic offset by mask width
# select left-ish border pixels
candidate_mask = xs >= max_x - offset
xs = xs[candidate_mask]
ys = ys[candidate_mask]
if xs.size == 0:
return None
# desired vertical middle
target_y = (ys.min() + ys.max()) / 2
# find the candidate whose y is closest to target
idx = np.argmin(np.abs(ys - target_y))
chosen_x = xs[idx]
chosen_y = ys[idx]
return {"x": int(chosen_x), "y": int(chosen_y)}
def prepare_data(data):
"""
- Split left-sorted masks into two left and right whose total content lengths are approximately balanced.
Returns:
left_data, right_data
"""
# Build formated_data and sort by "left"
formated_data = []
for roi in data["explain"]:
heading = roi["roi"].title()
content = roi["reason"]
mask = roi["mask"]
# protect
mask = mask if mask.any() else np.ones_like(mask, dtype=mask.dtype)
formated_data.append({
"heading": heading,
"content": content,
"mask": mask,
"area": get_area(mask),
# "center": get_center_point(mask),
"left": get_left_point(mask),
"right": get_right_point(mask),
})
# formated_data.sort(key=lambda x: x["center"]["x"])
formated_data.sort(key=lambda x: x["area"], reverse=True)
# --- split logic ---
lengths = [len(b["content"]) + 60 for b in formated_data]
total_chars = sum(lengths)
half_chars = total_chars / 2
forward_cumsum = [0] + list(itertools.accumulate(lengths))
backward_cumsum = list(itertools.accumulate(reversed(lengths)))[::-1] + [0]
cut_idx = None
min_diff = float("inf")
for i, (left_sum, right_sum) in enumerate(zip(forward_cumsum, backward_cumsum)):
diff = abs(right_sum - left_sum)
if diff < min_diff:
min_diff = diff
cut_idx = i
left_data = formated_data[:cut_idx]
right_data = formated_data[cut_idx:]
left_data.sort(key=lambda x: x["left"]["y"])
right_data.sort(key=lambda x: x["right"]["y"])
return left_data, right_data
def hover_on_other(i, n, target_type, types=("svg", "tbox", "connector"), subtype=None):
"""
i: index of the element being hovered
n: total number of elements
target_type: type of element to affect (e.g., 'connector', 'svg', 'tbox')
subtype: optional CSS subtype (e.g., ':before', '> span')
"""
selectors = []
for t in types: # hover source types
for j in range(n):
if j != i: # skip self
a = f"#{t}{i}"
b = f"#{target_type}{j}{'' if subtype is None else subtype}"
selectors.append(templates.diagram_html.hover_a_set_b.render(a=a, b=b))
return ", ".join(selectors)
def hover_on_self(i, target_type, types=("svg", "tbox", "connector"), subtype=None):
"""
Logic:
For each type in types (except target_type), generate:
body:has(#<type><i>:hover) #<target_type><i>{subtype}
"""
selectors = []
for t in types:
if t != target_type: # only other types with same index
a = f"#{t}{i}"
b = f"#{target_type}{i}{'' if subtype is None else subtype}"
selectors.append(templates.diagram_html.hover_a_set_b.render(a=a, b=b))
return ", ".join(selectors)
import base64
def to_b64(path):
with open(path, "rb") as f:
return "data:image/png;base64," + base64.b64encode(f.read()).decode()
def render_diagram_html(data):
img_width, img_height = imagesize.get(data["image_path"])
env = Environment(loader=FileSystemLoader("templates"))
html = env.get_template("template.html")
css = env.get_template("template.css")
js = env.get_template("script.js")
# Setup
svg_html_ls = []
tbox_left_html_ls = []
tbox_right_html_ls = []
svg_css_ls = []
tbox_css_ls = []
connector_css_ls = []
# Scale and transform masks to match css style and split them into left and right groups
left_data, right_data = prepare_data(data)
import re
def print_abbr(s):
s = re.sub(r'\bd="[^"]*"', 'd="..."', s)
print(s)
n = len(left_data) + len(right_data)
i = 0
for data_point in left_data:
color = templates.COLORS[i%len(templates.COLORS)]
mask = data_point["mask"]
left = f'{data_point["left"]["x"] / img_width :.4f}'
top = f'{data_point["left"]["y"] / img_height :.4f}'
extra_data = f'data-left="{left}" data-top="{top}" data-color="{color}"'
svg = mask_to_svg(mask).format(index=i, extra_data=extra_data, sub_class="", prefix="", hidden_style="", sub_svgs="")
data_point.update({
"i": i,
"side": "left",
"color": color,
"svg_hover_on_self": hover_on_self(i, "svg"),
"svg_hover_on_self_area": hover_on_self(i, "svg", subtype=" .level0"),
"svg_hover_on_self_stroke": ", ".join([hover_on_self(i, "svg", subtype=" .outer"), hover_on_self(i, "svg", subtype=" .inner")]),
"svg_hover_on_self_bg": hover_on_self(i, "svg", subtype=" .bg"),
"svg_hover_on_other": hover_on_other(i, n, "svg"),
"tbox_hover_on_self": hover_on_self(i, "tbox", subtype="::before"),
"connector_hover_on_self": hover_on_self(i, "connector"),
"connector_hover_on_other": hover_on_other(i, n, "connector"),
"connector_hover_on_self_line": hover_on_self(i, "connector", subtype=" line"),
"connector_hover_on_other_line": hover_on_other(i, n, "connector", subtype=" line"),
})
svg_html_ls.append(svg.strip("\n"))
svg_css_ls.append(templates.diagram_html.svg_css.render(**data_point).strip("\n"))
tbox_left_html_ls.append(templates.diagram_html.tbox_html.render(**data_point).strip("\n"))
tbox_css_ls.append(templates.diagram_html.tbox_css.render(**data_point).strip("\n"))
connector_css_ls.append(templates.diagram_html.connector_css.render(**data_point).strip("\n"))
i += 1
for data_point in right_data:
color = templates.COLORS[i%len(templates.COLORS)]
mask = data_point["mask"]
left = f'{data_point["right"]["x"] / img_width :.4f}'
top = f'{data_point["right"]["y"] / img_height :.4f}'
extra_data = f'data-left="{left}" data-top="{top}" data-color="{color}"'
svg = mask_to_svg(mask).format(index=i, extra_data=extra_data, sub_class="", prefix="", hidden_style="", sub_svgs="")
data_point.update({
"i": i,
"side": "right",
"color": color,
"svg_hover_on_self": hover_on_self(i, "svg"),
"svg_hover_on_self_area": hover_on_self(i, "svg", subtype=" .level0"),
"svg_hover_on_self_stroke": ", ".join([hover_on_self(i, "svg", subtype=" .outer"), hover_on_self(i, "svg", subtype=" .inner")]),
"svg_hover_on_self_bg": hover_on_self(i, "svg", subtype=" .bg"),
"svg_hover_on_other": hover_on_other(i, n, "svg"),
"tbox_hover_on_self": hover_on_self(i, "tbox", subtype="::before"),
"connector_hover_on_self": hover_on_self(i, "connector"),
"connector_hover_on_other": hover_on_other(i, n, "connector"),
"connector_hover_on_self_line": hover_on_self(i, "connector", subtype=" line"),
"connector_hover_on_other_line": hover_on_other(i, n, "connector", subtype=" line"),
})
svg_html_ls.append(svg.strip("\n"))
svg_css_ls.append(templates.diagram_html.svg_css.render(**data_point).strip("\n"))
tbox_right_html_ls.append(templates.diagram_html.tbox_html.render(**data_point).strip("\n"))
tbox_css_ls.append(templates.diagram_html.tbox_css.render(**data_point).strip("\n"))
connector_css_ls.append(templates.diagram_html.connector_css.render(**data_point).strip("\n"))
i += 1
css_content = css.render(
svgs = "\n".join(svg_css_ls),
tboxs = "\n".join(tbox_css_ls),
connectors = "\n".join(connector_css_ls),
)
js_content = js.render()
html_content = html.render(
stylesheet_content = css_content,
javascript_content = js_content,
image_path = to_b64(data["image_path"]),
svgs = "\n".join(svg_html_ls),
tboxs_left = "\n".join(tbox_left_html_ls),
tboxs_right = "\n".join(tbox_right_html_ls),
)
return html_content
import lorem
def random_mask(width, height, min_size=10):
# empty mask
mask = np.zeros((height, width), dtype=np.uint8)
# random number of circles
n_circles = np.random.randint(1, 15)
# maximum radius based on sqrt(width*height)
max_radius = int((width * height) ** 0.5 * 0.3)
for _ in range(n_circles):
# random center inside the image
cx = np.random.randint(0, width - 1)
cy = np.random.randint(0, height - 1)
# random radius within bounds
radius = np.random.randint(min_size, max_radius)
# draw circle on mask
cv2.circle(mask, (cx, cy), radius, 255, -1) # -1 fills the circle
return mask
def random_data(image_path):
np.random.seed(4)
n_boxes = 4 #np.random.randint(1, 10)
data = {
"image_id": 0,
"image_path": image_path,
"explain": [{
"roi": lorem.sentence()[:np.random.randint(3, 30)],
"mask": random_mask(*imagesize.get(image_path)),
"reason": " ".join([lorem.sentence() for i in range(np.random.randint(1, 3))])
} for i in range(n_boxes)]
}
print(f"Randomly generted {n_boxes} boxes.")
return data
# image_path = "tmp/img2.jpg"
# data = random_data(image_path)
# render_diagram_html(data)
|