431 lines
16 KiB
Python
431 lines
16 KiB
Python
import pygame
|
||
import math
|
||
import numpy as np
|
||
import random
|
||
from utils.cv_marker import cap_and_mark
|
||
from utils.stack_exe import Stackbot
|
||
import cv2
|
||
|
||
|
||
pygame.init()
|
||
|
||
WIDTH, HEIGHT = 800, 600
|
||
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
||
pygame.display.set_caption("积木塔仿真")
|
||
|
||
WHITE = (255, 255, 255)
|
||
BLACK = (0, 0, 0)
|
||
RED = (255, 0, 0)
|
||
|
||
LAYER_COLORS = [
|
||
(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255),
|
||
(0, 255, 255), (128, 0, 0), (0, 128, 0), (0, 0, 128), (128, 128, 0),
|
||
]
|
||
|
||
cap = cv2.VideoCapture(1)
|
||
cap.set(6,cv2.VideoWriter.fourcc('M','J','P','G'))
|
||
|
||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
|
||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
|
||
|
||
class Block:
|
||
def __init__(self, x, y, width, height, angle, layer):
|
||
self.x = x
|
||
self.y = y
|
||
self.width = width
|
||
self.height = height
|
||
self.angle = angle
|
||
self.layer = layer
|
||
self.color = LAYER_COLORS[layer % len(LAYER_COLORS)]
|
||
|
||
def draw(self, surface):
|
||
rotated_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
|
||
rotated_surface.set_alpha(125)
|
||
pygame.draw.rect(rotated_surface, self.color, (0, 0, self.width, self.height))
|
||
rotated_surface = pygame.transform.rotate(rotated_surface, self.angle)
|
||
surface.blit(rotated_surface, (self.x - rotated_surface.get_width() // 2, self.y - rotated_surface.get_height() // 2))
|
||
|
||
def get_corners(self):
|
||
w, h = self.width / 2, self.height / 2
|
||
corners = [(-w, -h), (w, -h), (w, h), (-w, h)]
|
||
angle_rad = np.deg2rad(self.angle)
|
||
rotation_matrix = np.array([
|
||
[np.cos(angle_rad), -np.sin(angle_rad)],
|
||
[np.sin(angle_rad), np.cos(angle_rad)]
|
||
])
|
||
rotated_corners = np.dot(corners, rotation_matrix)
|
||
translated_corners = rotated_corners + np.array([self.x, self.y])
|
||
# print(translated_corners)
|
||
rotated_corners = [(float(point[0]), float(point[1])) for point in translated_corners]
|
||
|
||
return rotated_corners
|
||
|
||
|
||
class Tower:
|
||
def __init__(self):
|
||
self.blocks = []
|
||
self.current_layer = 0
|
||
self.layer_centroids = [] # 存储每层的重心
|
||
self.stability_threshold = 10 # 稳定性阈值(像素)
|
||
self.rotation_angle = 15 # 预设每层旋转角度
|
||
self.angle_tolerance = 5 # 允许的角度偏移
|
||
self.position_tolerance = 10 # 允许的位置偏移
|
||
|
||
self.sim_to_robot_matrix = np.array([
|
||
[1, 0, 0, -300], # 假设的转换矩阵,需要根据实际情况调整
|
||
[0, -1, 0, -200],
|
||
[0, 0, 1, 0],
|
||
[0, 0, 0, 1]
|
||
])
|
||
self.robot_to_sim_matrix = np.linalg.inv(self.sim_to_robot_matrix)
|
||
|
||
self.stack_bot = Stackbot()
|
||
|
||
self.is_upper = False
|
||
|
||
|
||
def sim_to_robot_coords(self, x, y, angle):
|
||
sim_coords = np.array([x, y, 0, 1])
|
||
robot_coords = self.sim_to_robot_matrix.dot(sim_coords)
|
||
robot_angle = (angle + 360) % 360 # 确保角度在0-359范围内
|
||
return robot_coords[0], robot_coords[1], robot_angle
|
||
|
||
def robot_to_sim_coords(self, x, y, angle):
|
||
robot_coords = np.array([x, y, 0, 1])
|
||
sim_coords = self.robot_to_sim_matrix.dot(robot_coords)
|
||
sim_angle = (angle + 360) % 360
|
||
return sim_coords[0], sim_coords[1], sim_angle
|
||
|
||
def add_block_from_robot(self, x, y, angle):
|
||
"""
|
||
这个函数根据机器人的坐标和角度,在模拟世界中添加一个方块
|
||
|
||
参数:
|
||
x (float): 机器人的 x 坐标
|
||
y (float): 机器人的 y 坐标
|
||
angle (float): 机器人的角度
|
||
|
||
返回值:
|
||
bool:如果方块添加成功,返回 True,否则返回 False
|
||
|
||
注意:这个函数需要先调用 robot_to_sim_coords 来将机器人坐标转换为模拟坐标,并且需要一个名为 Block 的类来创建方块对象
|
||
"""
|
||
sim_x, sim_y, sim_angle = self.robot_to_sim_coords(x, y, angle)
|
||
new_block = Block(sim_x, sim_y, 100, 20, sim_angle, self.current_layer)
|
||
if self.add_block(new_block):
|
||
print(f"Block added from robot at ({sim_x:.2f}, {sim_y:.2f}) with angle {sim_angle}")
|
||
return True
|
||
else:
|
||
print(f"Failed to add block from robot at ({sim_x:.2f}, {sim_y:.2f}) with angle {sim_angle}")
|
||
return False
|
||
|
||
|
||
def add_block(self, block):
|
||
if self.current_layer == 5:
|
||
self.is_upper = True
|
||
print("is upper")
|
||
|
||
if not self.check_interference(block):
|
||
self.blocks.append(block)
|
||
self.stack_bot.pick_stack()
|
||
robo_xyz = self.sim_to_robot_coords(block.x, block.y, block.angle)
|
||
print(block.x, block.y, block.angle)
|
||
if self.is_upper:
|
||
self.stack_bot.place_stack(robo_xyz[0], robo_xyz[1], robo_xyz[2], self.current_layer - 5)
|
||
else:
|
||
self.stack_bot.place_stack(robo_xyz[0], robo_xyz[1], robo_xyz[2], self.current_layer)
|
||
return True
|
||
return False
|
||
|
||
def draw(self, surface):
|
||
for block in self.blocks:
|
||
block.draw(surface)
|
||
|
||
def check_stability(self):
|
||
current_layer_centroid = self.calculate_current_layer_centroid()
|
||
tower_centroid = self.calculate_tower_centroid()
|
||
|
||
if current_layer_centroid is None or tower_centroid is None:
|
||
return True # 如果没有足够的块来计算重心,我们假设它是稳定的
|
||
|
||
horizontal_distance = abs(current_layer_centroid[0] - tower_centroid[0])
|
||
|
||
return horizontal_distance <= self.stability_threshold
|
||
|
||
def blocks_overlap(self, block1, block2):
|
||
corners1 = block1.get_corners()
|
||
corners2 = block2.get_corners()
|
||
|
||
# 检查 block1 的角是否在 block2 内部
|
||
for corner in corners1:
|
||
if self.point_inside_polygon(corner, corners2):
|
||
return True
|
||
|
||
# 检查 block2 的角是否在 block1 内部
|
||
for corner in corners2:
|
||
if self.point_inside_polygon(corner, corners1):
|
||
return True
|
||
|
||
# 检查边是否相交
|
||
for i in range(4):
|
||
for j in range(4):
|
||
if self.line_intersects(corners1[i], corners1[(i+1)%4], corners2[j], corners2[(j+1)%4]):
|
||
return True
|
||
|
||
return False
|
||
|
||
def point_inside_polygon(self, point, polygon):
|
||
x, y = point
|
||
n = len(polygon)
|
||
inside = False
|
||
p1x, p1y = polygon[0]
|
||
for i in range(n + 1):
|
||
p2x, p2y = polygon[i % n]
|
||
if y > min(p1y, p2y):
|
||
if y <= max(p1y, p2y):
|
||
if x <= max(p1x, p2x):
|
||
if p1y != p2y:
|
||
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
||
if p1x == p2x or x <= xinters:
|
||
inside = not inside
|
||
p1x, p1y = p2x, p2y
|
||
return inside
|
||
|
||
def line_intersects(self, p1, p2, p3, p4):
|
||
def ccw(A, B, C):
|
||
return (C[1]-A[1]) * (B[0]-A[0]) > (B[1]-A[1]) * (C[0]-A[0])
|
||
return ccw(p1,p3,p4) != ccw(p2,p3,p4) and ccw(p1,p2,p3) != ccw(p1,p2,p4)
|
||
|
||
def check_interference(self, new_block):
|
||
# 检查是否与其他木块重叠
|
||
for block in self.blocks:
|
||
if block.layer == new_block.layer and self.blocks_overlap(block, new_block):
|
||
return True
|
||
return False
|
||
|
||
def calculate_current_layer_centroid(self):
|
||
current_layer_blocks = [block for block in self.blocks if block.layer == self.current_layer]
|
||
if not current_layer_blocks:
|
||
return None
|
||
|
||
total_area = 0
|
||
weighted_x = 0
|
||
weighted_y = 0
|
||
|
||
for block in current_layer_blocks:
|
||
area = block.width * block.height
|
||
total_area += area
|
||
weighted_x += block.x * area
|
||
weighted_y += block.y * area
|
||
|
||
if total_area == 0:
|
||
return None
|
||
|
||
centroid_x = weighted_x / total_area
|
||
centroid_y = weighted_y / total_area
|
||
return (centroid_x, centroid_y)
|
||
|
||
def calculate_tower_centroid(self):
|
||
if not self.blocks:
|
||
return None
|
||
|
||
total_area = 0
|
||
weighted_x = 0
|
||
weighted_y = 0
|
||
|
||
for block in self.blocks:
|
||
area = block.width * block.height
|
||
total_area += area
|
||
weighted_x += block.x * area
|
||
weighted_y += block.y * area
|
||
|
||
if total_area == 0:
|
||
return None
|
||
|
||
centroid_x = weighted_x / total_area
|
||
centroid_y = weighted_y / total_area
|
||
return (centroid_x, centroid_y)
|
||
|
||
def increase_layer(self):
|
||
centroid = self.calculate_current_layer_centroid()
|
||
if centroid:
|
||
self.layer_centroids.append(centroid)
|
||
self.current_layer += 1
|
||
|
||
def auto_place_block(self):
|
||
if self.current_layer == 0:
|
||
# 对于第一层,直接在中心放置
|
||
return Block(WIDTH // 2, HEIGHT // 2, 100, 20, 0, self.current_layer)
|
||
|
||
base_centroid = self.layer_centroids[0] if self.layer_centroids else (WIDTH // 2, HEIGHT // 2)
|
||
current_centroid = self.calculate_current_layer_centroid() or base_centroid
|
||
|
||
# 计算当前层的总质量(假设质量与宽度成正比)
|
||
total_mass = sum(block.width for block in self.blocks if block.layer == self.current_layer)
|
||
|
||
# 假设新积木块的质量与宽度成正比
|
||
new_block_mass = 100 # 新积木块的宽度
|
||
|
||
# 计算理想位置
|
||
ideal_x = (base_centroid[0] * (total_mass + new_block_mass) - current_centroid[0] * total_mass) / new_block_mass
|
||
ideal_y = (base_centroid[1] * (total_mass + new_block_mass) - current_centroid[1] * total_mass) / new_block_mass
|
||
|
||
# 计算木块中心到塔重心的角度
|
||
dx = base_centroid[0] - ideal_x
|
||
dy = base_centroid[1] - ideal_y
|
||
center_to_centroid_angle = math.degrees(math.atan2(dy, dx))
|
||
|
||
# 计算垂直于中心线的理想角度
|
||
ideal_angle = (center_to_centroid_angle + 90) % 180 + 90
|
||
|
||
# 步骤 2: 寻找最佳角度
|
||
best_block = None
|
||
min_angle_diff = float('inf')
|
||
|
||
for angle in range(0, 180, 15): # 每15度尝试一次
|
||
test_block = Block(ideal_x, ideal_y, 100, 20, angle, self.current_layer)
|
||
|
||
if not self.check_interference(test_block):
|
||
# 检查是否有支撑
|
||
if self.has_support(test_block):
|
||
# 计算与理想角度的差异
|
||
angle_diff = min((angle - ideal_angle) % 180, (ideal_angle - angle) % 180)
|
||
if angle_diff < min_angle_diff:
|
||
min_angle_diff = angle_diff
|
||
best_block = test_block
|
||
|
||
return best_block
|
||
|
||
def has_support(self, block):
|
||
if self.current_layer == 0:
|
||
return True # 第一层总是有支撑
|
||
|
||
block_corners = block.get_corners()
|
||
for lower_block in [b for b in self.blocks if b.layer == self.current_layer - 1]:
|
||
lower_corners = lower_block.get_corners()
|
||
for corner in block_corners:
|
||
if self.point_inside_polygon(corner, lower_corners):
|
||
return True
|
||
return False
|
||
|
||
def auto_place_layer(self):
|
||
center_x, center_y = 359, 90
|
||
block_width, block_height = 100, 20
|
||
square_size = 110 # 正方形的边长
|
||
|
||
# 基于当前层数计算旋转角度
|
||
layer_rotation = (self.current_layer * 30) % 360
|
||
|
||
# 计算正方形四个角的坐标
|
||
base_positions = [
|
||
(-square_size/2, -square_size/2), # 左上
|
||
(square_size/2, -square_size/2), # 右上
|
||
(square_size/2, square_size/2), # 右下
|
||
(-square_size/2, square_size/2) # 左下
|
||
]
|
||
|
||
# 旋转基础位置
|
||
rotated_positions = []
|
||
for x, y in base_positions:
|
||
angle_rad = math.radians(layer_rotation)
|
||
rotated_x = x * math.cos(angle_rad) - y * math.sin(angle_rad)
|
||
rotated_y = x * math.sin(angle_rad) + y * math.cos(angle_rad)
|
||
rotated_positions.append((center_x + rotated_x, center_y + rotated_y))
|
||
|
||
base_angles = [45, 135, 225, 315]
|
||
angles = [(angle - layer_rotation) % 360 for angle in base_angles]
|
||
|
||
for (x, y), angle in zip(rotated_positions, angles):
|
||
new_block = Block(x, y, block_width, block_height, angle, self.current_layer)
|
||
if self.add_block(new_block):
|
||
print(f"Block placed at ({x:.2f}, {y:.2f}) with angle {angle}")
|
||
else:
|
||
print(f"Failed to place block at ({x:.2f}, {y:.2f}) with angle {angle}")
|
||
|
||
|
||
def clear_current_layer(self):
|
||
self.blocks = [block for block in self.blocks if block.layer!= self.current_layer]
|
||
|
||
def scan_current_layer(self):
|
||
current_block_pos = cap_and_mark(cap=cap)
|
||
for block in current_block_pos:
|
||
print(block)
|
||
self.add_block_from_robot(block[0][0], block[0][1], block[1])
|
||
|
||
|
||
def main():
|
||
tower = Tower()
|
||
clock = pygame.time.Clock()
|
||
current_block = None
|
||
|
||
running = True
|
||
while running:
|
||
for event in pygame.event.get():
|
||
if event.type == pygame.QUIT:
|
||
running = False
|
||
elif event.type == pygame.MOUSEBUTTONDOWN:
|
||
if event.button == 1:
|
||
x, y = pygame.mouse.get_pos()
|
||
current_block = Block(x, y, 100, 20, 0, tower.current_layer)
|
||
elif event.type == pygame.MOUSEMOTION:
|
||
if current_block:
|
||
current_block.x, current_block.y = pygame.mouse.get_pos()
|
||
elif event.type == pygame.MOUSEBUTTONUP:
|
||
if event.button == 1 and current_block:
|
||
if tower.add_block(current_block):
|
||
current_block = None
|
||
else:
|
||
print("CANNOT place block here, there is already a block there.")
|
||
elif event.type == pygame.KEYDOWN:
|
||
# 处理所有按键事件
|
||
if event.key == pygame.K_r and current_block: # 按 'R' 键旋转当前块
|
||
current_block.angle += 15
|
||
elif event.key == pygame.K_SPACE: # 按 'Space' 键确认当前层完成放置
|
||
tower.increase_layer()
|
||
elif event.key == pygame.K_a: # 按 'A' 键自动放置
|
||
auto_block = tower.auto_place_block()
|
||
if auto_block:
|
||
tower.add_block(auto_block)
|
||
else:
|
||
print("Cannot find a suitable position for auto-placement.")
|
||
elif event.key == pygame.K_n: # 按 'N' 键自动放置新的一层
|
||
tower.auto_place_layer()
|
||
elif event.key == pygame.K_s: # 按 'S' 键扫描当前层
|
||
tower.scan_current_layer()
|
||
|
||
|
||
screen.fill(WHITE)
|
||
tower.draw(screen)
|
||
if current_block:
|
||
current_block.draw(screen)
|
||
|
||
# 计算并绘制当前层的重心
|
||
current_centroid = tower.calculate_current_layer_centroid()
|
||
if current_centroid:
|
||
pygame.draw.circle(screen, RED, (int(current_centroid[0]), int(current_centroid[1])), 5)
|
||
font = pygame.font.Font(None, 24)
|
||
|
||
# 绘制之前层的重心
|
||
for i, centroid in enumerate(tower.layer_centroids):
|
||
pygame.draw.circle(screen, BLACK, (int(centroid[0]), int(centroid[1])), 3)
|
||
font = pygame.font.Font(None, 24)
|
||
text = font.render(f"{i+1}", True, BLACK)
|
||
screen.blit(text, (int(centroid[0]) + 10, int(centroid[1]) - 10))
|
||
|
||
if not tower.check_stability():
|
||
font = pygame.font.Font(None, 36)
|
||
text = font.render("WARNING: Tower is NOT stable", True, RED)
|
||
screen.blit(text, (WIDTH // 2 - text.get_width() // 2, 20))
|
||
|
||
font = pygame.font.Font(None, 36)
|
||
layer_text = font.render(f"Current layer: {tower.current_layer + 1}", True, BLACK)
|
||
screen.blit(layer_text, (10, 10))
|
||
|
||
pygame.display.flip()
|
||
clock.tick(60)
|
||
|
||
pygame.quit()
|
||
|
||
if __name__ == "__main__":
|
||
main()
|