项目初始化
This commit is contained in:
commit
423f87f29b
118
.gitignore
vendored
Normal file
118
.gitignore
vendored
Normal file
@ -0,0 +1,118 @@
|
||||
# General
|
||||
.DS_Store
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Project specific
|
||||
face_recognition.log
|
||||
86
001保存摄像头流.py
Normal file
86
001保存摄像头流.py
Normal file
@ -0,0 +1,86 @@
|
||||
import cv2
|
||||
import datetime
|
||||
import os
|
||||
|
||||
def save_rtsp_to_video(rtsp_url, output_dir='output', duration_minutes=10):
|
||||
"""
|
||||
将RTSP流保存为视频文件
|
||||
|
||||
参数:
|
||||
rtsp_url: RTSP流地址
|
||||
output_dir: 输出目录
|
||||
duration_minutes: 每个视频文件的时长(分钟)
|
||||
"""
|
||||
# 创建输出目录
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 打开RTSP流
|
||||
cap = cv2.VideoCapture(rtsp_url)
|
||||
|
||||
if not cap.isOpened():
|
||||
print("无法打开RTSP流")
|
||||
return
|
||||
|
||||
# 获取视频的帧率和尺寸
|
||||
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
||||
if fps <= 0:
|
||||
fps = 25 # 默认帧率
|
||||
|
||||
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
|
||||
# 计算最大帧数(按duration_minutes分钟分割)
|
||||
max_frames = fps * 60 * duration_minutes
|
||||
|
||||
frame_count = 0
|
||||
file_count = 1
|
||||
|
||||
# 创建第一个视频文件
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_file = os.path.join(output_dir, f"video_{timestamp}_part{file_count}.mp4")
|
||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # 或者使用'avc1'
|
||||
out = cv2.VideoWriter(output_file, fourcc, fps, (frame_width, frame_height))
|
||||
|
||||
print(f"开始录制,保存到: {output_file}")
|
||||
|
||||
try:
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
|
||||
if not ret:
|
||||
print("无法获取帧,可能流已断开")
|
||||
break
|
||||
|
||||
# 写入帧
|
||||
out.write(frame)
|
||||
frame_count += 1
|
||||
|
||||
# 如果达到最大帧数,创建新文件
|
||||
if frame_count >= max_frames:
|
||||
out.release()
|
||||
file_count += 1
|
||||
frame_count = 0
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_file = os.path.join(output_dir, f"video_{timestamp}_part{file_count}.mp4")
|
||||
out = cv2.VideoWriter(output_file, fourcc, fps, (frame_width, frame_height))
|
||||
print(f"创建新文件: {output_file}")
|
||||
|
||||
# # 按q键退出(需要显示窗口时才有效)
|
||||
# if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||
# break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("用户中断录制")
|
||||
finally:
|
||||
cap.release()
|
||||
out.release()
|
||||
# cv2.destroyAllWindows()
|
||||
print("录制结束")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例RTSP URL - 替换为你的实际RTSP地址
|
||||
rtsp_url = "rtsp://10.0.0.61/live/video6"
|
||||
|
||||
# 调用函数开始录制
|
||||
save_rtsp_to_video(rtsp_url, duration_minutes=10)
|
||||
15
002查看摄像头id.py
Normal file
15
002查看摄像头id.py
Normal file
@ -0,0 +1,15 @@
|
||||
import cv2
|
||||
|
||||
def list_cameras():
|
||||
index = 0
|
||||
cameras = []
|
||||
while True:
|
||||
cap = cv2.VideoCapture(index)
|
||||
if not cap.read()[0]:
|
||||
break
|
||||
cameras.append(f"Camera {index}: {cap.getBackendName()}")
|
||||
cap.release()
|
||||
index += 1
|
||||
return cameras
|
||||
|
||||
print(list_cameras())
|
||||
3
003查看GUI后端.py
Normal file
3
003查看GUI后端.py
Normal file
@ -0,0 +1,3 @@
|
||||
import cv2
|
||||
print(cv2.getBuildInformation())
|
||||
print("UI backend:", cv2.currentUIFramework())
|
||||
82
config.yaml
Normal file
82
config.yaml
Normal file
@ -0,0 +1,82 @@
|
||||
# 人脸识别系统配置文件
|
||||
|
||||
# CompreFace API配置
|
||||
compreface:
|
||||
# host: "http://192.168.30.195"
|
||||
host: "http://10.0.0.202"
|
||||
# port: "9900"
|
||||
port: "8000"
|
||||
# api_key: "your_api_key_here"
|
||||
# recognition_api_key: "9115667c-4968-400f-9006-7b2646fba8df"
|
||||
recognition_api_key: "a5924457-62c9-47dc-a6e7-15462c502d2c"
|
||||
# detection_api_key: "08f31eab-de28-4c00-aea5-eb69f2619783"
|
||||
detection_api_key: "070283a2-faa3-423b-9772-2cd48ecc5362"
|
||||
|
||||
# WebSocket配置
|
||||
websocket:
|
||||
url: "ws://127.0.0.1:3344"
|
||||
status_interval: 0.2 # 状态查询间隔(秒)
|
||||
reconnect_delay: 5 # 重连延迟(秒)
|
||||
|
||||
# 二维码设置
|
||||
qrcode:
|
||||
image_path: "qrcode.jpg" # 二维码图片路径
|
||||
display_duration: 10 # 显示持续时间(秒)
|
||||
|
||||
video:
|
||||
path: "exhibition.mp4" # 视频文件路径
|
||||
loop: true # 是否循环播放
|
||||
|
||||
# 摄像头配置
|
||||
camera:
|
||||
# device_id: "/dev/v4l/by-id/usb-SYX-230524-J_HD_Camera-video-index0" # 摄像头设备ID, id是动态变化的, ls /dev/v4l/by-id/ 查看系统创建的链接
|
||||
device_id: 0 # 摄像头设备ID, id是动态变化的, ls /dev/v4l/by-id/ 查看系统创建的链接
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 30
|
||||
retry_interval: 2 # 打开摄像头失败后的重试间隔(秒)
|
||||
|
||||
# 人脸检测配置
|
||||
face_detection:
|
||||
frame_interval: 10 # 检测帧间隔(每N帧检测一次)
|
||||
quality_threshold: 10 # 图像质量阈值(Laplacian方差)
|
||||
min_face_size: 80 # 最小人脸尺寸(像素)
|
||||
face_present_duration: 2.0 # 持续出现时长(秒)才触发识别
|
||||
|
||||
# 人脸识别配置
|
||||
face_recognition:
|
||||
# similarity_threshold: 0.85 # 相似度阈值(低于此值视为陌生人)
|
||||
recognition_cooldown: 20.0 # 同一人识别冷却时间(秒)
|
||||
|
||||
# 角色映射配置
|
||||
role_mapping:
|
||||
stranger_threshold: 0.98 # 人脸识别阈值
|
||||
# visitor_threshold: 0.70 # 访客识别阈值
|
||||
# 低于visitor_threshold视为陌生人
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level: "INFO" # DEBUG, INFO, WARNING, ERROR
|
||||
file: "face_recognition.log"
|
||||
max_bytes: 10485760 # 10MB
|
||||
backup_count: 5
|
||||
|
||||
|
||||
# 中文字体设置
|
||||
display:
|
||||
font_path: "/usr/share/fonts/truetype/noto/NotoMono-Regular.ttf"
|
||||
|
||||
|
||||
stream:
|
||||
# enabled: true # 开关推流功能
|
||||
enabled: false # 开关推流功能
|
||||
rtmp_url: "rtsp://127.0.0.1/live/video6"
|
||||
|
||||
ffmpeg:
|
||||
fps: 10 # 推流帧率
|
||||
video_bitrate: "1000k" # 码率
|
||||
preset: "ultrafast" # 编码速度
|
||||
tune: "zerolatency" # 低延迟优化
|
||||
pixel_format: "yuv420p"
|
||||
video_codec: "libx264"
|
||||
audio: false # 不包含音频
|
||||
BIN
exhibition.mp4
Normal file
BIN
exhibition.mp4
Normal file
Binary file not shown.
1968
face_rec.py
Normal file
1968
face_rec.py
Normal file
File diff suppressed because it is too large
Load Diff
24
face_rec.service
Normal file
24
face_rec.service
Normal file
@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=Face Rec GUI Service (system-level)
|
||||
After=display-manager.service
|
||||
# 或者 After=gdm.service / lightdm.service,根据你系统用的显示管理器
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=unitree
|
||||
WorkingDirectory=/home/unitree/robot_face_rec
|
||||
|
||||
ExecStart=/home/unitree/robot_face_rec/start.sh
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStartSec=30
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# 如果需要,你可以设置环境,但脚本里会重写 DISPLAY
|
||||
# Environment=DISPLAY=:0
|
||||
# Environment=XAUTHORITY=/home/unitree/.Xauthority
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
1189
face_rec_改进版.py
Normal file
1189
face_rec_改进版.py
Normal file
File diff suppressed because it is too large
Load Diff
1189
face_rec_本地测试版.py
Normal file
1189
face_rec_本地测试版.py
Normal file
File diff suppressed because it is too large
Load Diff
333
preview_qrcode.py
Normal file
333
preview_qrcode.py
Normal file
@ -0,0 +1,333 @@
|
||||
import math
|
||||
import os
|
||||
import cv2
|
||||
import yaml
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
|
||||
# Canvas Configuration
|
||||
CANVAS_SIZE = (1920, 1080)
|
||||
BG_COLOR_START = (240, 242, 245)
|
||||
BG_COLOR_END = (200, 210, 230)
|
||||
|
||||
# Colors
|
||||
PRIMARY_COLOR = (23, 43, 77) # Dark Blue for headings
|
||||
SECONDARY_COLOR = (94, 108, 132) # Grey for secondary text
|
||||
ACCENT_COLOR = (0, 82, 204) # Bright Blue for highlights/numbers
|
||||
CARD_BG_COLOR = (255, 255, 255)
|
||||
CARD_SHADOW_COLOR = (9, 30, 66, 40) # Shadow color
|
||||
|
||||
INSTRUCTION_TITLE = "访客预约流程"
|
||||
INSTRUCTION_SUBTITLE = "Visitor Registration Process"
|
||||
INSTRUCTION_STEPS = [
|
||||
"第一步:扫码关注“康达新材”公众号。",
|
||||
"第二步:点击【关于我们】→【我是访客】进入“访客注册”界面,填写并上传相应信息并点击“提交”。",
|
||||
"第三步:将第二步信息提交完毕后在“访客预约”界面选择右下角的“+”号按钮,填写预约信息。",
|
||||
"第四步:请仔细阅读安全告知书,点击我知道了。",
|
||||
"第五步:填写被访人信息及来访事由等内容并提交。",
|
||||
"第六步:显示提交成功,并且手机、微信会收到预约相关短信通知。",
|
||||
]
|
||||
|
||||
def load_config(path="config.yaml"):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
def resolve_font(size: int, preferred: str | None = None) -> ImageFont.FreeTypeFont:
|
||||
candidates = [preferred] if preferred else []
|
||||
candidates += [
|
||||
"C:/Windows/Fonts/msyhbd.ttc", # Microsoft YaHei Bold
|
||||
"C:/Windows/Fonts/msyh.ttc", # Microsoft YaHei
|
||||
"C:/Windows/Fonts/simhei.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansCJK-Bold.ttc",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/truetype/noto/NotoMono-Regular.ttf",
|
||||
]
|
||||
for path in candidates:
|
||||
if path and os.path.exists(path):
|
||||
try:
|
||||
return ImageFont.truetype(path, size)
|
||||
except OSError:
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.ImageDraw):
|
||||
lines = []
|
||||
current = ""
|
||||
for ch in text:
|
||||
tentative = current + ch
|
||||
# Pillow 10+ uses textlength, older uses textsize
|
||||
try:
|
||||
width = draw.textlength(tentative, font=font)
|
||||
except AttributeError:
|
||||
width, _ = draw.textsize(tentative, font=font)
|
||||
|
||||
if width <= max_width:
|
||||
current = tentative
|
||||
else:
|
||||
if current:
|
||||
lines.append(current)
|
||||
current = ch
|
||||
if current:
|
||||
lines.append(current)
|
||||
return lines
|
||||
|
||||
def create_gradient_background(size: tuple[int, int], start_color: tuple[int, int, int], end_color: tuple[int, int, int]) -> Image.Image:
|
||||
width, height = size
|
||||
# Vertical gradient
|
||||
base = Image.new('RGB', size, start_color)
|
||||
top = Image.new('RGB', size, end_color)
|
||||
mask = Image.new('L', size)
|
||||
mask_data = np.tile(np.linspace(0, 255, height, dtype=np.uint8), (width, 1)).T
|
||||
mask = Image.fromarray(mask_data, 'L')
|
||||
return Image.composite(top, base, mask)
|
||||
|
||||
def draw_rounded_rect(draw, box, radius, fill, outline=None, width=0):
|
||||
draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width)
|
||||
|
||||
def draw_shadow(image, box, radius, offset=(0, 4), blur=10, shadow_color=(0,0,0,50)):
|
||||
# Create a separate shadow layer
|
||||
shadow_layer = Image.new('RGBA', image.size, (0,0,0,0))
|
||||
shadow_draw = ImageDraw.Draw(shadow_layer)
|
||||
|
||||
sx0, sy0, sx1, sy1 = box
|
||||
dx, dy = offset
|
||||
|
||||
shadow_box = (sx0 + dx, sy0 + dy, sx1 + dx, sy1 + dy)
|
||||
shadow_draw.rounded_rectangle(shadow_box, radius=radius, fill=shadow_color)
|
||||
|
||||
# Blur the shadow
|
||||
shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(blur))
|
||||
|
||||
# Composite
|
||||
image.alpha_composite(shadow_layer)
|
||||
|
||||
def build_canvas(qr_image: np.ndarray, title_font, subtitle_font, body_font, step_title_font, badge_font) -> np.ndarray:
|
||||
canvas_width, canvas_height = CANVAS_SIZE
|
||||
|
||||
# 1. Background
|
||||
bg = create_gradient_background(CANVAS_SIZE, (245, 247, 250), (223, 225, 230)).convert("RGBA")
|
||||
|
||||
draw = ImageDraw.Draw(bg, "RGBA")
|
||||
|
||||
# Layout Constants
|
||||
MARGIN = 50
|
||||
GUTTER = 40
|
||||
|
||||
# Left Panel (QR Code) - 35% width approx
|
||||
left_width = int((canvas_width - 2 * MARGIN - GUTTER) * 0.35)
|
||||
right_width = canvas_width - 2 * MARGIN - GUTTER - left_width
|
||||
|
||||
left_box = (MARGIN, MARGIN, MARGIN + left_width, canvas_height - MARGIN)
|
||||
right_box = (MARGIN + left_width + GUTTER, MARGIN, canvas_width - MARGIN, canvas_height - MARGIN)
|
||||
|
||||
# --- Draw Left Panel ---
|
||||
# Shadow
|
||||
draw_shadow(bg, left_box, radius=30, offset=(0, 10), blur=20, shadow_color=(0,0,0,30))
|
||||
# Card
|
||||
draw_rounded_rect(draw, left_box, radius=30, fill=(255, 255, 255, 255))
|
||||
|
||||
# Left Content
|
||||
cx = (left_box[0] + left_box[2]) // 2
|
||||
cy_top = left_box[1] + 120
|
||||
|
||||
# Title
|
||||
text = "访客登记"
|
||||
try:
|
||||
w = draw.textlength(text, font=title_font)
|
||||
except:
|
||||
w, _ = draw.textsize(text, font=title_font)
|
||||
draw.text((cx - w/2, cy_top), text, font=title_font, fill=PRIMARY_COLOR)
|
||||
|
||||
# Subtitle
|
||||
text = "Visitor Registration"
|
||||
try:
|
||||
w = draw.textlength(text, font=subtitle_font)
|
||||
except:
|
||||
w, _ = draw.textsize(text, font=subtitle_font)
|
||||
draw.text((cx - w/2, cy_top + 70), text, font=subtitle_font, fill=SECONDARY_COLOR)
|
||||
|
||||
# QR Code
|
||||
qr_size = min(left_width - 100, 500)
|
||||
qr_y = cy_top + 180
|
||||
|
||||
# Resize QR
|
||||
qr_pil = Image.fromarray(cv2.cvtColor(qr_image, cv2.COLOR_BGR2RGB))
|
||||
qr_pil = qr_pil.resize((qr_size, qr_size), Image.LANCZOS)
|
||||
bg.paste(qr_pil, (cx - qr_size//2, qr_y))
|
||||
|
||||
# Scan Hint
|
||||
hint_y = qr_y + qr_size + 50
|
||||
hint_text = "请使用微信扫码登记"
|
||||
try:
|
||||
w = draw.textlength(hint_text, font=step_title_font)
|
||||
except:
|
||||
w, _ = draw.textsize(hint_text, font=step_title_font)
|
||||
|
||||
# Draw a pill background for hint
|
||||
pill_padding = 20
|
||||
pill_box = (cx - w/2 - pill_padding, hint_y - pill_padding, cx + w/2 + pill_padding, hint_y + 40 + pill_padding)
|
||||
draw.rounded_rectangle(pill_box, radius=30, fill=(240, 242, 245), outline=None)
|
||||
draw.text((cx - w/2, hint_y), hint_text, font=step_title_font, fill=ACCENT_COLOR)
|
||||
|
||||
# --- Draw Right Panel ---
|
||||
|
||||
# Right Title Area - Compacted
|
||||
rt_y = MARGIN + 20
|
||||
draw.text((right_box[0], rt_y), INSTRUCTION_TITLE, font=title_font, fill=PRIMARY_COLOR)
|
||||
draw.text((right_box[0], rt_y + 60), INSTRUCTION_SUBTITLE, font=subtitle_font, fill=SECONDARY_COLOR)
|
||||
|
||||
# Separator Line
|
||||
sep_y = rt_y + 110
|
||||
draw.line((right_box[0], sep_y, right_box[2], sep_y), fill=(200, 200, 200), width=2)
|
||||
|
||||
# Grid Configuration
|
||||
grid_y_start = sep_y + 40
|
||||
grid_width = right_width
|
||||
cols = 2
|
||||
col_gap = 30
|
||||
row_gap = 30
|
||||
|
||||
col_width = (grid_width - (cols - 1) * col_gap) // cols
|
||||
|
||||
# Pre-calculate text layout to find uniform height
|
||||
max_lines = 0
|
||||
processed_steps = []
|
||||
|
||||
padding = 30
|
||||
badge_size = 50
|
||||
text_left_margin = badge_size + 20
|
||||
|
||||
# Calculate available width for text inside a card
|
||||
text_max_width = col_width - padding * 2 - text_left_margin
|
||||
|
||||
for i, step in enumerate(INSTRUCTION_STEPS):
|
||||
# Remove "第一步:" etc prefix if present to make it cleaner, we have badges
|
||||
clean_step = step.split(":", 1)[-1] if ":" in step else step
|
||||
|
||||
lines = wrap_text(clean_step, body_font, text_max_width, draw)
|
||||
processed_steps.append(lines)
|
||||
max_lines = max(max_lines, len(lines))
|
||||
|
||||
# Calculate Card Height
|
||||
# Padding top + max_lines * line_height + Padding bottom
|
||||
line_height = body_font.size + 10
|
||||
card_content_height = max(badge_size, max_lines * line_height)
|
||||
uniform_card_height = int(padding * 2 + card_content_height)
|
||||
|
||||
# Check if we overflow canvas height
|
||||
total_grid_height = 3 * uniform_card_height + 2 * row_gap
|
||||
if grid_y_start + total_grid_height > canvas_height - MARGIN:
|
||||
print(f"Warning: Content might overflow vertically. Required: {grid_y_start + total_grid_height}, Available: {canvas_height - MARGIN}")
|
||||
# Dynamic adjustment if needed (e.g. reduce gaps) but for now we rely on the font resizing done in main()
|
||||
|
||||
# Draw Grid
|
||||
for idx, lines in enumerate(processed_steps):
|
||||
row = idx // cols
|
||||
col = idx % cols
|
||||
|
||||
x = right_box[0] + col * (col_width + col_gap)
|
||||
y = grid_y_start + row * (uniform_card_height + row_gap)
|
||||
|
||||
card_box = (x, y, x + col_width, y + uniform_card_height)
|
||||
|
||||
# Card Shadow
|
||||
draw_shadow(bg, card_box, radius=20, offset=(0, 4), blur=12, shadow_color=(0,0,0,15))
|
||||
# Card Body
|
||||
draw_rounded_rect(draw, card_box, radius=20, fill=(255, 255, 255))
|
||||
|
||||
# Badge (Step Number)
|
||||
bx = x + padding
|
||||
by = y + padding
|
||||
draw.ellipse((bx, by, bx + badge_size, by + badge_size), fill=ACCENT_COLOR)
|
||||
|
||||
num_text = str(idx + 1)
|
||||
try:
|
||||
nw = draw.textlength(num_text, font=badge_font)
|
||||
except:
|
||||
nw, _ = draw.textsize(num_text, font=badge_font)
|
||||
# Center number
|
||||
draw.text((bx + (badge_size - nw)/2, by + (badge_size - badge_font.size)/2 - 2),
|
||||
num_text, font=badge_font, fill=(255, 255, 255))
|
||||
|
||||
# Text
|
||||
tx = x + padding + text_left_margin
|
||||
ty = y + padding
|
||||
|
||||
for line in lines:
|
||||
draw.text((tx, ty), line, font=body_font, fill=PRIMARY_COLOR)
|
||||
ty += line_height
|
||||
|
||||
return cv2.cvtColor(np.array(bg.convert("RGB")), cv2.COLOR_RGB2BGR)
|
||||
|
||||
def create_mock_qrcode(size: int = 640) -> np.ndarray:
|
||||
img = np.full((size, size, 3), 255, dtype=np.uint8) # White background
|
||||
|
||||
# Draw some patterns
|
||||
block = size // 15
|
||||
for y in range(0, size, block):
|
||||
for x in range(0, size, block):
|
||||
if (x // block + y // block) % 2 == 0:
|
||||
color = (0, 0, 0)
|
||||
# Corner markers
|
||||
if (x < 3*block and y < 3*block) or (x > size-4*block and y < 3*block) or (x < 3*block and y > size-4*block):
|
||||
color = (0, 0, 0)
|
||||
elif np.random.rand() > 0.3:
|
||||
color = (0, 0, 0)
|
||||
else:
|
||||
color = (255, 255, 255)
|
||||
|
||||
cv2.rectangle(img, (x, y), (x + block, y + block), color, -1)
|
||||
|
||||
# Borders for markers
|
||||
marker_len = 3 * block
|
||||
thickness = block
|
||||
# Top Left
|
||||
cv2.rectangle(img, (0,0), (marker_len, marker_len), (0,0,0), thickness)
|
||||
# Top Right
|
||||
cv2.rectangle(img, (size-marker_len,0), (size, marker_len), (0,0,0), thickness)
|
||||
# Bottom Left
|
||||
cv2.rectangle(img, (0,size-marker_len), (marker_len, size), (0,0,0), thickness)
|
||||
|
||||
return img
|
||||
|
||||
def main():
|
||||
config = load_config()
|
||||
preferred_font = config.get("display", {}).get("font_path") if config else None
|
||||
|
||||
# Define Fonts - Adjusted sizes for better fit
|
||||
# Title for left/right headers
|
||||
title_font = resolve_font(56, preferred_font)
|
||||
# Subtitles
|
||||
subtitle_font = resolve_font(30, preferred_font)
|
||||
# Step text - Reduced to ensure fit
|
||||
body_font = resolve_font(24, preferred_font)
|
||||
# Step titles or emphasis
|
||||
step_title_font = resolve_font(28, preferred_font)
|
||||
# Badge numbers
|
||||
badge_font = resolve_font(30, preferred_font)
|
||||
|
||||
# Mock data
|
||||
mock_qr = create_mock_qrcode(500)
|
||||
|
||||
# Build
|
||||
canvas = build_canvas(mock_qr, title_font, subtitle_font, body_font, step_title_font, badge_font)
|
||||
|
||||
# Display
|
||||
window_name = "Visitor Registration Preview"
|
||||
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
|
||||
cv2.resizeWindow(window_name, 1280, 720)
|
||||
cv2.imshow(window_name, canvas)
|
||||
|
||||
print("Displaying preview. Press any key to exit.")
|
||||
cv2.waitKey(0)
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
# Save for verification
|
||||
cv2.imwrite("preview_qrcode_sota.jpg", canvas)
|
||||
print("Saved preview to preview_qrcode_sota.jpg")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
preview_qrcode_sota.jpg
Normal file
BIN
preview_qrcode_sota.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
BIN
qrcode.jpeg
Normal file
BIN
qrcode.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
qrcode.jpg
Normal file
BIN
qrcode.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
62
start.sh
Normal file
62
start.sh
Normal file
@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
|
||||
# --- 日志相关 ---
|
||||
LOG_DIR="/home/unitree/robot_face_rec/logs/face_rec"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="$LOG_DIR/face_rec_$(date +%Y%m%d_%H%M%S).log"
|
||||
|
||||
{
|
||||
echo "===== $(date) Starting face_rec script ====="
|
||||
echo "User: $(whoami)"
|
||||
echo "PWD before cd: $(pwd)"
|
||||
echo "Python: $(which python) / Version: $(python --version 2>&1)"
|
||||
} >> "$LOG_FILE"
|
||||
|
||||
# 切换到项目目录
|
||||
cd /home/unitree/robot_face_rec || {
|
||||
echo "Failed to cd to project dir" >> "$LOG_FILE"
|
||||
exit 1
|
||||
}
|
||||
|
||||
{
|
||||
echo "PWD after cd: $(pwd)"
|
||||
echo "Env before display detection: DISPLAY=$DISPLAY, XAUTHORITY=$XAUTHORITY"
|
||||
} >> "$LOG_FILE"
|
||||
|
||||
# --- 动态检测 DISPLAY 的逻辑 ---
|
||||
# 方法:尝试从正在运行的 Xorg / X 进程环境中读取 DISPLAY
|
||||
|
||||
display_found=""
|
||||
|
||||
for pid in $(pgrep Xorg); do
|
||||
if [ -r /proc/$pid/environ ]; then
|
||||
disp=$(tr '\0' '\n' < /proc/$pid/environ | grep '^DISPLAY=' | cut -d'=' -f2-)
|
||||
if [ -n "$disp" ]; then
|
||||
# display_found="$disp"
|
||||
echo "Chosen "
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# 如果还没找到 DISPLAY,则尝试 fallback
|
||||
if [ -z "$display_found" ]; then
|
||||
# 常见默认::0
|
||||
display_found=":0"
|
||||
echo "Chosen default"
|
||||
fi
|
||||
|
||||
export DISPLAY="$display_found"
|
||||
|
||||
{
|
||||
echo "Chosen DISPLAY = $DISPLAY"
|
||||
} >> "$LOG_FILE"
|
||||
|
||||
# XAUTHORITY:假设用户的 .Xauthority 存在于 home 目录
|
||||
export XAUTHORITY="/home/unitree/.Xauthority"
|
||||
{
|
||||
echo "Using XAUTHORITY = $XAUTHORITY"
|
||||
} >> "$LOG_FILE"
|
||||
|
||||
# 最终执行 Python 程序(不后台化,用 exec 替换进程)
|
||||
exec python face_rec.py >> "$LOG_FILE" 2>&1
|
||||
Loading…
Reference in New Issue
Block a user