Localized Motion / Activity Detection
Three near-neighbours, all distinct: wait_until_screen_stable returns a boolean
over a live poll loop (not localized boxes on an injectable pair); ssim_changed_regions
is structural (Gaussian-windowed SSIM, illumination-tolerant — it deliberately ignores
the fast pixel motion you want for “where is the spinner / video / animation”);
diff_screenshots highlights pixel diffs but is not framed as activity blobs with a
score. changed_regions / has_motion / activity_score are the cheap absdiff
path: which sub-regions are moving between two frames, so a script can pick a quiet
area or locate a busy spinner.
They operate on two injectable frames (ndarray / path / PIL), so they are headless-
testable on synthetic arrays, and reuse the shared connected-component helper. OpenCV +
NumPy come in via je_open_cv. Imports no PySide6.
Headless API
from je_auto_control import changed_regions, has_motion, activity_score
before = screenshot_to_array()
# ... time passes ...
for box in changed_regions(before, after): # boxes that moved, largest first
print(box["x"], box["y"], box["width"], box["height"])
if has_motion(before, after):
print("still animating, activity =", activity_score(before, after))
changed_regions thresholds the absolute difference (threshold), denoises
(blur), dilates and returns {x, y, width, height, area, center} blobs of at
least min_area, largest first. has_motion is the boolean form; activity_score
is the fraction (0..1) of pixels that moved. Frames of different sizes raise
ValueError.
Executor commands
AC_changed_regions (before / after / threshold / min_area /
blur → {count, regions}) and AC_has_motion (before / after /
threshold / min_area → {moved, activity}); after defaults to a live
screen grab. They are exposed as the MCP tools ac_changed_regions /
ac_has_motion and as Script Builder commands under Image.