显示机械臂摄像头图片

This commit is contained in:
ywb
2026-06-05 10:27:42 +08:00
parent 671351aa89
commit 4126e01bba
3 changed files with 128 additions and 21 deletions
+46 -16
View File
@@ -1574,31 +1574,61 @@ def api_qr_read_angles(qr_id):
@app.route("/api/qr/scan/<qr_id>", methods=["POST"]) @app.route("/api/qr/scan/<qr_id>", methods=["POST"])
def api_qr_config_scan(qr_id): def api_qr_config_scan(qr_id):
"""获取机械臂摄像头图像,识别二维码并保存到指定配置项""" """获取机械臂摄像头图像,识别二维码并保存到指定配置项pyzbar 优先,OpenCV 兜底)"""
import requests import requests
try: try:
jpg_bytes = None
# 多次尝试获取清晰帧
for _ in range(3):
try:
r = requests.get(ARM_CAMERA_CONFIG.get("snapshot_url", ARM_CAMERA_CONFIG["url"]), timeout=8)
if r.status_code == 200 and r.content:
jpg_bytes = r.content
break
except:
pass
import time; time.sleep(0.3)
if jpg_bytes is None:
return jsonify({"ok": False, "error": "无法连接机械臂摄像头"}), 400
import cv2 import cv2
import numpy as np import numpy as np
# 从机械臂摄像头 snapshot 端点拉取一帧 JPEG
r = requests.get(ARM_CAMERA_CONFIG.get("snapshot_url", ARM_CAMERA_CONFIG["url"]), timeout=8)
if r.status_code != 200 or not r.content:
return jsonify({"ok": False, "error": "无法连接机械臂摄像头"}), 400
jpg_bytes = r.content
# 解码为 numpy 数组并检测二维码
nparr = np.frombuffer(jpg_bytes, np.uint8) nparr = np.frombuffer(jpg_bytes, np.uint8)
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR) frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if frame is None: if frame is None:
return jsonify({"ok": False, "error": "图像解码失败"}), 400 return jsonify({"ok": False, "error": "图像解码失败"}), 400
# 使用 OpenCV QRCodeDetector 检测
detector = cv2.QRCodeDetector() result = None
result, _, _ = detector.detectAndDecode(frame)
if result and len(result.strip()) > 0: # 方法1: pyzbar(识别率更高)
result = result.strip() try:
from PIL import Image
from pyzbar.pyzbar import decode as pyzbar_decode
pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
codes = pyzbar_decode(pil_img)
if codes and codes[0].data:
result = codes[0].data.decode("utf-8").strip()
logger.info(f"pyzbar 扫码成功: {result}")
except Exception as e:
logger.debug(f"pyzbar 扫码失败: {e}")
# 方法2: OpenCV QRCodeDetector(兜底)
if not result:
try:
detector = cv2.QRCodeDetector()
val, _, _ = detector.detectAndDecode(frame)
if val and len(val.strip()) > 0:
result = val.strip()
logger.info(f"OpenCV 扫码成功: {result}")
except Exception as e:
logger.debug(f"OpenCV 扫码失败: {e}")
if result:
# 保存到配置项 # 保存到配置项
for entry in gs.qr_config: for entry in gs.qr_config:
if entry["id"] == qr_id: if entry["id"] == qr_id:
entry["qr_value"] = result entry["qr_value"] = result
# 尝试匹配机型
matched_model = None matched_model = None
for model in gs.models_config: for model in gs.models_config:
prefix = model.get("serial_prefix", "") prefix = model.get("serial_prefix", "")
@@ -1615,10 +1645,10 @@ def api_qr_config_scan(qr_id):
"model_name": matched_model["name"] if matched_model else "" "model_name": matched_model["name"] if matched_model else ""
}) })
return jsonify({"ok": False, "error": f"二维码 {qr_id} 不存在"}), 404 return jsonify({"ok": False, "error": f"二维码 {qr_id} 不存在"}), 404
else:
return jsonify({"ok": False, "error": "未检测到二维码"}) return jsonify({"ok": False, "error": "未检测到二维码,请调整机械臂姿态或手动输入"})
except Exception as ex: except Exception as ex:
logger.error(f"QR 扫描机械臂摄像头失败: {ex}") logger.error(f"QR 扫描失败: {ex}")
return jsonify({"ok": False, "error": f"扫描失败: {str(ex)}"}), 400 return jsonify({"ok": False, "error": f"扫描失败: {str(ex)}"}), 400
+43 -2
View File
@@ -57,7 +57,13 @@ createApp({
qrScanning: false, qrScanning: false,
qrConfigs: [], qrConfigs: [],
qrScanningId: null, qrScanningId: null,
showQrInputDialog: false,
qrInputId: null,
qrInputValue: '',
armCameraUrl: API + '/api/camera/arm_preview', armCameraUrl: API + '/api/camera/arm_preview',
armSnapshotUrl: '',
showArmSnapshot: false,
armSnapshotLoading: false,
newQrName: '', newQrName: '',
armInitialPose: [0, 0, 0, 0, 0, 0], armInitialPose: [0, 0, 0, 0, 0, 0],
} }
@@ -1104,10 +1110,39 @@ createApp({
if (data.model_name) msg += ' 匹配机型: ' + data.model_name if (data.model_name) msg += ' 匹配机型: ' + data.model_name
else msg += ' 未匹配到机型' else msg += ' 未匹配到机型'
alert(msg) alert(msg)
} else { alert(data.error || '扫描失败') } } else {
} catch (e) { alert('扫描失败: ' + e.message) } // 自动扫描失败,弹出手动输入框
this.qrInputId = qrId
this.qrInputValue = ''
this.showQrInputDialog = true
}
} catch (e) {
this.qrInputId = qrId
this.qrInputValue = ''
this.showQrInputDialog = true
}
this.qrScanningId = null this.qrScanningId = null
}, },
async submitManualQr() {
if (!this.qrInputId || !this.qrInputValue.trim()) return
try {
const res = await fetch(API + '/api/qr/configs/' + this.qrInputId, {
method: 'PUT',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ qr_value: this.qrInputValue.trim() })
})
const data = await res.json()
if (data.ok) {
await this.loadQrConfigs()
alert('✅ 二维码值已保存')
} else {
alert('保存失败')
}
} catch (e) { alert('保存失败: ' + e.message) }
this.showQrInputDialog = false
this.qrInputId = null
this.qrInputValue = ''
},
async applyQrAngles(qrId) { async applyQrAngles(qrId) {
if (!this.armConnected) { alert('机械臂未连接'); return } if (!this.armConnected) { alert('机械臂未连接'); return }
const q = this.qrConfigs.find(x => x.id === qrId) const q = this.qrConfigs.find(x => x.id === qrId)
@@ -1126,6 +1161,12 @@ createApp({
onArmPreviewError() { onArmPreviewError() {
// 机械臂摄像头预览失败,静默处理 // 机械臂摄像头预览失败,静默处理
}, },
async captureArmSnapshot() {
this.armSnapshotLoading = true
this.armSnapshotUrl = API + '/api/camera/arm_refresh?t=' + Date.now()
this.showArmSnapshot = true
setTimeout(() => { this.armSnapshotLoading = false }, 500)
},
async agvResetCollision() { async agvResetCollision() {
if (!this.agvConnected) { if (!this.agvConnected) {
alert('AGV 未连接') alert('AGV 未连接')
+39 -3
View File
@@ -433,11 +433,16 @@
<h2>📷 二维码配置</h2> <h2>📷 二维码配置</h2>
<p style="color:#9aa0a6;font-size:13px;margin-bottom:16px">配置机械臂姿态(6个关节角度),通过机械臂摄像头识别二维码并匹配机型。</p> <p style="color:#9aa0a6;font-size:13px;margin-bottom:16px">配置机械臂姿态(6个关节角度),通过机械臂摄像头识别二维码并匹配机型。</p>
<!-- 机械臂摄像头画面 --> <!-- 机械臂摄像头画面 -->
<div style="margin-bottom:16px"> <div style="margin-bottom:8px">
<div class="camera-preview" style="max-width:640px"> <div class="camera-preview" style="max-width:640px">
<img :src="armCameraUrl" @error="onArmPreviewError" style="width:100%;border-radius:8px"> <img :src="armCameraUrl" @error="onArmPreviewError" style="width:100%;border-radius:8px">
</div> </div>
</div> </div>
<div style="display:flex;justify-content:flex-end;margin-bottom:16px">
<button class="btn btn-secondary btn-small" @click="captureArmSnapshot" :disabled="armSnapshotLoading">
📸 获取图片
</button>
</div>
<div style="display:flex;justify-content:flex-end;margin-bottom:16px"> <div style="display:flex;justify-content:flex-end;margin-bottom:16px">
<input type="text" v-model="newQrName" placeholder="输入名称..." style="background:#0f1923;border:1px solid #2a3441;color:#fff;padding:8px 12px;border-radius:6px;margin-right:8px;width:180px"> <input type="text" v-model="newQrName" placeholder="输入名称..." style="background:#0f1923;border:1px solid #2a3441;color:#fff;padding:8px 12px;border-radius:6px;margin-right:8px;width:180px">
<button class="btn btn-primary" @click="addQrConfig()"> 添加</button> <button class="btn btn-primary" @click="addQrConfig()"> 添加</button>
@@ -476,6 +481,7 @@
<button class="btn btn-secondary btn-small" @click="readQrAngles(q.id)" :disabled="!armConnected" title="读取当前机械臂关节角度">📋 加载姿态</button> <button class="btn btn-secondary btn-small" @click="readQrAngles(q.id)" :disabled="!armConnected" title="读取当前机械臂关节角度">📋 加载姿态</button>
<button class="btn btn-primary btn-small" @click="applyQrAngles(q.id)" :disabled="!armConnected" style="margin-left:3px" title="将姿态应用到机械臂">🤖 应用姿态</button> <button class="btn btn-primary btn-small" @click="applyQrAngles(q.id)" :disabled="!armConnected" style="margin-left:3px" title="将姿态应用到机械臂">🤖 应用姿态</button>
<button class="btn btn-success btn-small" @click="scanQrEntry(q.id)" :disabled="qrScanningId === q.id" style="margin-left:3px" title="扫描二维码">📷</button> <button class="btn btn-success btn-small" @click="scanQrEntry(q.id)" :disabled="qrScanningId === q.id" style="margin-left:3px" title="扫描二维码">📷</button>
<button class="btn btn-secondary btn-small" @click="qrInputId = q.id; qrInputValue = q.qr_value || ''; showQrInputDialog = true" style="margin-left:3px" title="手动输入二维码值">✏️</button>
<button class="btn btn-danger btn-small" @click="deleteQrConfig(q.id)" style="margin-left:3px" title="删除">🗑️</button> <button class="btn btn-danger btn-small" @click="deleteQrConfig(q.id)" style="margin-left:3px" title="删除">🗑️</button>
</td> </td>
</tr> </tr>
@@ -582,9 +588,39 @@
</section> </section>
</div> </div>
</main> </main>
</div>
<!-- 手动输入二维码弹窗 -->
<div class="modal-overlay" v-if="showQrInputDialog">
<div class="modal" style="max-width:420px">
<h3>⌨️ 手动输入二维码</h3>
<p style="color:#9aa0a6;font-size:13px;margin:8px 0 16px">自动扫码未识别到二维码,请手动输入二维码内容:</p>
<input type="text" v-model="qrInputValue" placeholder="输入二维码内容..."
style="width:100%;background:#0f1923;border:1px solid #2a3441;color:#fff;padding:10px 12px;border-radius:6px;font-size:14px;box-sizing:border-box"
autofocus @keyup.enter="submitManualQr">
<div class="modal-actions" style="margin-top:16px">
<button class="btn btn-primary" @click="submitManualQr">确认</button>
<button class="btn" @click="showQrInputDialog = false; qrInputId = null; qrInputValue = ''">取消</button>
</div>
</div>
</div>
<!-- 机械臂截图弹窗 -->
<div class="modal-overlay" v-if="showArmSnapshot" @click.self="showArmSnapshot = false">
<div class="modal" style="max-width:800px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3>📸 机械臂摄像头截图</h3>
<button class="btn btn-small" @click="showArmSnapshot = false" style="font-size:18px;background:none;border:none;color:#9aa0a6;cursor:pointer"></button>
</div>
<div style="background:#000;border-radius:8px;overflow:hidden">
<img :src="armSnapshotUrl" style="width:100%;display:block">
</div>
<div style="margin-top:8px;text-align:center;color:#9aa0a6;font-size:12px">
点击弹窗外关闭
</div>
</div>
</div>
<script src="/static/js/vue3.global.prod.js?v=20260526a"></script> <script src="/static/js/vue3.global.prod.js?v=20260526a"></script>
<script src="/static/js/setting.js?v=20260529b"></script> <script src="/static/js/setting.js?v=20260605a"></script>
</body> </body>
</html> </html>