Files

193 lines
7.3 KiB
Python
Raw Permalink Normal View History

"""Teach an ROI once (human draws), persist it as a .hobj for headless reuse.
The rectangle case (default, the common "box a Mark" flow) uses an interactive
matplotlib viewer with **pan (right-drag), zoom (scroll wheel), and box-select
(left-drag)** — smooth even on multi-megapixel images. circle/region fall back to
HALCON's native draw_* window. Downstream procedures read_region + reduce_domain
the saved .hobj, so no human is needed after teaching.
Run (needs a visible desktop — this is the interactive seam):
/mnt/c/Users/NAURA/.local/bin/uv.exe run python \
.claude/skills/halcon/scripts/teach_roi.py \
--image-file "C:/data/frames/a.bmp" \
--out C:/workspace/agent-studio/halcon-001/roi.hobj
Controls (rectangle): left-drag = box · right-drag = pan · wheel = zoom ·
Enter = save · q/Esc = cancel.
Headless (no GUI, scripted): add --box r1,c1,r2,c2 to write a rectangle directly.
Requires matplotlib + numpy in the venv (one-time):
/mnt/c/Users/NAURA/.local/bin/uv.exe pip install matplotlib numpy
Tip: draw TIGHTLY around the target features. A loose ROI captures extra
contours and breaks downstream count checks (e.g. "expected 16 circles").
"""
import argparse
import sys
def pick_reference_image(ha, image_dir, image_file):
if image_file:
return image_file
files = ha.list_files(image_dir, "files")
imgs = sorted(ha.tuple_regexp_select(files, r"\.(png|bmp|jpg|jpeg|tif|tiff)$"))
if not imgs:
raise SystemExit(f"No images found in {image_dir}")
return imgs[0]
def load_gray(ha, image_file):
# HALCON 默认 clip_region=true 会把区域裁到 128x128,全分辨率坐标建的 region
# 会被裁空 -> 写盘回读 bbox=0,0,0,0。建/读区域前务必关掉。
ha.set_system("clip_region", "false")
img = ha.read_image(image_file)
if ha.count_channels(img)[0] >= 3:
img = ha.rgb1_to_gray(img)
w, h = ha.get_image_size(img)
return ha.himage_as_numpy_array(img), int(h[0]), int(w[0])
def pick_rectangle_interactive(arr, max_disp):
"""matplotlib pan/zoom/box-select; returns (r1,c1,r2,c2) in image coords or None."""
import matplotlib.pyplot as plt
from matplotlib.widgets import RectangleSelector
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
# Downsample for display (fast redraws), map coords back to full res via extent.
H, W = arr.shape
step = max(1, round(max(H, W) / max_disp))
disp = arr[::step, ::step]
fig, ax = plt.subplots()
ax.imshow(disp, cmap="gray", origin="upper", extent=(0, W, H, 0),
interpolation="nearest", aspect="equal")
ax.set_xlim(0, W); ax.set_ylim(H, 0)
ax.set_title("left=box right-drag=pan wheel=zoom Enter=save q=cancel")
box, pan = {}, {}
def on_select(p, r):
box.update(r1=p.ydata, c1=p.xdata, r2=r.ydata, c2=r.xdata)
# keep the reference or RectangleSelector is GC'd and box-select stops working
ax._roi_selector = RectangleSelector(
ax, on_select, useblit=True, button=[1], minspanx=3, minspany=3,
interactive=True)
def on_scroll(ev):
if ev.inaxes is not ax:
return
s = 0.8 if ev.button == "up" else 1.25
x0, x1 = ax.get_xlim(); y0, y1 = ax.get_ylim()
ax.set_xlim(ev.xdata - (ev.xdata - x0) * s, ev.xdata + (x1 - ev.xdata) * s)
ax.set_ylim(ev.ydata - (ev.ydata - y0) * s, ev.ydata + (y1 - ev.ydata) * s)
fig.canvas.draw_idle()
def on_press(ev):
if ev.button == 3 and ev.inaxes is ax:
pan["p"] = (ev.xdata, ev.ydata)
def on_motion(ev):
if "p" in pan and ev.xdata is not None and ev.inaxes is ax:
dx, dy = ev.xdata - pan["p"][0], ev.ydata - pan["p"][1]
x0, x1 = ax.get_xlim(); y0, y1 = ax.get_ylim()
ax.set_xlim(x0 - dx, x1 - dx); ax.set_ylim(y0 - dy, y1 - dy)
fig.canvas.draw_idle()
def on_release(ev):
if ev.button == 3:
pan.pop("p", None)
def on_key(ev):
if ev.key == "enter" and box:
plt.close(fig)
elif ev.key in ("q", "escape"):
box.clear(); plt.close(fig)
for name, fn in [("scroll_event", on_scroll), ("button_press_event", on_press),
("motion_notify_event", on_motion),
("button_release_event", on_release), ("key_press_event", on_key)]:
fig.canvas.mpl_connect(name, fn)
plt.show()
if not box:
return None
return box["r1"], box["c1"], box["r2"], box["c2"]
def save_rectangle(ha, out, r1, c1, r2, c2, H, W):
r1, r2 = sorted((max(0, round(r1)), min(H - 1, round(r2))))
c1, c2 = sorted((max(0, round(c1)), min(W - 1, round(c2))))
ha.write_region(ha.gen_rectangle1(r1, c1, r2, c2), out)
print(f"ROI saved: {out}")
print(f"Rectangle Row1={r1} Col1={c1} Row2={r2} Col2={c2}")
def draw_native(ha, ref, shape, out):
"""circle / region via HALCON's own draw window (no pan/zoom)."""
img = ha.read_image(ref)
width, height = ha.get_image_size(img)
width, height = width[0], height[0]
win = ha.open_window(0, 0, 1024, 768, 0, "visible", "")
ha.set_part(win, 0, 0, height - 1, width - 1)
ha.disp_obj(img, win)
ha.set_color(win, "green")
print(f"Draw a {shape} with the left mouse button, release to finish ...", flush=True)
if shape == "circle":
row, col, radius = ha.draw_circle(win)
region = ha.gen_circle(row, col, radius)
print(f"Circle Row={row:.1f} Col={col:.1f} Radius={radius:.1f}")
else: # freehand region
region = ha.draw_region(win)
ha.write_region(region, out)
ha.close_window(win)
print(f"ROI saved: {out}")
def main() -> int:
ap = argparse.ArgumentParser(description="Teach an ROI and write it to disk")
src = ap.add_mutually_exclusive_group(required=True)
src.add_argument("--image-dir", help="Directory; first sorted image is shown")
src.add_argument("--image-file", help="Explicit reference image to display")
ap.add_argument("--out", required=True, help="Output region file, e.g. .../roi.hobj")
ap.add_argument("--shape", choices=["rectangle", "circle", "region"],
default="rectangle",
help="rectangle = interactive pan/zoom picker; circle/region = HALCON draw")
ap.add_argument("--disp", type=int, default=1000,
help="rectangle picker: display long-side px cap (smaller=faster/blurrier)")
ap.add_argument("--box", help="rectangle headless (no GUI): r1,c1,r2,c2 image coords")
args = ap.parse_args()
try:
import halcon as ha
except Exception as e:
print("ERROR: cannot import 'halcon'. Run via the project venv using uv.",
e, file=sys.stderr)
return 1
ref = pick_reference_image(ha, args.image_dir, args.image_file)
print(f"Reference image: {ref}")
if args.shape != "rectangle":
draw_native(ha, ref, args.shape, args.out)
return 0
arr, H, W = load_gray(ha, ref)
if args.box:
r1, c1, r2, c2 = (float(v) for v in args.box.split(","))
save_rectangle(ha, args.out, r1, c1, r2, c2, H, W)
return 0
rect = pick_rectangle_interactive(arr, args.disp)
if rect is None:
print("cancelled, nothing saved")
return 0
save_rectangle(ha, args.out, *rect, H, W)
return 0
if __name__ == "__main__":
sys.exit(main())