matplot patchcollection(s) moving and rotating, all together and each seprately, what is best?
what is best practice, professional software program to user can move rotate all patchcollection together/ each seprately? to fit patch(s) to ROI get array data from an image? any tiny advice is highly appreciated. below code has 4 frame like patchcollection and 1 center patchcollection. can be moved and rotated
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
from matplotlib.collections import PatchCollection
from matplotlib import transforms
import numpy as np
import time # For calculating draw and execution speed
import os # For reading process memory usage
# ---- Global toggle variable ----
toggle_mode = 0 # 0 = single, 1 = all
# ---- Function to create collections ----
def create_circle_collection(center_x, center_y, radius, rows, cols, spacing,
color, gid):
patches = []
for i in range(rows):
for j in range(cols):
x = center_x + j * spacing
y = center_y + i * spacing
circle = Circle((x, y), radius)
patches.append(circle)
center_dot = Circle((x, y), radius=0.03)
patches.append(center_dot)
collection = PatchCollection(patches,
facecolor=color,
edgecolor='black',
alpha=0.6)
collection.set_gid(gid)
return collection, patches
# ---- Setup Figure ----
fig, ax = plt.subplots(figsize=(8, 6))
ax.set_xlim(0, 20)
ax.set_ylim(0, 20)
ax.grid(True, linestyle='--', color='gray', alpha=0.7)
collections = []
patches_list = []
rotation_points = {} # collection -> fixed rotation point
all_rotation_point = None # for ALL mode
collection_transforms = {}
params = [(7, 7, 0.5, 1, 7, 1.0, 'skyblue', "Group1"),
(7, 8, 0.5, 5, 1, 1.0, 'lightcoral', "Group2"),
(8, 8, 0.5, 5, 5, 1.0, 'green', "Group3"),
(13, 8, 0.5, 5, 1, 1.0, 'lightcoral', "Group4"),
(7, 13, 0.5, 1, 7, 1.0, 'skyblue', "Group5")]
for cx, cy, r, rows, cols, spacing, color, gid in params:
coll, patches = create_circle_collection(cx, cy, r, rows, cols, spacing,
color, gid)
ax.add_collection(coll)
collections.append(coll)
patches_list.append(patches)
collection_transforms[coll] = transforms.Affine2D()
rotation_points[coll] = [patches[0].center[0], patches[0].center[1]]
first_collection = collections[0]
all_rotation_point = list(rotation_points[first_collection])
# Rotation tracking
collection_angles = {coll: 0 for coll in collections}
# Dragging state
dragging = {'collection': None, 'start_pos': (0, 0)}
def rotate_collection(collection, angle_deg, pivot):
cx, cy = pivot
# Apply rotation incrementally to the existing Affine2D transform
collection_transforms[collection].rotate_deg_around(cx, cy, angle_deg)
# Update the collection's transform for drawing
collection.set_transform(collection_transforms[collection] + ax.transData)
# Track total rotation angle for this collection
collection_angles[collection] = (collection_angles.get(collection, 0) +
angle_deg) % 360
print(
f"Transform Layer Log: Rotated {collection.get_gid()} around pivot ({cx:.2f}, {cy:.2f}) by {collection_angles[collection]:.2f}°"
)
# ---- Event Handlers ----
def on_press(event):
if event.inaxes != ax:
return
global all_rotation_point
for coll in collections:
contains, _ = coll.contains(event)
if contains:
if toggle_mode == 1:
dragging['collection'] = 'ALL'
dragging['start_pos'] = (event.xdata, event.ydata)
print(
f"---All pressed center: {all_rotation_point[0] + params[0][2]:.1f}, {all_rotation_point[1] - params[0][2]:.1f}"
)
return
dragging['collection'] = coll
dragging['start_pos'] = (event.xdata, event.ydata)
idx = collections.index(coll)
patch = patches_list[idx][0]
x_new, y_new = collection_transforms[coll].transform(patch.center)
print(
f"{coll.get_gid()} Pressed center: {x_new:.1f}, {y_new:.1f} ")
def on_release(event):
coll = dragging['collection']
if coll is None:
return # nothing being dragged
if coll == 'ALL':
print(
f"---All released center: {all_rotation_point[0]:.1f}, {all_rotation_point[1]:.1f}"
)
else:
idx = collections.index(coll)
patch = patches_list[idx][0]
x_new, y_new = collection_transforms[coll].transform(patch.center)
print(
f"---Released center of {coll.get_gid()}: {x_new:.1f}, {y_new:.1f}"
)
dragging['collection'] = None
def on_motion(event):
if dragging['collection'] is None or event.inaxes != ax:
return
# START PERFORMANCE SPEED TRACKING
start_time = time.perf_counter()
dx = event.xdata - dragging['start_pos'][0]
dy = event.ydata - dragging['start_pos'][1]
# --- RULE 1: Dragging ALL patch collections ---
# Update global pivot and all local pivots according to dx, dy
if dragging['collection'] == 'ALL':
target_collections = collections
if all_rotation_point is not None:
all_rotation_point[0] += dx
all_rotation_point[1] += dy
for coll in collections:
rotation_points[coll][0] += dx
rotation_points[coll][1] += dy
else:
# --- RULE 2: Dragging SINGLE patch collection ---
# Update only that collection's local pivot.
# If it is the global pivot owner (first_collection), also update global pivot.
target_collections = [dragging['collection']]
active_coll = dragging['collection']
rotation_points[active_coll][0] += dx
rotation_points[active_coll][1] += dy
if active_coll == first_collection:
all_rotation_point[0] += dx
all_rotation_point[1] += dy
new_rotation = ""
for coll in target_collections:
# Mutate transform matrix layers directly
collection_transforms[coll].translate(dx, dy)
coll.set_transform(collection_transforms[coll] + ax.transData)
x, y = rotation_points[coll]
new_rotation = f"{new_rotation}{coll.get_gid()} => ({x:.2f}, {y:.2f}). "
print(new_rotation)
dragging['start_pos'] = (event.xdata, event.ydata)
fig.canvas.draw_idle()
# LOG PERFORMANCE SPEED & MEMORY
execution_time_ms = (time.perf_counter() - start_time) * 1000
try:
import psutil
process_memory_mb = psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024)
except ImportError:
# Fallback if psutil library isn't installed
process_memory_mb = 0.0
print(f" [PERF LOG] Motion Event Process Speed: {execution_time_ms:.2f} ms | App RAM Usage: {process_memory_mb:.1f} MB")
def on_key_press(event):
global toggle_mode, all_rotation_point, rotation_points
coll = dragging['collection']
if event.key in ['ctrl+a', 'ctrl+A']:
print("on_key_press")
toggle_mode = 1 - toggle_mode
print(
f"Toggle mode: {'ALL GROUPS' if toggle_mode else 'SINGLE GROUP'}")
return
if toggle_mode == 1:
target_collections = collections
elif coll is not None and coll != 'ALL':
target_collections = [coll]
else:
return
if event.key in ['ctrl+r', 'alt+r']:
# START KEY ROTATION PERFORMANCE SPEED TRACKING
start_time = time.perf_counter()
angle = 10 if event.key == 'ctrl+r' else -10
if toggle_mode == 1:
# --- RULE 3: Rotation ALL patch collections ---
# Update only local pivots (to track first-circle visually), do not touch global pivot.
for coll in target_collections:
rotate_collection(coll, angle, all_rotation_point)
# Recompute local pivot post-rotation from the first circle's live center
first_circle = patches_list[collections.index(coll)][0]
rotation_points[coll] = list(collection_transforms[coll].transform(first_circle.center))
else:
# --- RULE 4: Rotation SINGLE patch collection ---
# Do not update any pivot. Pivot remains fixed.
for coll in target_collections:
rotate_collection(coll, angle, rotation_points[coll])
fig.canvas.draw_idle()
# LOG ROTATION RENDER SPEED
execution_time_ms = (time.perf_counter() - start_time) * 1000
print(f" [PERF LOG] Rotation Event Render Speed: {execution_time_ms:.2f} ms")
# ---- Performance Monitor for Core Matplotlib Renderer Engine ----
def on_draw_perf_log(event):
"""Prints exactly how long Matplotlib takes to redraw the pixel canvas from scratch."""
print(f" [MATPLOTLIB CANVAS DRAW] Full Screen Render Pipeline complete.")
fig.canvas.mpl_connect('draw_event', on_draw_perf_log)
# ---- Connect Handlers & Render Loop ----
fig.canvas.mpl_connect('button_press_event', on_press)
fig.canvas.mpl_connect('button_release_event', on_release)
fig.canvas.mpl_connect('motion_notify_event', on_motion)
fig.canvas.mpl_connect('key_press_event', on_key_press)
plt.show()