增加运维机器人单元
This commit is contained in:
1 parent
479d263b88
commit
1b60ab0a2e
42 files changed
+20469
No files matched your search
Executable
+392
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env python3
|
||||
"""摄像头拍照工具 - 获取相机设备列表、选择指定相机拍照、保存照片。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
|
||||
# 定义默认的截图保存目录
|
||||
CAMERA_DIR = os.path.expanduser("~/.aibox/camera")
|
||||
|
||||
|
||||
def ensure_camera_dir():
|
||||
"""确保摄像头照片保存目录存在"""
|
||||
os.makedirs(CAMERA_DIR, exist_ok=True)
|
||||
return CAMERA_DIR
|
||||
|
||||
|
||||
def list_cameras(max_test=10):
|
||||
"""列出可用的摄像头设备"""
|
||||
available_cameras = []
|
||||
|
||||
for i in range(max_test):
|
||||
cap = cv2.VideoCapture(i)
|
||||
if cap.isOpened():
|
||||
ret, frame = cap.read()
|
||||
if ret:
|
||||
height, width = frame.shape[:2]
|
||||
backend = cap.getBackendName()
|
||||
|
||||
camera_info = {
|
||||
"id": i,
|
||||
"resolution": f"{width}x{height}",
|
||||
"backend": backend,
|
||||
"status": "可用"
|
||||
}
|
||||
|
||||
# 在Linux系统上尝试获取设备路径
|
||||
if sys.platform.startswith('linux'):
|
||||
dev_path = f"/dev/video{i}"
|
||||
if os.path.exists(dev_path):
|
||||
camera_info["device_path"] = dev_path
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['udevadm', 'info', '--query=property', '--name', dev_path],
|
||||
capture_output=True, text=True, timeout=2
|
||||
)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'ID_MODEL=' in line:
|
||||
camera_info["model"] = line.split('=')[1].strip()
|
||||
elif 'ID_VENDOR=' in line:
|
||||
camera_info["vendor"] = line.split('=')[1].strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
available_cameras.append(camera_info)
|
||||
cap.release()
|
||||
else:
|
||||
cap.release()
|
||||
|
||||
return available_cameras
|
||||
|
||||
|
||||
def get_camera_info(camera_id):
|
||||
"""获取指定摄像头的详细信息"""
|
||||
cap = cv2.VideoCapture(camera_id)
|
||||
if not cap.isOpened():
|
||||
return None
|
||||
|
||||
info = {
|
||||
"id": camera_id,
|
||||
"properties": {}
|
||||
}
|
||||
|
||||
# 获取各种摄像头属性
|
||||
properties = [
|
||||
(cv2.CAP_PROP_FRAME_WIDTH, "width"),
|
||||
(cv2.CAP_PROP_FRAME_HEIGHT, "height"),
|
||||
(cv2.CAP_PROP_FPS, "fps"),
|
||||
(cv2.CAP_PROP_BRIGHTNESS, "brightness"),
|
||||
(cv2.CAP_PROP_CONTRAST, "contrast"),
|
||||
(cv2.CAP_PROP_SATURATION, "saturation"),
|
||||
(cv2.CAP_PROP_HUE, "hue"),
|
||||
(cv2.CAP_PROP_GAIN, "gain"),
|
||||
(cv2.CAP_PROP_EXPOSURE, "exposure"),
|
||||
(cv2.CAP_PROP_FOCUS, "focus"),
|
||||
(cv2.CAP_PROP_AUTO_EXPOSURE, "auto_exposure"),
|
||||
(cv2.CAP_PROP_AUTO_WB, "auto_white_balance"),
|
||||
(cv2.CAP_PROP_WB_TEMPERATURE, "white_balance_temperature"),
|
||||
]
|
||||
|
||||
for prop_id, prop_name in properties:
|
||||
value = cap.get(prop_id)
|
||||
if value >= 0:
|
||||
info["properties"][prop_name] = value
|
||||
|
||||
ret, frame = cap.read()
|
||||
if ret:
|
||||
height, width = frame.shape[:2]
|
||||
info["current_resolution"] = f"{width}x{height}"
|
||||
info["channels"] = frame.shape[2] if len(frame.shape) > 2 else 1
|
||||
|
||||
cap.release()
|
||||
|
||||
if sys.platform.startswith('linux'):
|
||||
dev_path = f"/dev/video{camera_id}"
|
||||
if os.path.exists(dev_path):
|
||||
info["device_path"] = dev_path
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def capture_photo(camera_id=0, save_path=None, resolution=None, wait_time=1, exposure=None):
|
||||
"""调用指定摄像头拍照"""
|
||||
cap = cv2.VideoCapture(camera_id)
|
||||
if not cap.isOpened():
|
||||
return None, f"无法打开摄像头 #{camera_id},请检查摄像头连接", None, None
|
||||
|
||||
if resolution:
|
||||
try:
|
||||
width, height = map(int, resolution.lower().replace('x', ' ').split())
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 如果指定了曝光值,关闭自动曝光并设置手动曝光
|
||||
if exposure is not None:
|
||||
# 0.25 = 手动模式(部分后端用 1),3 = 自动模式
|
||||
cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25)
|
||||
cap.set(cv2.CAP_PROP_EXPOSURE, exposure)
|
||||
|
||||
time.sleep(wait_time)
|
||||
|
||||
for _ in range(5):
|
||||
cap.read()
|
||||
|
||||
ret, frame = cap.read()
|
||||
cap.release()
|
||||
|
||||
if not ret:
|
||||
return None, "拍照失败,无法获取图像帧", None, None
|
||||
|
||||
actual_height, actual_width = frame.shape[:2]
|
||||
|
||||
if save_path is None:
|
||||
ensure_camera_dir()
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"camera_{camera_id}_{timestamp}.jpg"
|
||||
save_path = os.path.join(CAMERA_DIR, filename)
|
||||
else:
|
||||
save_dir = os.path.dirname(save_path)
|
||||
if save_dir:
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
if not os.path.splitext(save_path)[1]:
|
||||
save_path += '.jpg'
|
||||
|
||||
cv2.imwrite(save_path, frame)
|
||||
return save_path, None, actual_width, actual_height
|
||||
|
||||
|
||||
def format_camera_list(cameras):
|
||||
"""格式化相机列表输出"""
|
||||
if not cameras:
|
||||
return "❌ 未找到可用的摄像头设备"
|
||||
|
||||
lines = ["📷 可用摄像头设备:"]
|
||||
for cam in cameras:
|
||||
model_info = ""
|
||||
if "model" in cam:
|
||||
model_info = f" ({cam.get('vendor', '')} {cam['model']})".strip()
|
||||
|
||||
dev_path = cam.get("device_path", f"Camera #{cam['id']}")
|
||||
lines.append(f" [{cam['id']}] {dev_path}{model_info}")
|
||||
lines.append(f" 分辨率: {cam['resolution']}, 后端: {cam['backend']}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def list_recent_photos(limit=10):
|
||||
"""列出最近的摄像头照片"""
|
||||
ensure_camera_dir()
|
||||
|
||||
if not os.path.exists(CAMERA_DIR):
|
||||
return f"摄像头照片目录不存在: {CAMERA_DIR}"
|
||||
|
||||
photos = []
|
||||
for f in os.listdir(CAMERA_DIR):
|
||||
if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||
filepath = os.path.join(CAMERA_DIR, f)
|
||||
mtime = os.path.getmtime(filepath)
|
||||
size = os.path.getsize(filepath)
|
||||
photos.append((mtime, f, size))
|
||||
|
||||
photos.sort(reverse=True)
|
||||
|
||||
if not photos:
|
||||
return f"摄像头照片目录中没有照片: {CAMERA_DIR}"
|
||||
|
||||
result = [f"📸 最近的摄像头照片 (共 {len(photos)} 个):"]
|
||||
for i, (mtime, fname, size) in enumerate(photos[:limit], 1):
|
||||
dt = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime))
|
||||
|
||||
if size < 1024:
|
||||
size_str = f"{size}B"
|
||||
elif size < 1024 * 1024:
|
||||
size_str = f"{size/1024:.1f}KB"
|
||||
else:
|
||||
size_str = f"{size/1024/1024:.1f}MB"
|
||||
|
||||
result.append(f" {i}. {fname} - {dt} ({size_str})")
|
||||
|
||||
if len(photos) > limit:
|
||||
result.append(f" ... 还有 {len(photos) - limit} 个文件")
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="摄像头拍照工具 - 获取相机设备列表、选择指定相机拍照、保存照片。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 列出所有可用摄像头
|
||||
camera --action list
|
||||
|
||||
# 查看摄像头详细信息
|
||||
camera --action info --camera-id 0
|
||||
|
||||
# 拍照(自动生成文件名)
|
||||
camera --action capture --camera-id 0
|
||||
|
||||
# 拍照并指定分辨率
|
||||
camera --action capture --camera-id 0 --resolution "1920x1080"
|
||||
|
||||
# 拍照并自定义文件名
|
||||
camera --action capture --camera-id 0 --filename "my_photo"
|
||||
|
||||
# 拍照并等待3秒预热
|
||||
camera --action capture --camera-id 0 --wait-time 3
|
||||
|
||||
# 手动降低曝光(数值越低越暗,-7 比 -5 更暗)
|
||||
camera --action capture --camera-id 0 --exposure -7
|
||||
|
||||
# 调整曝光至较亮
|
||||
camera --action capture --camera-id 0 --exposure -4
|
||||
|
||||
# 列出最近的照片
|
||||
camera --action list-photos --limit 5
|
||||
|
||||
照片保存位置:
|
||||
~/.aibox/camera/
|
||||
自动命名格式: camera_{摄像头ID}_{YYYYMMDD_HHMMSS}.jpg
|
||||
|
||||
需要安装:
|
||||
pip install opencv-python
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--action", "-a", required=True,
|
||||
choices=["list", "info", "capture", "list-photos"],
|
||||
help="操作类型")
|
||||
|
||||
# 摄像头参数
|
||||
parser.add_argument("--camera-id", "-c", type=int, default=0,
|
||||
help="摄像头ID(默认: 0)")
|
||||
|
||||
parser.add_argument("--resolution", "-r",
|
||||
help="照片分辨率,格式: 宽度x高度(如 1920x1080)")
|
||||
|
||||
parser.add_argument("--wait-time", "-w", type=float, default=1,
|
||||
help="等待摄像头预热时间(秒,默认: 1)")
|
||||
|
||||
parser.add_argument("--exposure", "-e", type=float,
|
||||
help="曝光值(越低画面越暗。设置后自动关闭自动曝光,值范围取决于摄像头驱动,通常 -4 到 -7 为常见范围)")
|
||||
|
||||
parser.add_argument("--filename", "-f",
|
||||
help="自定义文件名(不含路径,自动保存到默认目录)")
|
||||
|
||||
parser.add_argument("--limit", "-l", type=int, default=10,
|
||||
help="列出照片时的数量限制(默认: 10)")
|
||||
|
||||
args = parser.parse_args()
|
||||
# 延迟导入 cv2,确保 --help 不受依赖缺失影响
|
||||
global cv2
|
||||
try:
|
||||
import cv2
|
||||
except ImportError:
|
||||
print("❌ 缺少依赖库: opencv-python。请运行: pip install opencv-python")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# 执行操作
|
||||
try:
|
||||
if args.action == "list":
|
||||
print("🔍 正在扫描摄像头设备...")
|
||||
cameras = list_cameras(10)
|
||||
print(format_camera_list(cameras))
|
||||
|
||||
if cameras:
|
||||
print(f"\n💡 使用建议:")
|
||||
print(f" 要使用摄像头 #{cameras[0]['id']} 拍照,请执行:")
|
||||
print(f" camera --action capture --camera-id {cameras[0]['id']}")
|
||||
print(f" 要查看摄像头详细信息,请执行:")
|
||||
print(f" camera --action info --camera-id {cameras[0]['id']}")
|
||||
|
||||
elif args.action == "list-photos":
|
||||
result = list_recent_photos(args.limit)
|
||||
print(result)
|
||||
|
||||
elif args.action == "info":
|
||||
print(f"🔍 正在获取摄像头 #{args.camera_id} 的详细信息...")
|
||||
|
||||
info = get_camera_info(args.camera_id)
|
||||
if not info:
|
||||
print(f"❌ 无法打开摄像头 #{args.camera_id},请检查设备是否存在")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"📷 摄像头 #{args.camera_id} 详细信息:")
|
||||
if "device_path" in info:
|
||||
print(f" 设备路径: {info['device_path']}")
|
||||
print(f" 当前分辨率: {info.get('current_resolution', '未知')}")
|
||||
if "channels" in info:
|
||||
print(f" 色彩通道: {info['channels']}")
|
||||
|
||||
if info["properties"]:
|
||||
print(f"\n 支持设置的属性:")
|
||||
for name, value in info["properties"].items():
|
||||
print(f" {name}: {value}")
|
||||
|
||||
print(f"\n💡 要拍照请执行:")
|
||||
print(f" camera --action capture --camera-id {args.camera_id}")
|
||||
|
||||
elif args.action == "capture":
|
||||
ensure_camera_dir()
|
||||
|
||||
if args.filename:
|
||||
save_path = os.path.join(CAMERA_DIR, args.filename)
|
||||
else:
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
save_path = os.path.join(CAMERA_DIR, f"camera_{args.camera_id}_{timestamp}.jpg")
|
||||
|
||||
print(f"📸 正在使用摄像头 #{args.camera_id} 拍照...")
|
||||
if args.resolution:
|
||||
print(f" 请求分辨率: {args.resolution}")
|
||||
print(f" 预热时间: {args.wait_time}秒")
|
||||
if args.exposure is not None:
|
||||
print(f" 曝光值: {args.exposure}(手动模式,自动曝光已关闭)")
|
||||
print(f" 保存目录: {CAMERA_DIR}")
|
||||
|
||||
image_path, error, width, height = capture_photo(
|
||||
camera_id=args.camera_id,
|
||||
save_path=save_path,
|
||||
resolution=args.resolution,
|
||||
wait_time=args.wait_time,
|
||||
exposure=args.exposure
|
||||
)
|
||||
|
||||
if error:
|
||||
print(f"❌ {error}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✅ 拍照成功!")
|
||||
print(f" 📷 实际分辨率: {width}x{height}")
|
||||
print(f" 💾 保存路径: {image_path}")
|
||||
|
||||
if os.path.exists(image_path):
|
||||
file_size = os.path.getsize(image_path)
|
||||
if file_size < 1024:
|
||||
size_str = f"{file_size}B"
|
||||
elif file_size < 1024 * 1024:
|
||||
size_str = f"{file_size/1024:.1f}KB"
|
||||
else:
|
||||
size_str = f"{file_size/1024/1024:.1f}MB"
|
||||
print(f" 📦 文件大小: {size_str}")
|
||||
|
||||
photos = [f for f in os.listdir(CAMERA_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))]
|
||||
print(f" 📁 该目录共有 {len(photos)} 张照片")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user