inital commit, add lab1

This commit is contained in:
2026-01-26 22:36:29 -05:00
commit 4316bc80a9
24 changed files with 1098 additions and 0 deletions

608
lab1/ibezier.py Normal file
View File

@@ -0,0 +1,608 @@
# Interactive plot of Bezier interpolation.
# Interactively set polyline vertices or read vertices from a file.
# Buttons to subdivide polygonal line.
# Button to save vertices (points) to a file.
# Created: 1/24/2026 - R. Wenger
# Edited: 1/26/2026 - Zhe Yuan
import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button, TextBox
def ibezier(inputA):
# Global variables
global vX, vY, plot_center, text_message, fig, ax, save_message # fix: fig should be global
global jv, iv_selected, numv
global default_output_filename, filename_textbox
global cid_bpress, cid_pick
global save_message_xloc
global num_sample
global curve_degree, is_subdivided # add for bezier
# Compute a point on the Bezier curve at parameter t using de Casteljau's algorithm.
# points: list of tuples/lists [(x0,y0), (x1,y1), ...]
# Returns: (x, y) coordinates on the curve
def deCasteljau(points, t):
current_points = np.array(points)
n = len(current_points) - 1 # degree
# Iterate r from 1 to n
for r in range(1, n + 1):
# Calculate next level of points
# P_i^r = (1-t)*P_i^{r-1} + t*P_{i+1}^{r-1}
new_points = []
for i in range(len(current_points) - 1):
x = (1 - t) * current_points[i][0] + t * current_points[i+1][0]
y = (1 - t) * current_points[i][1] + t * current_points[i+1][1]
new_points.append([x, y])
current_points = np.array(new_points)
return (current_points[0][0], current_points[0][1])
# Perform subdivision at t=0.5
# Input: List of control points for ONE Bezier curve segment.
# Output: A tuple (left_points, right_points) where they share the middle point.
def subdivide_points_algorithm(segment_x, segment_y):
# Combine to (x,y)
points = list(zip(segment_x, segment_y))
n = len(points) - 1
# structure: level 0 has n+1 points, level n has 1 point
levels = [points]
# Run de Casteljau for t=0.5 and store all intermediate points
current_points = points
for r in range(1, n + 1):
next_level = []
for i in range(len(current_points) - 1):
px = 0.5 * current_points[i][0] + 0.5 * current_points[i+1][0]
py = 0.5 * current_points[i][1] + 0.5 * current_points[i+1][1]
next_level.append((px, py))
levels.append(next_level)
current_points = next_level
# Left curve control points are the first point of each level (P0^0, P0^1, ..., P0^n)
left_x = [level[0][0] for level in levels]
left_y = [level[0][1] for level in levels]
# Right curve cpts are the last point of each level, reversed (P0^n, P1^{n-1}, ..., Pn^0)
right_x = [level[-1][0] for level in levels]
right_y = [level[-1][1] for level in levels]
right_x.reverse()
right_y.reverse()
return (left_x, left_y), (right_x, right_y)
# Draw the Bezier curve
def drawBezierCurve(vX, vY):
global num_sample, curve_degree
# Number of points per segment = degree + 1
nump = len(vX)
num_segments = (nump - 1) // curve_degree
for k in range(num_segments):
start_idx = k * curve_degree
end_idx = start_idx + curve_degree + 1
segX = vX[start_idx:end_idx]
segY = vY[start_idx:end_idx]
points = list(zip(segX, segY))
pX = []
pY = []
steps = np.linspace(0, 1, num_sample)
for t in steps:
(cx, cy) = deCasteljau(points, t)
pX.append(cx)
pY.append(cy)
# Draw Bezier curve
plt.plot(pX, pY, color="red", linewidth=1.5)
# Draw control polygon
plt.plot(vX, vY, '--', color='gray', linewidth=0.5)
# Draw control points
plt.plot(vX, vY, 'ob', picker=True, pickradius=5)
return
# Redraw the entire plot
def redrawPlot(vX, vY):
global text_message, ax
plt.subplot(111)
# Store current axes limits
xL, xR = ax.get_xlim()
yB, yT = ax.get_ylim()
# Bug: Clearing the axes resets the navigation toolbar so you can't return
# to previous states.
# In particular, you can't return to the starting (0,0), (1,1) configuration.
plt.cla()
text_message=None
# Restore axes limits
plt.xlim([xL,xR])
plt.ylim([yB,yT])
drawBezierCurve(vX, vY)
outputPlotMessage("Drag and move control points")
clearSaveMessage()
plt.draw()
return
# Output a message above the plot
def outputPlotMessage(s):
global text_message, ax
text_yloc=1.02
if text_message is None:
text_message=plt.text(0.0, text_yloc, s, transform=ax.transAxes)
else:
text_message.set_text(s)
return
# Output the message: Select vertex str(j) location
def outputSelectVertexLocation(j):
s = "Select vertex " + str(j) + " location"
outputPlotMessage(s)
return
# Set a message related to saving control points to file
def outputSaveMessage(s):
global save_message, save_message_xloc
text_yloc=1.02
if save_message is not None:
save_message.remove()
save_message=plt.text(save_message_xloc-0.14, text_yloc, s, transform=ax.transAxes)
return
# Set the save message to ''
def clearSaveMessage():
global save_message
if save_message is not None:
save_message.remove()
save_message = None
return
# CallbackS
# Subdivide curve
def subdivideCallback(event):
global vX, vY, curve_degree, is_subdivided, smooth_button
if len(vX) == 0: return
tempX = []
tempY = []
# We must subdivide EVERY segment in the current curve
num_segments = (len(vX) - 1) // curve_degree
for k in range(num_segments):
start_idx = k * curve_degree
end_idx = start_idx + curve_degree + 1
segX = vX[start_idx:end_idx]
segY = vY[start_idx:end_idx]
# Get divided points
(lx, ly), (rx, ry) = subdivide_points_algorithm(segX, segY)
if k == 0:
tempX.extend(lx[:-1]) # Add all Left except last (shared)
tempX.append(lx[-1]) # Add shared point
tempX.extend(rx[1:]) # Add all Right except first (shared)
tempY.extend(ly[:-1])
tempY.append(ly[-1])
tempY.extend(ry[1:])
else:
# For subsequent segments, the starting point is already in the list
# lx[0] should equal tempX[-1]
tempX.extend(lx[1:-1]) # Skip first, add middle
tempX.append(lx[-1]) # Add shared middle
tempX.extend(rx[1:]) # Skip first (shared), add rest
tempY.extend(ly[1:-1])
tempY.append(ly[-1])
tempY.extend(ry[1:])
vX = tempX
vY = tempY
# Set flag to enable Smooth button
is_subdivided = True
# update bottom to distinguish enabled or not
smooth_button.color = 'white'
smooth_button.hovercolor = 'green'
redrawPlot(vX, vY)
return
# Smooth button callback
def smoothCallback(event):
global vX, vY, curve_degree, is_subdivided
if not is_subdivided:
outputPlotMessage("Curve must be subdivided first.")
plt.draw()
return
# Iterate through join points.
nump = len(vX)
changes = {} # Map index to new (x,y)
for i in range(1, nump - 1):
if i % curve_degree == 0:
# This is a junction point between two segments
# q_i = (q_{i-1} + q_{i+1}) / 2
nx = (vX[i-1] + vX[i+1]) / 2.0
ny = (vY[i-1] + vY[i+1]) / 2.0
changes[i] = (nx, ny)
# Apply changes
for i, (nx, ny) in changes.items():
vX[i] = nx
vY[i] = ny
redrawPlot(vX, vY)
outputPlotMessage("Curve smoothed at junctions.")
# Don't disable smooth button again
plt.draw()
return
# Save control points to file
def saveCallback(event):
global vX, vY, filename_textbox, curve_degree
if len(vX) == 0: return
filename = filename_textbox.text
if (filename == '' or filename.isspace()):
# Should pop up an error window/message, but the GUI code would become
# even more complicated.
outputSaveMessage('Illegal filename. Reset filename to default. No file saved.')
filename_textbox.set_val(default_output_filename)
plt.draw()
else:
# Prepare data for BEZIER format
# Header:
# BEZIER
# # comments
# {ndim} {nump} {ndegree}
# coords...
try:
with open(filename, 'w') as f:
f.write("BEZIER\n")
f.write(f"# Saved from ibezier.py\n")
# ndim=2, nump=len(vX), ndegree=curve_degree
f.write(f"2 {len(vX)} {curve_degree}\n")
for i in range(len(vX)):
f.write(f"{vX[i]} {vY[i]}\n")
outputSaveMessage('Saved to ' + filename)
except Exception as e:
outputSaveMessage('Error saving file.')
print(e)
plt.draw()
return
# Create TextBox containing output filename and
# output message text box
def createSaveTextBoxes(default_filename):
global filename_textbox, save_message_xloc, fig
# *** DEBUG ***
print("Creating save text box.")
ax_filename_textbox = fig.add_axes([save_message_xloc, 0.94, 0.3, 0.03])
filename_textbox = TextBox(ax_filename_textbox, 'Output filename:', default_filename)
plt.draw()
return
# Create buttons
def createButtons(left_pos, button_width, button_height):
global ax_subdivide_button, subdivide_button
global ax_smooth_button, smooth_button
global ax_save_button, save_button
# Colors
inactive_color = 'lightgray'
# Subdivide
ax_subdivide_button = plt.axes([left_pos, 0.8, button_width, button_height])
subdivide_button = Button(ax_subdivide_button, "Subdivide")
subdivide_button.on_clicked(subdivideCallback)
subdivide_button.color = inactive_color
subdivide_button.hovercolor = inactive_color
# Smooth
ax_smooth_button = plt.axes([left_pos, 0.7, button_width, button_height])
smooth_button = Button(ax_smooth_button, "Smooth")
smooth_button.on_clicked(smoothCallback)
smooth_button.color = inactive_color
smooth_button.hovercolor = inactive_color
# Save
ax_save_button = plt.axes([left_pos, 0.6, button_width, button_height])
save_button = Button(ax_save_button, "Save")
save_button.on_clicked(saveCallback)
save_button.color = inactive_color
save_button.hovercolor = inactive_color
createSaveTextBoxes(default_output_filename)
return
# not enable all buttons as smooth needs subdivision first
def enableButtons():
global subdivide_button, smooth_button, save_button
subdivide_button.color = 'white'
subdivide_button.hovercolor = 'green'
save_button.color = 'white'
save_button.hovercolor = 'green'
plt.draw()
return
# Add a vertex at (event.xdata, event.ydata)
def addVertexCallback(event):
global jv, vX, vY, numv, cid_bpress, fig, curve_degree
plt.subplot(111)
x=event.xdata
y=event.ydata
# Check that x and y are defined
if (x is None) or (y is None): return
# get (again) plot lower left (LL) and upper right (UR)
# (Coordinates can change if window is moved from one screen
# to another.)
axLL = ax.transData.transform((0,0));
axUR = ax.transData.transform((1,1));
# check that event.x and event.y are within draw region
if ((event.x < axLL[0]) or (event.x > axUR[0])): return
if ((event.y < axLL[1]) or (event.y > axUR[1])): return
if (jv < numv):
vX.append(x)
vY.append(y)
plt.plot(x, y, 'ob')
jv =jv+1
if (jv < numv):
outputSelectVertexLocation(jv)
plt.draw()
else:
# Disconnect button press callback before creating pick event
fig.canvas.mpl_disconnect(cid_bpress)
# Determine degree (n points => degree n-1)
curve_degree = numv - 1
drawBezierEnableButtonsPickEvent(vX, vY)
enableButtons()
return
# Pick and move a vertex
def pickPointCallback(event):
global iv_selected, cid_moveCallback
if (len(event.ind) < 1): return
if (event.ind[0] >= 0) and (event.ind[0] < len(vX)):
iv_selected = event.ind[0]
cid_moveCallback = \
fig.canvas.mpl_connect('button_release_event', moveVertexCallback)
return
# Move selected vertex to location (event.xdata, event.ydata)
def moveVertexCallback(event):
global cid_moveCallback, iv_selected, vX, vY
global points, polyline
plt.subplot(111)
x=event.xdata
y=event.ydata
# Check that x and y are defined
if (x is None) or (y is None): return
# get (again) plot lower left (LL) and upper right (UR)
# (Coordinates can change if window is moved from one screen
# to another.)
axLL = ax.transData.transform((0,0));
axUR = ax.transData.transform((1,1));
# check that event.x and event.y are within draw region
if ((event.x < axLL[0]) or (event.x > axUR[0])): return
if ((event.y < axLL[1]) or (event.y > axUR[1])): return
vX[iv_selected] = x
vY[iv_selected] = y
redrawPlot(vX, vY)
fig.canvas.mpl_disconnect(cid_moveCallback)
# reset iv_selected
iv_selected = -1
return
# After control points are created or read, draw bezier
# curve and enable buttons and pick event
def drawBezierEnableButtonsPickEvent(vX, vY):
global cid_pick, fig
plt.subplot(111)
outputPlotMessage("Select and move vertices")
redrawPlot(vX, vY)
cid_pick = fig.canvas.mpl_connect('pick_event', pickPointCallback)
enableButtons()
return
# load vX and vY from a textfile
# edit for BEZIER format
def loadTextFile(filename):
global vX, vY, jv, numv, curve_degree
try:
with open(filename, 'r') as f:
lines = f.readlines()
# Strip whitespace and remove empty lines
lines = [l.strip() for l in lines if l.strip()]
if not lines:
raise ValueError("Empty file")
if lines[0] != "BEZIER":
raise ValueError("File must start with BEZIER")
# Find the line with dimension/nump/degree
data_start_idx = 1
header_vals = []
while data_start_idx < len(lines):
line = lines[data_start_idx]
if line.startswith('#'):
data_start_idx += 1
continue
else:
# Found the header line
parts = line.split()
if len(parts) < 3:
raise ValueError("Invalid header line format")
header_vals = [int(p) for p in parts]
data_start_idx += 1
break
if not header_vals:
raise ValueError("Could not find data dimensions")
ndim = header_vals[0]
nump = header_vals[1]
ndegree = header_vals[2]
# Read points
tempX = []
tempY = []
for i in range(data_start_idx, len(lines)):
line = lines[i]
if line.startswith('#'): continue
parts = line.split()
if len(parts) >= 2:
tempX.append(float(parts[0]))
tempY.append(float(parts[1]))
if len(tempX) != nump:
print(f"Warning: Expected {nump} points, found {len(tempX)}")
vX = tempX
vY = tempY
numv = len(vX)
jv = numv
curve_degree = ndegree
except Exception as e:
print(f"Error loading file: {e}")
sys.exit(1)
return
# Initialize global variables
text_message = None
save_message = None
filename_textbox = None
save_message_xloc = 0.5
iv_selected = -1
jv = 0
plot_center = [ 0.5, 0.5 ]
default_output_filename = 'controlpts.txt'
num_sample = 100
is_subdivided = False
# If inputA is an integer, set number of vertices to inputA
# If inputA is a string, read control points from inputA
if isinstance(inputA, int):
numv = inputA
jv = 0
vX = []
vY = []
elif isinstance(inputA, str):
try:
loadTextFile(inputA)
except Exception as e:
print(f"Error loading file: {e}")
print('Unable to open filename: ', inputA)
print('Check that file exists and that you have read permission.')
print('Exiting.')
return
else:
print('Illegal input: ', inputA)
print('Exiting.')
return
if (numv < 2):
print('Illegal number of vertices: ', numv)
print('Requires at least two vertices.')
print('Exiting')
return
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.0])
plt.subplots_adjust(right=0.9)
outputSelectVertexLocation(0)
# get plot lower left (LL) and upper right (UR)
axLL = ax.transData.transform((0,0));
axUR = ax.transData.transform((1,1));
createButtons(0.91, 0.08, 0.05)
if (jv < numv):
# User interactively selects vertices
cid_bpress = fig.canvas.mpl_connect('button_press_event', addVertexCallback)
else:
# Vertices already read in from input file
drawBezierEnableButtonsPickEvent(vX, vY)
# Enable smooth for already loaded curve if cpts - 1 is multiple of degree
# and cpts - 1 != degree
if (len(vX) - 1) % curve_degree == 0 and curve_degree != len(vX) - 1:
# Set flag to enable Smooth button
is_subdivided = True
# update bottom to distinguish enabled or not
smooth_button.color = 'white'
smooth_button.hovercolor = 'green'
plt.show()
return
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python ibezier.py <num_points> OR <filename>")
sys.exit()
arg = sys.argv[1]
try:
val = int(arg)
ibezier(val)
except ValueError:
ibezier(arg)
except Exception as e:
print(f"Error: {e}")
sys.exit()