193 lines
7.3 KiB
Python
193 lines
7.3 KiB
Python
|
|
"""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())
|