显示机械臂摄像头图片
This commit is contained in:
+45
-15
@@ -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 检测
|
|
||||||
|
result = None
|
||||||
|
|
||||||
|
# 方法1: pyzbar(识别率更高)
|
||||||
|
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()
|
detector = cv2.QRCodeDetector()
|
||||||
result, _, _ = detector.detectAndDecode(frame)
|
val, _, _ = detector.detectAndDecode(frame)
|
||||||
if result and len(result.strip()) > 0:
|
if val and len(val.strip()) > 0:
|
||||||
result = result.strip()
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 未连接')
|
||||||
|
|||||||
@@ -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 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>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user