工业视觉 OCR 与瓶盖缺陷检测结合 IoTDA 智慧物联应用实践
·
1 工效组合展示

2 具身智能工业视觉获取与分析应用
2.1 工具软件安装




2.2 工业视觉环境构建与应用

2.3 昇腾工业视觉终端应用展现



3 具身智能工业视觉识别快速编码实现
3.1 OCR识别

3.1.1 IMA-DS借力
给出python虚拟环境OCR文字识别的编码及其运行过程,包括汉字,windows环境,非COnDA,使用本机连接的网络摄像机,windows只能使用CPU,整个代码需要用一个文件表示,我的python版本是3.9.10,摄像头使用的是usb连接,我现在需要调用海康机器人的工业相机,请帮我在程序中再加上可以调用MVS数据库的程序,请你也适配一下设备

3.1.2 运行环境构造



3.1.3编码实现
# -*- coding: utf-8 -*-
"""
海康机器人工业相机(MVS SDK) 实时中文 OCR 文字识别 —— 单文件版
环境: Windows 10/11 + Python 3.9.10(64位) + venv(非conda) + CPU
相机: 海康机器人工业相机(GigE 网口 / USB3 均可)
按键: q=退出 s=截图 c=立即识别
"""
import os
# 必须在 import paddleocr 之前禁用 oneDNN,否则 paddlepaddle 3.3.x CPU 会报错
os.environ["PADDLE_PDX_ENABLE_MKLDNN_BYDEFAULT"] = "False"
import sys
import time
import argparse
import cv2
import numpy as np
from ctypes import *
from PIL import Image, ImageDraw, ImageFont
if sys.platform == "win32":
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
# ==================== 配置区 ====================
# 海康 MVS 的 Python SDK 目录(MvImport)。
# 推荐:把 MvImport 里的 .py 文件复制到本文件同目录,这里留空即可;
# 或填写完整路径,例如:
# MVS_IMPORT_PATH = r"C:\Program Files (x86)\MVS\Development\Samples\Python\MvImport"
MVS_IMPORT_PATH = ""
CAMERA_INDEX = 0 # 相机索引(0=第一台)
RESIZE_MAX_SIDE = 1280 # 图像最长边超过该值则等比缩小(提速;0=不缩放)
RECOGNIZE_INTERVAL = 1.0 # 识别间隔(秒)
CONF_THRESHOLD = 0.5 # 置信度阈值
SHOW_WINDOW = True
SAVE_LOG = True
LOG_FILE = "ocr_result.log"
FONT_CANDIDATES = [r"C:\Windows\Fonts\msyh.ttc",
r"C:\Windows\Fonts\simhei.ttf",
r"C:\Windows\Fonts\simsun.ttc"]
FONT_SIZE = 20
# ================================================
if MVS_IMPORT_PATH and os.path.isdir(MVS_IMPORT_PATH):
sys.path.insert(0, MVS_IMPORT_PATH)
try:
from MvCameraControl_class import *
except ImportError:
print("[错误] 找不到海康 MVS 的 Python SDK(MvCameraControl_class.py)")
print("请把 MVS 安装目录下 MvImport 文件夹里的 .py 文件复制到本程序同目录,")
print("或在本文件配置区 MVS_IMPORT_PATH 填写 MvImport 完整路径。")
sys.exit(1)
def log(msg):
line = "[{}] {}".format(time.strftime("%Y-%m-%d %H:%M:%S"), msg)
print(line, flush=True)
if SAVE_LOG:
try:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(line + "\n")
except OSError:
pass
# 像素格式编码(GigE Vision 国际标准,海康 MVS 遵循)
PF_MONO8 = 0x01080001 # 黑白
PF_RGB8 = 0x02180014 # 彩色 RGB
PF_BAYER_GR8 = 0x01080008 # 彩色 Bayer
PF_BAYER_RG8 = 0x01080009
PF_BAYER_GB8 = 0x0108000A
PF_BAYER_BG8 = 0x0108000B
class HKCamera(object):
"""海康机器人工业相机封装(MVS SDK)"""
def __init__(self, index=0):
self.index = index
self.cam = None
self.payload_size = 0
self.data_buf = None
self.frame_info = None
def connect(self):
self.release()
try:
device_list = MV_CC_DEVICE_INFO_LIST()
ret = MvCamera.MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, device_list)
if ret != 0:
log("枚举设备失败,错误码 0x{:X}".format(ret))
return False
if device_list.nDeviceNum == 0:
log("未找到海康工业相机:请检查连接/驱动,或 MVS 客户端是否占用")
return False
if self.index >= device_list.nDeviceNum:
log("相机索引 {} 超出范围(共 {} 台)".format(self.index, device_list.nDeviceNum))
return False
st_dev = cast(device_list.pDeviceInfo[self.index],
POINTER(MV_CC_DEVICE_INFO)).contents
self.cam = MvCamera()
ret = self.cam.MV_CC_CreateHandle(st_dev)
if ret != 0:
log("创建句柄失败,错误码 0x{:X}".format(ret))
self.release()
return False
ret = self.cam.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0)
if ret != 0:
log("打开设备失败,错误码 0x{:X}。请确认 MVS 客户端已关闭".format(ret))
self.release()
return False
# 关闭触发模式 -> 连续采集(关键,否则可能取不到流)
self.cam.MV_CC_SetEnumValue("TriggerMode", MV_TRIGGER_MODE_OFF)
st_param = MVCC_INTVALUE()
memset(byref(st_param), 0, sizeof(st_param))
ret = self.cam.MV_CC_GetIntValue("PayloadSize", st_param)
if ret != 0:
log("获取 PayloadSize 失败,错误码 0x{:X}".format(ret))
self.release()
return False
self.payload_size = st_param.nCurValue
ret = self.cam.MV_CC_StartGrabbing()
if ret != 0:
log("开始取流失败,错误码 0x{:X}".format(ret))
self.release()
return False
self.data_buf = (c_ubyte * self.payload_size)()
self.frame_info = MV_FRAME_OUT_INFO_EX()
log("海康相机打开成功:第 {} 台,PayloadSize={}".format(self.index, self.payload_size))
return True
except Exception as e:
log("连接相机异常: {}".format(e))
self.release()
return False
def read(self):
if self.cam is None:
return False, None
try:
memset(byref(self.frame_info), 0, sizeof(self.frame_info))
ret = self.cam.MV_CC_GetOneFrameTimeout(
byref(self.data_buf), self.payload_size, self.frame_info, 1000)
if ret != 0:
return False, None
frame = self._to_bgr()
return frame is not None, frame
except Exception:
return False, None
def _to_bgr(self):
w = self.frame_info.nWidth
h = self.frame_info.nHeight
if w <= 0 or h <= 0:
return None
pt = self.frame_info.enPixelType
n = w * h
try:
if pt == PF_MONO8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
elif pt == PF_RGB8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n * 3).reshape(h, w, 3)
return cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
elif pt == PF_BAYER_RG8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerRG2BGR)
elif pt == PF_BAYER_GB8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerGB2BGR)
elif pt == PF_BAYER_GR8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerGR2BGR)
elif pt == PF_BAYER_BG8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerBG2BGR)
else:
log("不支持的像素格式 0x{:X},请用 MVS 客户端把 PixelFormat 改为 Mono8 或 RGB8".format(pt))
return None
except Exception as e:
log("图像转换失败: {}".format(e))
return None
def release(self):
if self.cam is not None:
for fn in ("MV_CC_StopGrabbing", "MV_CC_CloseDevice", "MV_CC_DestroyHandle"):
try:
getattr(self.cam, fn)()
except Exception:
pass
self.cam = None
def resize_if_needed(frame):
if RESIZE_MAX_SIDE <= 0:
return frame
h, w = frame.shape[:2]
longest = max(h, w)
if longest > RESIZE_MAX_SIDE:
scale = RESIZE_MAX_SIDE / float(longest)
frame = cv2.resize(frame, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)
return frame
def init_ocr():
from paddleocr import PaddleOCR
log("正在初始化 PaddleOCR 中文模型(首次运行自动下载,请稍候)...")
ocr = PaddleOCR(
text_detection_model_name="PP-OCRv4_mobile_det",
text_recognition_model_name="PP-OCRv4_mobile_rec",
use_doc_orientation_classify=False,
use_doc_unwarping=False,
use_textline_orientation=True,
)
log("OCR 模型就绪")
return ocr
def parse_result(result):
items = []
if not result:
return items
res = result[0]
texts = res.get("rec_texts")
polys = res.get("rec_polys")
scores = res.get("rec_scores")
if texts is None:
return items
for i, txt in enumerate(texts):
score = float(scores[i]) if scores is not None and i < len(scores) else 1.0
poly = polys[i] if polys is not None and i < len(polys) else None
items.append((txt, poly, score))
return items
def draw_results(frame, items, font):
if not items:
return frame
pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_img)
for txt, poly, score in items:
color = (255, 0, 0)
label = "{} {:.2f}".format(txt, score)
if poly is not None and len(poly) >= 4:
pts = [(int(float(p[0])), int(float(p[1]))) for p in poly]
draw.polygon(pts, outline=color, width=2)
x0 = min(p[0] for p in pts)
y0 = min(p[1] for p in pts)
else:
x0, y0 = 10, 30
if font is not None:
draw.text((x0, max(0, y0 - FONT_SIZE - 4)), label, fill=color, font=font)
return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
def load_font():
try:
for p in FONT_CANDIDATES:
if os.path.exists(p):
return ImageFont.truetype(p, FONT_SIZE)
log("警告: 未找到中文字体,画面只画框")
return None
except Exception as e:
log("警告: 中文字体加载失败({})".format(e))
return None
def main():
p = argparse.ArgumentParser()
p.add_argument("--camera", type=int, default=CAMERA_INDEX, help="相机索引(0/1/2...)")
p.add_argument("--interval", type=float, default=RECOGNIZE_INTERVAL)
p.add_argument("--threshold", type=float, default=CONF_THRESHOLD)
p.add_argument("--no-window", action="store_true")
args = p.parse_args()
show_window = (not args.no_window) and SHOW_WINDOW
font = load_font()
ocr = init_ocr()
cam = HKCamera(args.camera)
if not cam.connect():
log("相机打开失败,程序退出。请关闭 MVS 客户端后重试,或换 --camera 1")
return
log("开始识别: 间隔{}s / 阈值{}".format(args.interval, args.threshold))
last_ocr_time = 0.0
last_texts = []
fps_start, fps_frames = time.time(), 0
while True:
ok, frame = cam.read()
if not ok:
log("相机取流中断,重连中...")
if not cam.connect():
time.sleep(3)
continue
continue
frame = resize_if_needed(frame)
if show_window:
key = cv2.waitKey(1) & 0xFF
else:
key = -1
time.sleep(0.01)
do_ocr = (time.time() - last_ocr_time >= args.interval) or key == ord("c")
if do_ocr:
last_ocr_time = time.time()
try:
items = parse_result(ocr.predict(frame))
items = [it for it in items if it[2] >= args.threshold]
if items:
last_texts = items
for txt, _, sc in items:
log("识别: {} (置信度 {:.3f})".format(txt, sc))
else:
last_texts = []
log("本帧未识别到文字")
except Exception as e:
log("OCR 出错: {}".format(e))
frame = draw_results(frame, last_texts, font)
fps_frames += 1
if time.time() - fps_start >= 1.0:
fps = fps_frames / (time.time() - fps_start)
cv2.putText(frame, "FPS:{:.1f}".format(fps), (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
fps_start, fps_frames = time.time(), 0
if show_window:
cv2.imshow("HIKROBOT OCR (q=退出 s=截图 c=识别)", frame)
if key == ord("q"):
break
elif key == ord("s"):
fn = "snapshot_{}.jpg".format(time.strftime("%Y%m%d_%H%M%S"))
cv2.imwrite(fn, frame)
log("已保存截图: {}".format(fn))
cam.release()
cv2.destroyAllWindows()
log("程序结束")
if __name__ == "__main__":
main()
3.1.4 运行测试

3.2 缺陷检查(瓶盖有无)
3.2.1 IMA-DS借力
现在我需要重新弄一份python虚拟环境可以检查矿泉水瓶盖有无缺陷检查的编码及其运行过程,Windows环境,只用CPU运行,非conda,使用本机USB连接的海康工业相机(MV系列),编码需要用一个完整文件,我的摄像头离瓶盖位置较近,是10cm左右,而且我所需要的瓶盖可能会有毛糙,还有一部分颜色检测范围会出现错误

3.2.2 运行环境构造

![]()
![]()

3.2.3编码实现
# -*- coding: utf-8 -*-
"""
海康工业相机(MV系列) 矿泉水瓶盖缺陷检测 —— 单文件版
环境: Windows 10/11 + Python 3.9.10(64位) + venv(非conda) + CPU
依赖: opencv-python / pillow / 海康 MVS SDK(无需 PaddlePaddle)
按键: q=退出 s=截图 d=显示/隐藏颜色分割图(调阈值用)
"""
import os
import sys
import time
import argparse
import cv2
import numpy as np
from ctypes import *
from PIL import Image, ImageDraw, ImageFont
if sys.platform == "win32":
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
# ==================== 相机配置区 ====================
MVS_IMPORT_PATH = "" # MvImport 复制到同目录则留空,否则填完整路径
CAMERA_INDEX = 0 # 相机索引(0=第一台)
RESIZE_MAX_SIDE = 1280 # 图像最长边超过该值则等比缩小(0=不缩放)
# ==================================================
# ==================== 检测配置区(按实际调整)====================
ROI = None # 瓶盖区域 (x,y,宽,高),None=整个画面
# 瓶盖颜色 HSV 范围(默认蓝色瓶盖,务必按实际颜色调整)
# 调法:运行后按 d 键看分割图,调到瓶盖白色、背景黑色为止
CAP_COLOR_LOWER = (100, 60, 60)
CAP_COLOR_UPPER = (130, 255, 255)
MIN_AREA = 5000 # 瓶盖最小面积(像素)
MAX_AREA = 200000 # 瓶盖最大面积(像素)
MIN_FILL_RATIO = 0.80 # 最小填充率=面积/外接圆面积,正常圆形≈0.9+
CENTER_OFFSET = 40 # 圆心允许偏移(像素)
# =============================================================
DETECT_INTERVAL = 0.2
SAVE_LOG = True
LOG_FILE = "cap_inspection.log"
FONT_CANDIDATES = [r"C:\Windows\Fonts\msyh.ttc",
r"C:\Windows\Fonts\simhei.ttf",
r"C:\Windows\Fonts\simsun.ttc"]
FONT_SIZE = 22
if MVS_IMPORT_PATH and os.path.isdir(MVS_IMPORT_PATH):
sys.path.insert(0, MVS_IMPORT_PATH)
try:
from MvCameraControl_class import *
except ImportError:
print("[错误] 找不到海康 MVS 的 Python SDK(MvCameraControl_class.py)")
print("请把 MVS 安装目录下 MvImport 文件夹里的 .py 文件复制到本程序同目录,")
print("或在本文件 MVS_IMPORT_PATH 填写 MvImport 完整路径。")
sys.exit(1)
def log(msg):
line = "[{}] {}".format(time.strftime("%Y-%m-%d %H:%M:%S"), msg)
print(line, flush=True)
if SAVE_LOG:
try:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(line + "\n")
except OSError:
pass
PF_MONO8 = 0x01080001
PF_RGB8 = 0x02180014
PF_BAYER_GR8 = 0x01080008
PF_BAYER_RG8 = 0x01080009
PF_BAYER_GB8 = 0x0108000A
PF_BAYER_BG8 = 0x0108000B
class HKCamera(object):
"""海康机器人工业相机封装(MVS SDK)"""
def __init__(self, index=0):
self.index = index
self.cam = None
self.payload_size = 0
self.data_buf = None
self.frame_info = None
def connect(self):
self.release()
try:
device_list = MV_CC_DEVICE_INFO_LIST()
ret = MvCamera.MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, device_list)
if ret != 0:
log("枚举设备失败,错误码 0x{:X}".format(ret))
return False
if device_list.nDeviceNum == 0:
log("未找到海康工业相机:请检查连接/驱动,或 MVS 客户端是否占用")
return False
if self.index >= device_list.nDeviceNum:
log("相机索引 {} 超出范围(共 {} 台)".format(self.index, device_list.nDeviceNum))
return False
st_dev = cast(device_list.pDeviceInfo[self.index],
POINTER(MV_CC_DEVICE_INFO)).contents
self.cam = MvCamera()
ret = self.cam.MV_CC_CreateHandle(st_dev)
if ret != 0:
log("创建句柄失败,错误码 0x{:X}".format(ret))
self.release()
return False
ret = self.cam.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0)
if ret != 0:
log("打开设备失败,错误码 0x{:X}。请确认 MVS 客户端已关闭".format(ret))
self.release()
return False
self.cam.MV_CC_SetEnumValue("TriggerMode", MV_TRIGGER_MODE_OFF)
st_param = MVCC_INTVALUE()
memset(byref(st_param), 0, sizeof(st_param))
ret = self.cam.MV_CC_GetIntValue("PayloadSize", st_param)
if ret != 0:
log("获取 PayloadSize 失败,错误码 0x{:X}".format(ret))
self.release()
return False
self.payload_size = st_param.nCurValue
ret = self.cam.MV_CC_StartGrabbing()
if ret != 0:
log("开始取流失败,错误码 0x{:X}".format(ret))
self.release()
return False
self.data_buf = (c_ubyte * self.payload_size)()
self.frame_info = MV_FRAME_OUT_INFO_EX()
log("海康相机打开成功:第 {} 台".format(self.index))
return True
except Exception as e:
log("连接相机异常: {}".format(e))
self.release()
return False
def read(self):
if self.cam is None:
return False, None
try:
memset(byref(self.frame_info), 0, sizeof(self.frame_info))
ret = self.cam.MV_CC_GetOneFrameTimeout(
byref(self.data_buf), self.payload_size, self.frame_info, 1000)
if ret != 0:
return False, None
frame = self._to_bgr()
return frame is not None, frame
except Exception:
return False, None
def _to_bgr(self):
w = self.frame_info.nWidth
h = self.frame_info.nHeight
if w <= 0 or h <= 0:
return None
pt = self.frame_info.enPixelType
n = w * h
try:
if pt == PF_MONO8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
elif pt == PF_RGB8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n * 3).reshape(h, w, 3)
return cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
elif pt == PF_BAYER_RG8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerRG2BGR)
elif pt == PF_BAYER_GB8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerGB2BGR)
elif pt == PF_BAYER_GR8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerGR2BGR)
elif pt == PF_BAYER_BG8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerBG2BGR)
else:
log("不支持的像素格式 0x{:X},请用 MVS 客户端改为 Mono8 或 RGB8".format(pt))
return None
except Exception as e:
log("图像转换失败: {}".format(e))
return None
def release(self):
if self.cam is not None:
for fn in ("MV_CC_StopGrabbing", "MV_CC_CloseDevice", "MV_CC_DestroyHandle"):
try:
getattr(self.cam, fn)()
except Exception:
pass
self.cam = None
def resize_if_needed(frame):
if RESIZE_MAX_SIDE <= 0:
return frame
h, w = frame.shape[:2]
longest = max(h, w)
if longest > RESIZE_MAX_SIDE:
scale = RESIZE_MAX_SIDE / float(longest)
frame = cv2.resize(frame, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)
return frame
def detect_cap(roi):
"""检测瓶盖,返回 (是否OK, 缺陷列表, 检测信息, 分割掩码图)。"""
h, w = roi.shape[:2]
info = None
# 1) 高斯模糊:平滑颜色噪声和表面毛糙,减少反光/纹理造成的分割碎块
blurred = cv2.GaussianBlur(roi, (7, 7), 0)
# 2) HSV 颜色分割
hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, CAP_COLOR_LOWER, CAP_COLOR_UPPER)
# 3) 形态学:较大结构元平滑毛糙边缘、填小孔、去小噪点
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=3)
# 4) 找轮廓
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return False, ["未检测到瓶盖(可能缺盖)"], info, mask
# 5) 取最大轮廓
largest = max(contours, key=cv2.contourArea)
area = cv2.contourArea(largest)
# 6) 填充率 = 面积 / 外接圆面积(对毛糙不敏感)
(cx, cy), radius = cv2.minEnclosingCircle(largest)
cx, cy, radius = int(cx), int(cy), int(radius)
enclosing_area = np.pi * radius * radius
fill_ratio = area / enclosing_area if enclosing_area > 0 else 0.0
info = {"cx": cx, "cy": cy, "radius": radius,
"area": int(area), "fill_ratio": round(fill_ratio, 3)}
# 7) 缺陷判定
defects = []
if area < MIN_AREA:
defects.append("面积过小(缺盖/残缺)")
elif area > MAX_AREA:
defects.append("面积过大(异常)")
if fill_ratio < MIN_FILL_RATIO:
defects.append("破损/缺口")
if ROI is not None:
offset = np.hypot(cx - w / 2.0, cy - h / 2.0)
info["offset"] = round(offset, 1)
if offset > CENTER_OFFSET:
defects.append("位置偏移/歪斜")
return len(defects) == 0, defects, info, mask
def draw_chinese(frame, text, pos, color, font):
pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
d = ImageDraw.Draw(pil)
if font is not None:
d.text(pos, text, fill=(color[2], color[1], color[0]), font=font)
return cv2.cvtColor(np.array(pil), cv2.COLOR_RGB2BGR)
def load_font():
try:
for p in FONT_CANDIDATES:
if os.path.exists(p):
return ImageFont.truetype(p, FONT_SIZE)
log("警告: 未找到中文字体,画面中文标注将不显示")
return None
except Exception as e:
log("警告: 中文字体加载失败({})".format(e))
return None
def main():
p = argparse.ArgumentParser()
p.add_argument("--camera", type=int, default=CAMERA_INDEX)
p.add_argument("--interval", type=float, default=DETECT_INTERVAL)
p.add_argument("--no-window", action="store_true")
args = p.parse_args()
show_window = not args.no_window
font = load_font()
cam = HKCamera(args.camera)
if not cam.connect():
log("相机打开失败,程序退出。请关闭 MVS 客户端后重试,或换 --camera 1")
return
log("开始瓶盖缺陷检测:间隔 {}s".format(args.interval))
last_detect = 0.0
last_state = None
last_defects = []
show_mask = False
fps_start, fps_frames = time.time(), 0
while True:
ok, frame = cam.read()
if not ok:
log("相机取流中断,重连中...")
if not cam.connect():
time.sleep(3)
continue
continue
frame = resize_if_needed(frame)
H, W = frame.shape[:2]
if ROI is not None:
x, y, rw, rh = ROI
x = max(0, min(x, W - 1)); y = max(0, min(y, H - 1))
rw = min(rw, W - x); rh = min(rh, H - y)
roi = frame[y:y + rh, x:x + rw]
else:
x, y, rw, rh = 0, 0, W, H
roi = frame
if show_window:
key = cv2.waitKey(1) & 0xFF
else:
key = -1
time.sleep(0.01)
if key == ord("d"):
show_mask = not show_mask
do_detect = (time.time() - last_detect >= args.interval)
if do_detect:
last_detect = time.time()
is_ok, defects, info, mask = detect_cap(roi)
last_defects = defects
state = "OK" if is_ok else "NG"
if state != last_state:
last_state = state
if is_ok:
log("结果: OK 面积={} 填充率={}".format(info["area"], info["fill_ratio"]) if info else "结果: OK")
else:
log("结果: NG 缺陷: {}".format("、".join(defects)))
else:
is_ok = (last_state == "OK")
info = None
mask = None
if show_mask and mask is not None:
display = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
else:
display = frame
if ROI is not None:
cv2.rectangle(display, (x, y), (x + rw, y + rh), (255, 200, 0), 2)
color = (0, 255, 0) if is_ok else (0, 0, 255)
status_text = "OK" if is_ok else "NG: " + "、".join(last_defects)
if font is not None:
display = draw_chinese(display, status_text, (10, 10), color, font)
else:
cv2.putText(display, status_text, (10, 35),
cv2.FONT_HERSHEY_SIMPLEX, 1.0, color, 2)
if not show_mask and info is not None:
cv2.circle(display, (x + info["cx"], y + info["cy"]), info["radius"], color, 3)
cv2.circle(display, (x + info["cx"], y + info["cy"]), 3, color, -1)
cv2.putText(display, "area:{} fill:{:.2f}".format(info["area"], info["fill_ratio"]),
(x + info["cx"] - info["radius"], max(10, y + info["cy"] - info["radius"] - 10)),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
fps_frames += 1
if time.time() - fps_start >= 1.0:
fps = fps_frames / (time.time() - fps_start)
cv2.putText(display, "FPS:{:.1f}".format(fps), (W - 140, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
fps_start, fps_frames = time.time(), 0
if show_window:
cv2.imshow("Cap Defect Inspection (q=退出 s=截图 d=分割图)", display)
if key == ord("q"):
break
elif key == ord("s"):
fn = "cap_{}.jpg".format(time.strftime("%Y%m%d_%H%M%S"))
cv2.imwrite(fn, display)
log("已保存截图: {}".format(fn))
cam.release()
cv2.destroyAllWindows()
log("程序结束")
if __name__ == "__main__":
main()
3.2.4 运行测试


3.2.5 运行中可能出现的问题及处理方法


在实验中可能会出现绿色识别范围不全,识别范围错误的问题其修改过后的代码为:
# -*- coding: utf-8 -*-
"""
海康工业相机(MV系列) 矿泉水瓶盖缺陷检测 —— 单文件版
========================================================
运行环境:Windows 10/11 + Python 3.9.10(64位) + venv(非conda) + CPU
相机:海康机器人工业相机(USB3 或 GigE,通过 MVS SDK 调用)
依赖:opencv-python / pillow / 海康 MVS SDK(无需 PaddlePaddle)
检测原理(传统图像处理,CPU 实时):
1. 默认用灰度 Otsu 自动阈值分割瓶盖(瓶盖亮、背景暗,红/绿/白盖通用)
2. 提取瓶盖轮廓,计算面积 / 填充率 / 圆心位置
3. 根据阈值判断缺陷:缺盖、破损/缺口、面积异常、位置偏移
4. 可选 HSV 颜色分割模式(DETECT_MODE="color")
【使用前必做】
1. 安装海康 MVS,把 MvImport 文件夹里的 .py 文件复制到本文件同目录
(或配置 MVS_IMPORT_PATH 为 MvImport 完整路径)
2. 运行本程序前关闭 MVS 客户端
按键:q=退出 s=截图 d=显示/隐藏分割图
"""
import os
import sys
import time
import argparse
import cv2
import numpy as np
from ctypes import *
from PIL import Image, ImageDraw, ImageFont
if sys.platform == "win32":
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
# ==================== 相机配置区 ====================
MVS_IMPORT_PATH = "" # MvImport 复制到同目录则留空,否则填完整路径
CAMERA_INDEX = 0 # 相机索引(0=第一台)
RESIZE_MAX_SIDE = 1280 # 图像最长边超过该值则等比缩小(0=不缩放)
# ==================================================
# ==================== 检测配置区(按实际调整)====================
# 检测模式:
# "gray" = 灰度Otsu自动阈值(推荐!瓶盖亮、背景暗即可,红/绿/白盖全部通用,免调颜色)
# "color" = HSV颜色分割(仅当灰度分割不适合时才用,需按颜色调范围)
DETECT_MODE = "gray"
ROI = None # 瓶盖区域 (x,y,宽,高),None=整个画面
# --- 灰度模式参数(DETECT_MODE="gray" 时生效)---
BRIGHT_CAP = True # True=瓶盖比背景亮(你的现场就是这种);False=瓶盖比背景暗
# --- 颜色分割模式参数(DETECT_MODE="color" 时生效)---
CAP_COLOR_LOWER = (100, 60, 60)
CAP_COLOR_UPPER = (130, 255, 255)
MIN_AREA = 5000 # 瓶盖最小面积(像素)
MAX_AREA = 200000 # 瓶盖最大面积(像素)
MIN_FILL_RATIO = 0.80 # 最小填充率=面积/外接圆面积,正常圆形≈0.9+
CENTER_OFFSET = 40 # 圆心允许偏移(像素)
# =============================================================
DETECT_INTERVAL = 0.2 # 检测间隔(秒)
SAVE_LOG = True
LOG_FILE = "cap_inspection.log"
FONT_CANDIDATES = [r"C:\Windows\Fonts\msyh.ttc",
r"C:\Windows\Fonts\simhei.ttf",
r"C:\Windows\Fonts\simsun.ttc"]
FONT_SIZE = 22
# ---------- 导入海康 MVS Python SDK ----------
if MVS_IMPORT_PATH and os.path.isdir(MVS_IMPORT_PATH):
sys.path.insert(0, MVS_IMPORT_PATH)
try:
from MvCameraControl_class import *
except ImportError:
print("[错误] 找不到海康 MVS 的 Python SDK(MvCameraControl_class.py)")
print("请把 MVS 安装目录下 MvImport 文件夹里的 .py 文件复制到本程序同目录,")
print("或在本文件 MVS_IMPORT_PATH 填写 MvImport 完整路径。")
sys.exit(1)
def log(msg):
line = "[{}] {}".format(time.strftime("%Y-%m-%d %H:%M:%S"), msg)
print(line, flush=True)
if SAVE_LOG:
try:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(line + "\n")
except OSError:
pass
# 像素格式编码(GigE Vision 标准)
PF_MONO8 = 0x01080001
PF_RGB8 = 0x02180014
PF_BAYER_GR8 = 0x01080008
PF_BAYER_RG8 = 0x01080009
PF_BAYER_GB8 = 0x0108000A
PF_BAYER_BG8 = 0x0108000B
class HKCamera(object):
"""海康机器人工业相机封装(MVS SDK)"""
def __init__(self, index=0):
self.index = index
self.cam = None
self.payload_size = 0
self.data_buf = None
self.frame_info = None
def connect(self):
self.release()
try:
device_list = MV_CC_DEVICE_INFO_LIST()
ret = MvCamera.MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, device_list)
if ret != 0:
log("枚举设备失败,错误码 0x{:X}".format(ret))
return False
if device_list.nDeviceNum == 0:
log("未找到海康工业相机:请检查连接/驱动,或 MVS 客户端是否占用")
return False
if self.index >= device_list.nDeviceNum:
log("相机索引 {} 超出范围(共 {} 台)".format(self.index, device_list.nDeviceNum))
return False
st_dev = cast(device_list.pDeviceInfo[self.index],
POINTER(MV_CC_DEVICE_INFO)).contents
self.cam = MvCamera()
ret = self.cam.MV_CC_CreateHandle(st_dev)
if ret != 0:
log("创建句柄失败,错误码 0x{:X}".format(ret))
self.release()
return False
ret = self.cam.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0)
if ret != 0:
log("打开设备失败,错误码 0x{:X}。请确认 MVS 客户端已关闭".format(ret))
self.release()
return False
self.cam.MV_CC_SetEnumValue("TriggerMode", MV_TRIGGER_MODE_OFF)
st_param = MVCC_INTVALUE()
memset(byref(st_param), 0, sizeof(st_param))
ret = self.cam.MV_CC_GetIntValue("PayloadSize", st_param)
if ret != 0:
log("获取 PayloadSize 失败,错误码 0x{:X}".format(ret))
self.release()
return False
self.payload_size = st_param.nCurValue
ret = self.cam.MV_CC_StartGrabbing()
if ret != 0:
log("开始取流失败,错误码 0x{:X}".format(ret))
self.release()
return False
self.data_buf = (c_ubyte * self.payload_size)()
self.frame_info = MV_FRAME_OUT_INFO_EX()
log("海康相机打开成功:第 {} 台".format(self.index))
return True
except Exception as e:
log("连接相机异常: {}".format(e))
self.release()
return False
def read(self):
if self.cam is None:
return False, None
try:
memset(byref(self.frame_info), 0, sizeof(self.frame_info))
ret = self.cam.MV_CC_GetOneFrameTimeout(
byref(self.data_buf), self.payload_size, self.frame_info, 1000)
if ret != 0:
return False, None
frame = self._to_bgr()
return frame is not None, frame
except Exception:
return False, None
def _to_bgr(self):
w = self.frame_info.nWidth
h = self.frame_info.nHeight
if w <= 0 or h <= 0:
return None
pt = self.frame_info.enPixelType
n = w * h
try:
if pt == PF_MONO8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
elif pt == PF_RGB8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n * 3).reshape(h, w, 3)
return cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
elif pt == PF_BAYER_RG8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerRG2BGR)
elif pt == PF_BAYER_GB8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerGB2BGR)
elif pt == PF_BAYER_GR8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerGR2BGR)
elif pt == PF_BAYER_BG8:
img = np.frombuffer(self.data_buf, dtype=np.uint8, count=n).reshape(h, w)
return cv2.cvtColor(img, cv2.COLOR_BayerBG2BGR)
else:
log("不支持的像素格式 0x{:X},请用 MVS 客户端改为 Mono8 或 RGB8".format(pt))
return None
except Exception as e:
log("图像转换失败: {}".format(e))
return None
def release(self):
if self.cam is not None:
for fn in ("MV_CC_StopGrabbing", "MV_CC_CloseDevice", "MV_CC_DestroyHandle"):
try:
getattr(self.cam, fn)()
except Exception:
pass
self.cam = None
def resize_if_needed(frame):
if RESIZE_MAX_SIDE <= 0:
return frame
h, w = frame.shape[:2]
longest = max(h, w)
if longest > RESIZE_MAX_SIDE:
scale = RESIZE_MAX_SIDE / float(longest)
frame = cv2.resize(frame, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)
return frame
def detect_cap(roi):
"""根据 DETECT_MODE 分发到对应检测方法。"""
if DETECT_MODE == "color":
return detect_by_color(roi)
return detect_by_gray(roi)
def detect_by_gray(roi):
"""灰度 Otsu 自动阈值检测:瓶盖亮、背景暗即可,红/绿/白盖通用,免调颜色。"""
h, w = roi.shape[:2]
info = None
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (9, 9), 0)
flag = cv2.THRESH_BINARY if BRIGHT_CAP else cv2.THRESH_BINARY_INV
_, binary = cv2.threshold(blurred, 0, 255, flag + cv2.THRESH_OTSU)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))
binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=1)
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=3)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return False, ["未检测到瓶盖(可能缺盖)"], info, binary
largest = max(contours, key=cv2.contourArea)
area = cv2.contourArea(largest)
(cx, cy), radius = cv2.minEnclosingCircle(largest)
cx, cy, radius = int(cx), int(cy), int(radius)
enclosing_area = np.pi * radius * radius
fill_ratio = area / enclosing_area if enclosing_area > 0 else 0.0
info = {"cx": cx, "cy": cy, "radius": radius,
"area": int(area), "fill_ratio": round(fill_ratio, 3)}
defects = []
if area < MIN_AREA:
defects.append("面积过小(缺盖/残缺)")
elif area > MAX_AREA:
defects.append("面积过大(异常)")
if fill_ratio < MIN_FILL_RATIO:
defects.append("破损/缺口")
if ROI is not None:
offset = np.hypot(cx - w / 2.0, cy - h / 2.0)
info["offset"] = round(offset, 1)
if offset > CENTER_OFFSET:
defects.append("位置偏移/歪斜")
return len(defects) == 0, defects, info, binary
def detect_by_color(roi):
"""HSV 颜色分割检测(备用模式,DETECT_MODE="color" 时使用)。"""
h, w = roi.shape[:2]
info = None
blurred = cv2.GaussianBlur(roi, (7, 7), 0)
hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, CAP_COLOR_LOWER, CAP_COLOR_UPPER)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=3)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return False, ["未检测到瓶盖(可能缺盖)"], info, mask
largest = max(contours, key=cv2.contourArea)
area = cv2.contourArea(largest)
(cx, cy), radius = cv2.minEnclosingCircle(largest)
cx, cy, radius = int(cx), int(cy), int(radius)
enclosing_area = np.pi * radius * radius
fill_ratio = area / enclosing_area if enclosing_area > 0 else 0.0
info = {"cx": cx, "cy": cy, "radius": radius,
"area": int(area), "fill_ratio": round(fill_ratio, 3)}
defects = []
if area < MIN_AREA:
defects.append("面积过小(缺盖/残缺)")
elif area > MAX_AREA:
defects.append("面积过大(异常)")
if fill_ratio < MIN_FILL_RATIO:
defects.append("破损/缺口")
if ROI is not None:
offset = np.hypot(cx - w / 2.0, cy - h / 2.0)
info["offset"] = round(offset, 1)
if offset > CENTER_OFFSET:
defects.append("位置偏移/歪斜")
return len(defects) == 0, defects, info, mask
def draw_chinese(frame, text, pos, color, font):
pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
d = ImageDraw.Draw(pil)
if font is not None:
d.text(pos, text, fill=(color[2], color[1], color[0]), font=font)
return cv2.cvtColor(np.array(pil), cv2.COLOR_RGB2BGR)
def load_font():
try:
for p in FONT_CANDIDATES:
if os.path.exists(p):
return ImageFont.truetype(p, FONT_SIZE)
log("警告: 未找到中文字体,画面中文标注将不显示")
return None
except Exception as e:
log("警告: 中文字体加载失败({})".format(e))
return None
def main():
p = argparse.ArgumentParser()
p.add_argument("--camera", type=int, default=CAMERA_INDEX)
p.add_argument("--interval", type=float, default=DETECT_INTERVAL)
p.add_argument("--no-window", action="store_true")
args = p.parse_args()
show_window = not args.no_window
font = load_font()
cam = HKCamera(args.camera)
if not cam.connect():
log("相机打开失败,程序退出。请关闭 MVS 客户端后重试,或换 --camera 1")
return
log("开始瓶盖缺陷检测:间隔 {}s,模式={}".format(args.interval, DETECT_MODE))
last_detect = 0.0
last_state = None
last_defects = []
show_mask = False
fps_start, fps_frames = time.time(), 0
while True:
ok, frame = cam.read()
if not ok:
log("相机取流中断,重连中...")
if not cam.connect():
time.sleep(3)
continue
continue
frame = resize_if_needed(frame)
H, W = frame.shape[:2]
if ROI is not None:
x, y, rw, rh = ROI
x = max(0, min(x, W - 1)); y = max(0, min(y, H - 1))
rw = min(rw, W - x); rh = min(rh, H - y)
roi = frame[y:y + rh, x:x + rw]
else:
x, y, rw, rh = 0, 0, W, H
roi = frame
if show_window:
key = cv2.waitKey(1) & 0xFF
else:
key = -1
time.sleep(0.01)
if key == ord("d"):
show_mask = not show_mask
do_detect = (time.time() - last_detect >= args.interval)
if do_detect:
last_detect = time.time()
is_ok, defects, info, mask = detect_cap(roi)
last_defects = defects
state = "OK" if is_ok else "NG"
if state != last_state:
last_state = state
if is_ok:
log("结果: OK 面积={} 填充率={}".format(info["area"], info["fill_ratio"]) if info else "结果: OK")
else:
log("结果: NG 缺陷: {}".format("、".join(defects)))
else:
is_ok = (last_state == "OK")
info = None
mask = None
if show_mask and mask is not None:
display = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
else:
display = frame
if ROI is not None:
cv2.rectangle(display, (x, y), (x + rw, y + rh), (255, 200, 0), 2)
color = (0, 255, 0) if is_ok else (0, 0, 255)
status_text = "OK" if is_ok else "NG: " + "、".join(last_defects)
if font is not None:
display = draw_chinese(display, status_text, (10, 10), color, font)
else:
cv2.putText(display, status_text, (10, 35),
cv2.FONT_HERSHEY_SIMPLEX, 1.0, color, 2)
if not show_mask and info is not None:
cv2.circle(display, (x + info["cx"], y + info["cy"]), info["radius"], color, 3)
cv2.circle(display, (x + info["cx"], y + info["cy"]), 3, color, -1)
cv2.putText(display, "area:{} fill:{:.2f}".format(info["area"], info["fill_ratio"]),
(x + info["cx"] - info["radius"], max(10, y + info["cy"] - info["radius"] - 10)),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
fps_frames += 1
if time.time() - fps_start >= 1.0:
fps = fps_frames / (time.time() - fps_start)
cv2.putText(display, "FPS:{:.1f}".format(fps), (W - 140, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
fps_start, fps_frames = time.time(), 0
if show_window:
cv2.imshow("Cap Defect Inspection (q=退出 s=截图 d=分割图)", display)
if key == ord("q"):
break
elif key == ord("s"):
fn = "cap_{}.jpg".format(time.strftime("%Y%m%d_%H%M%S"))
cv2.imwrite(fn, display)
log("已保存截图: {}".format(fn))
cam.release()
cv2.destroyAllWindows()
log("程序结束")
if __name__ == "__main__":
main()
修改代码后的运行测试图:

4 IoTDA中转空间构建
4.0 IoTDA构造



4.1 产品及其模型构建

4.1.1 温湿度计

4.1.2 吸顶灯

4.2 设备实例化应用

4.2.1 温湿度计设备注册及模拟测试





4.2.2 吸顶灯设备注册及模拟测试



5 CodeArts智慧路灯快速构建与部署
5.1 应用项目创建

5.2 代码托管操作


5.3 构建任务设立


5.4 项目构建实现


5.5 项目部署运行
1 工效组合展示


鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐
所有评论(0)