mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-17 18:01:26 +08:00
格式化工具部分: 1. pre-commit 引入 python 的格式化工具,包括 black(代码格式化)和 isort(对“包导入顺序”的规范) 2. 允许 prettier 对文档站的 markdown 文件格式化 3. 不允许 prettier 对 markdown 文件中的代码块的代码本身进行格式化 4. 升级了 pre-commit 的各个 hook 的版本 5. 优化了 pre-commit 的日志文本显示 容器总览部分: 1. 由原来的单一轻量环境转为区分空环境、轻量环境和全量环境 2. 空环境是裸 Linux 镜像(Ubuntu),为默认环境 3. 轻量环境适合开发文档站前端 4. 全量环境适合开发 MaaCore 5. 目前,全量环境完整包含了轻量环境,轻量环境完整包含了空环境 6. 在仓库 README.md 中更新了三个环境的描述,并将链接分别设置为对应环境的创建链接 **注意:没有修改文档站中的对应文件** 7. 在各个语言的开发指南的最后,移除了 Codespace 部分的“安装额外依赖”相关描述,且将链接设置为全量环境的创建链接 **注意:没有添加“开发文档站”的指南和对应 codespace 的使用方式** 容器的轻量环境和全量环境共有部分: 1. 安装 black 和 isort 包 2. 调整 VS Code 设置,取消先前(对 markdown 文件单独指定 markdownlint 扩展作为格式化工具)的错误设置,现在 markdown 文件仍然使用默认的 prettier 扩展作为格式化工具 3. 引入 markdown-all-in-one 扩展作为语法提示工具 4. 将 node_modules 和 3rdparty 排除在 VS Code 的文本的搜索路径之外 容器的全量环境部分: 1. 为 tools 下的所有 python 脚本安装依赖 2. 使用 tools/maadeps-download.py 下载 maadeps,且将必要二进制文件软链接到 /usr/local/bin/ 3. 使用 apt 安装 cmake 和 clangd-20,将后者通过 update-alternatives 设置为系统 clangd 的默认版本 4. 使用 cmake tools 扩展,并按照 Linux 编译方法进行配置 5. 使用 clang-format 作为 c/cpp 的格式化工具,clang-format 程序主体来自 maadeps(已经软链接到 /usr/local/bin/) 6. 使用 clangd 作为 c/cpp 的语法提示工具 7. 将 MaaDeps、install 和 build 排除在 VS Code 的文本搜索路径之外 其它手动调整: 1. 更新文档站的 package.json,指定 pnpm 包管理器的版本 2. 手动保证 markdown 文件中的列表前后有空行(注意到 MarkdownLint 官方规则不一定能精准定位所有“列表前后空行”的问题,详见:https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md032---lists-should-be-surrounded-by-blank-lines ) 3. 修改了部分 markdown 文件中的 json 代码块的语法问题 **注意:相同的问题并未全部发现,仅修改了两处** 4. 在 tools 目录中,一处 python 脚本的包名误用(本地包名和某个 pip 包重名),这里修改了相应代码 5. 在 tools 目录中,一处 python 脚本使用了弃用的包 cchardet 的问题,这里更换了推荐使用的功能相近的包并修改了相应代码 自动化脚本提交的修改: 1. 自动格式化了大量 tools 中的 python 脚本 2. 自动格式化了大量 docs 中的 markdown 文件 Commits: * chore: pre-commit引入black和isort规范py文件 * chore: Auto update by pre-commit hooks [skip changelog] * chore: devcontainer添加isort扩展,排序python导入 * chore: pre-commit任务命名及更名 * style: isort fix * chore: Auto update by pre-commit hooks [skip changelog] * chore: 更新pre-commit的hook版本 * fix: 模块名与第三方库重名,大忌 * chore: 容器构建时额外安装isort * docs: md -> markdown * chore: 容器安装python包和maadeps * fix: 修复过时python包 * chore: 指定pnpm版本 * chore: container支持选择轻量环境 * chore: 去掉rust * chore: add plain env * chore: 使用clangd语言服务器 * chore: 无需单独设置markdown的格式化工具 * chore: 更新安装的clangd版本 * docs: 简易文档适配 * docs: 在仓库README中重新编排codespaces相关指引 * chore: Auto update by pre-commit hooks [skip changelog] * style: 调整缩进 * chore: 格式化工具不用特意排除被gitignore忽略的文件 * chore: sh文件在gitattributes中单列一类 * chore: 格式化docs下的markdown文件 * chore: 暂时不修改md文件中的代码块 * style: 人为明确markdown中的部分列表相关格式 * docs: 补上部分markdown的json代码块中缺失的逗号 * chore: Auto update by pre-commit hooks [skip changelog] * chore: Auto update by pre-commit hooks [skip changelog] * fix: 补上tools的服务器排序相关代码中缺失的逗号 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: 使用maadeps的clangd * build: 更新maadeps工具链版本 * style: prettier fix * revert: 还原maadeps版本 * revert: 取消使用maadeps的clangd依赖,改用系统apt安装 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
424 lines
14 KiB
Python
424 lines
14 KiB
Python
try:
|
||
import cv2
|
||
import numpy as np
|
||
except ImportError:
|
||
print("Please install OpenCV and NumPy to run this script.")
|
||
print("You can install them using:")
|
||
print(" pip install opencv-python numpy")
|
||
exit(1)
|
||
|
||
from pathlib import Path
|
||
from typing import Optional, Tuple
|
||
|
||
|
||
class RoiCropper:
|
||
"""ROI裁剪工具类,支持实时预览和交互式选择"""
|
||
|
||
def __init__(self, std_width: int = 1280, std_height: int = 720):
|
||
self.std_width = std_width
|
||
self.std_height = std_height
|
||
self.std_ratio = std_width / std_height
|
||
|
||
# ROI选择状态
|
||
self.start_point: Optional[Tuple[int, int]] = None
|
||
self.end_point: Optional[Tuple[int, int]] = None
|
||
self.cropping = False
|
||
self.current_mouse_pos: Optional[Tuple[int, int]] = None
|
||
|
||
# 当前处理的图像
|
||
self.original_image: Optional[np.ndarray] = None
|
||
self.display_image: Optional[np.ndarray] = None
|
||
self.current_size: Optional[Tuple[int, int]] = None
|
||
|
||
# 扩展参数
|
||
self.horizontal_expansion = 100
|
||
self.vertical_expansion = 100
|
||
|
||
# 颜色定义
|
||
self.PREVIEW_COLOR = (255, 255, 0) # 预览
|
||
self.SELECTED_COLOR = (0, 255, 0) # 已选择
|
||
self.EXPANDED_COLOR = (0, 0, 255) # 扩展区域
|
||
|
||
self._setup_window()
|
||
|
||
def _setup_window(self):
|
||
"""设置OpenCV窗口"""
|
||
cv2.namedWindow("ROI Cropper", cv2.WINDOW_AUTOSIZE)
|
||
cv2.setMouseCallback("ROI Cropper", self._mouse_callback)
|
||
|
||
def _mouse_callback(self, event, x, y, flags, param):
|
||
"""鼠标事件回调函数"""
|
||
if event == cv2.EVENT_LBUTTONDOWN:
|
||
self.start_point = (x, y)
|
||
self.end_point = None
|
||
self.cropping = True
|
||
self._update_display()
|
||
|
||
elif event == cv2.EVENT_MOUSEMOVE:
|
||
self.current_mouse_pos = (x, y)
|
||
if self.cropping and self.start_point:
|
||
self._update_display()
|
||
|
||
elif event == cv2.EVENT_LBUTTONUP:
|
||
if self.start_point:
|
||
self.end_point = (x, y)
|
||
self.cropping = False
|
||
self._update_display()
|
||
|
||
def _update_display(self):
|
||
"""更新显示图像"""
|
||
if self.original_image is None:
|
||
return
|
||
|
||
display = self.original_image.copy()
|
||
|
||
# 显示当前ROI选择
|
||
if self.start_point:
|
||
if self.cropping and self.current_mouse_pos:
|
||
# 实时预览 - 黄色虚线
|
||
end_pos = self.current_mouse_pos
|
||
self._draw_rectangle(
|
||
display,
|
||
self.start_point,
|
||
end_pos,
|
||
self.PREVIEW_COLOR,
|
||
2,
|
||
is_dashed=True,
|
||
)
|
||
elif self.end_point:
|
||
# 最终选择 - 绿色实线
|
||
self._draw_rectangle(
|
||
display, self.start_point, self.end_point, self.SELECTED_COLOR, 2
|
||
)
|
||
# 显示扩展区域 - 红色虚线
|
||
expanded_rect = self._calculate_expanded_rect()
|
||
if expanded_rect:
|
||
self._draw_rectangle(
|
||
display,
|
||
(expanded_rect[0], expanded_rect[1]),
|
||
(
|
||
expanded_rect[0] + expanded_rect[2],
|
||
expanded_rect[1] + expanded_rect[3],
|
||
),
|
||
self.EXPANDED_COLOR,
|
||
1,
|
||
is_dashed=True,
|
||
)
|
||
|
||
# 添加操作提示
|
||
# self._draw_instructions(display)
|
||
|
||
cv2.imshow("ROI Cropper", display)
|
||
|
||
def _draw_rectangle(self, img, pt1, pt2, color, thickness, is_dashed=False):
|
||
"""绘制矩形(支持虚线)"""
|
||
if is_dashed:
|
||
# 绘制虚线矩形
|
||
self._draw_dashed_rectangle(img, pt1, pt2, color, thickness)
|
||
else:
|
||
cv2.rectangle(img, pt1, pt2, color, thickness)
|
||
|
||
def _draw_dashed_rectangle(self, img, pt1, pt2, color, thickness):
|
||
"""绘制虚线矩形"""
|
||
x1, y1 = pt1
|
||
x2, y2 = pt2
|
||
|
||
# 确保坐标顺序正确
|
||
x1, x2 = min(x1, x2), max(x1, x2)
|
||
y1, y2 = min(y1, y2), max(y1, y2)
|
||
|
||
dash_length = 10
|
||
|
||
# 绘制四条边的虚线
|
||
self._draw_dashed_line(
|
||
img, (x1, y1), (x2, y1), color, thickness, dash_length
|
||
) # 上边
|
||
self._draw_dashed_line(
|
||
img, (x2, y1), (x2, y2), color, thickness, dash_length
|
||
) # 右边
|
||
self._draw_dashed_line(
|
||
img, (x2, y2), (x1, y2), color, thickness, dash_length
|
||
) # 下边
|
||
self._draw_dashed_line(
|
||
img, (x1, y2), (x1, y1), color, thickness, dash_length
|
||
) # 左边
|
||
|
||
def _draw_dashed_line(self, img, pt1, pt2, color, thickness, dash_length):
|
||
"""绘制虚线"""
|
||
x1, y1 = pt1
|
||
x2, y2 = pt2
|
||
|
||
dist = int(np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2))
|
||
dashes = dist // (dash_length * 2)
|
||
|
||
if dashes == 0:
|
||
cv2.line(img, pt1, pt2, color, thickness)
|
||
return
|
||
|
||
for i in range(dashes):
|
||
start_ratio = (2 * i * dash_length) / dist
|
||
end_ratio = ((2 * i + 1) * dash_length) / dist
|
||
|
||
start_x = int(x1 + (x2 - x1) * start_ratio)
|
||
start_y = int(y1 + (y2 - y1) * start_ratio)
|
||
end_x = int(x1 + (x2 - x1) * end_ratio)
|
||
end_y = int(y1 + (y2 - y1) * end_ratio)
|
||
|
||
cv2.line(img, (start_x, start_y), (end_x, end_y), color, thickness)
|
||
|
||
def _draw_instructions(self, img):
|
||
"""在图像上绘制操作说明"""
|
||
instructions = [
|
||
"Mouse: Drag to select ROI",
|
||
"S: Save current ROI",
|
||
"R: Reset selection",
|
||
"Q: Next image / Quit",
|
||
"Blue: Preview",
|
||
"Green: Original ORI",
|
||
"Red: Expanded ROI",
|
||
]
|
||
|
||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||
font_scale = 0.6
|
||
color = (255, 255, 255)
|
||
thickness = 1
|
||
|
||
for i, text in enumerate(instructions):
|
||
y = 25 + i * 25
|
||
# 绘制黑色背景
|
||
(text_width, text_height), _ = cv2.getTextSize(
|
||
text, font, font_scale, thickness
|
||
)
|
||
cv2.rectangle(
|
||
img, (10, y - text_height - 5), (15 + text_width, y + 5), (0, 0, 0), -1
|
||
)
|
||
# 绘制白色文字
|
||
cv2.putText(img, text, (10, y), font, font_scale, color, thickness)
|
||
|
||
def _calculate_expanded_rect(self) -> Optional[Tuple[int, int, int, int]]:
|
||
"""计算扩展后的矩形区域"""
|
||
if not (self.start_point and self.end_point and self.current_size):
|
||
return None
|
||
|
||
x1, y1 = self.start_point
|
||
x2, y2 = self.end_point
|
||
|
||
# 确保坐标顺序正确
|
||
left = min(x1, x2)
|
||
right = max(x1, x2)
|
||
top = min(y1, y2)
|
||
bottom = max(y1, y2)
|
||
|
||
# 计算扩展区域
|
||
expand_x = self.horizontal_expansion // 2
|
||
expand_y = self.vertical_expansion // 2
|
||
|
||
final_x = max(0, left - expand_x)
|
||
final_y = max(0, top - expand_y)
|
||
final_w = min(
|
||
self.current_size[0] - final_x, (right - left) + self.horizontal_expansion
|
||
)
|
||
final_h = min(
|
||
self.current_size[1] - final_y, (bottom - top) + self.vertical_expansion
|
||
)
|
||
|
||
return (final_x, final_y, final_w, final_h)
|
||
|
||
def resize_image(self, image: np.ndarray) -> np.ndarray:
|
||
"""调整图像尺寸以符合标准比例"""
|
||
current_ratio = image.shape[1] / image.shape[0]
|
||
|
||
if current_ratio >= self.std_ratio:
|
||
# 宽屏或16:9,按高度缩放
|
||
new_height = self.std_height
|
||
new_width = int(current_ratio * self.std_height)
|
||
else:
|
||
# 偏正方形屏幕,按宽度缩放
|
||
new_width = self.std_width
|
||
new_height = int(self.std_width / current_ratio)
|
||
|
||
resized = cv2.resize(
|
||
image, (new_width, new_height), interpolation=cv2.INTER_AREA
|
||
)
|
||
self.current_size = (new_width, new_height)
|
||
return resized
|
||
|
||
def reset_selection(self):
|
||
"""重置ROI选择"""
|
||
self.start_point = None
|
||
self.end_point = None
|
||
self.cropping = False
|
||
self.current_mouse_pos = None
|
||
self._update_display()
|
||
|
||
def has_valid_selection(self) -> bool:
|
||
"""检查是否有有效的ROI选择"""
|
||
return self.start_point is not None and self.end_point is not None
|
||
|
||
def get_roi_info(
|
||
self,
|
||
) -> Optional[Tuple[Tuple[int, int, int, int], Tuple[int, int, int, int]]]:
|
||
"""获取ROI信息,返回(原始ROI, 扩展ROI)"""
|
||
if not self.has_valid_selection():
|
||
return None
|
||
|
||
x1, y1 = self.start_point
|
||
x2, y2 = self.end_point
|
||
|
||
# 原始ROI
|
||
left = min(x1, x2)
|
||
right = max(x1, x2)
|
||
top = min(y1, y2)
|
||
bottom = max(y1, y2)
|
||
original_roi = (left, top, right - left, bottom - top)
|
||
|
||
# 扩展ROI
|
||
expanded_rect = self._calculate_expanded_rect()
|
||
if expanded_rect is None:
|
||
return None
|
||
|
||
return original_roi, expanded_rect
|
||
|
||
def save_roi(self, image: np.ndarray, filename: Path, dst_path: Path) -> bool:
|
||
"""保存ROI图像"""
|
||
roi_info = self.get_roi_info()
|
||
if not roi_info:
|
||
print("No valid ROI selected!")
|
||
return False
|
||
|
||
original_roi, expanded_roi = roi_info
|
||
left, top, width, height = original_roi
|
||
|
||
# 裁剪ROI区域
|
||
roi_image = image[top : top + height, left : left + width]
|
||
|
||
# 生成文件名
|
||
exp_x, exp_y, exp_w, exp_h = expanded_roi
|
||
dst_filename = f"{filename.stem}_{exp_x},{exp_y},{exp_w},{exp_h}.png"
|
||
dst_file_path = dst_path / dst_filename
|
||
|
||
# 确保目标目录存在
|
||
dst_path.mkdir(exist_ok=True)
|
||
|
||
# 保存图像
|
||
success = cv2.imwrite(str(dst_file_path), roi_image)
|
||
|
||
if success:
|
||
print(f"✓ Saved: {dst_filename}")
|
||
print(f" Original ROI: {left}, {top}, {width}, {height}")
|
||
print(f" Expanded ROI: {exp_x}, {exp_y}, {exp_w}, {exp_h}")
|
||
else:
|
||
print(f"✗ Failed to save: {dst_filename}")
|
||
|
||
return success
|
||
|
||
def process_image(self, image: np.ndarray):
|
||
"""处理单张图像"""
|
||
self.original_image = self.resize_image(image)
|
||
self.reset_selection()
|
||
self._update_display()
|
||
|
||
def cleanup(self):
|
||
"""清理资源"""
|
||
cv2.destroyAllWindows()
|
||
|
||
|
||
def main():
|
||
print("=" * 60)
|
||
print("ROI Cropping Tool")
|
||
print("=" * 60)
|
||
print("Instructions:")
|
||
print("• Put 16:9 images under ./src")
|
||
print("• Drag mouse to select ROI (real-time preview)")
|
||
print("• Press 'S' to save current ROI")
|
||
print("• Press 'R' to reset selection")
|
||
print("• Press 'Q' to go to next image or quit")
|
||
print("• Green: Original ORI, Red: Expanded ROI")
|
||
print("• Cropped images will be saved in ./dst")
|
||
print("=" * 60)
|
||
# 初始化
|
||
cropper = RoiCropper()
|
||
|
||
# 设置路径
|
||
base_path = Path(__file__).parent.resolve()
|
||
src_path = base_path / "src"
|
||
dst_path = base_path / "dst"
|
||
|
||
# 检查源目录
|
||
if not src_path.exists():
|
||
print(f"❌ Source directory not found: {src_path}")
|
||
print("Please create the 'src' directory and put your images there.")
|
||
return
|
||
|
||
# 获取图像文件
|
||
image_files = (
|
||
list(src_path.glob("*.png"))
|
||
+ list(src_path.glob("*.jpg"))
|
||
+ list(src_path.glob("*.jpeg"))
|
||
)
|
||
|
||
if not image_files:
|
||
print(f"❌ No image files found in: {src_path}")
|
||
print("Supported formats: PNG, JPG, JPEG")
|
||
return
|
||
|
||
print(f"📁 Found {len(image_files)} image(s) to process")
|
||
print()
|
||
|
||
try:
|
||
# 处理每张图像
|
||
for i, filename in enumerate(image_files, 1):
|
||
print(f"📸 Processing ({i}/{len(image_files)}): {filename.name}")
|
||
|
||
# 读取图像
|
||
image = cv2.imread(str(filename))
|
||
if image is None:
|
||
print(f"❌ Failed to load image: {filename}")
|
||
continue
|
||
|
||
# 处理图像
|
||
cropper.process_image(image)
|
||
|
||
# 等待用户操作
|
||
while True:
|
||
key = cv2.waitKey(1) & 0xFF
|
||
|
||
if key == ord("s") or key == ord("S"):
|
||
# 保存ROI
|
||
if cropper.has_valid_selection():
|
||
if cropper.save_roi(cropper.original_image, filename, dst_path):
|
||
print()
|
||
break
|
||
else:
|
||
print("❌ Failed to save ROI!")
|
||
else:
|
||
print("⚠️ Please select a ROI first!")
|
||
|
||
elif key == ord("r") or key == ord("R"):
|
||
# 重置选择
|
||
cropper.reset_selection()
|
||
print("🔄 Selection reset")
|
||
|
||
elif key == ord("q") or key == ord("Q"):
|
||
# 下一张图像或退出
|
||
if cropper.has_valid_selection():
|
||
print("⚠️ ROI selected but not saved. Moving to next image...")
|
||
else:
|
||
print("⏭️ Moving to next image...")
|
||
print()
|
||
break
|
||
|
||
elif key == 27: # ESC
|
||
print("🛑 Exiting...")
|
||
return
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n🛑 Interrupted by user")
|
||
finally:
|
||
cropper.cleanup()
|
||
print("✅ Done!")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|