小白从零开始勇闯人工智能:计算机视觉初级篇(OpenCV综合实战(上))

引言

在人工智能潮中,计算机视觉让机器学会"看懂"世界。从人脸表情的微妙变化到疲劳状态的精准判断,再到年龄性别的智能预测,计算机视觉技术正悄然融入我们的生活。本篇文章将介绍OpenCV的案例实战,通过两个完整的代码案例,一步步实现表情识别、疲劳检测,为后续最终实现一个多功能的简单的综合检测系统打下基础。

一、表情识别:读懂你的微笑与大笑

1、实现原理

表情识别项目通过分析人脸关键点的几何关系,特别是嘴部区域的变化,来判断当前面部表情状态。基于dlib的68点人脸关键点检测模型,通过计算嘴部的宽高比和相对比例,将表情分为三类:"正常"、"微笑"和"大笑"。

2、核心技术解析

1、人脸关键点检测

首先定义表情识别项目的两个核心组件:detector 和 predictor。detector 使用 dlib 内置的基于 HOG(方向梯度直方图)特征和线性分类器的人脸检测算法,其功能是在图像中定位人脸的位置。predictor 则用于加载一个预训练的68点人脸关键点模型,该模型能够精准定位已检测出的人脸区域内的眉毛、眼睛、鼻子、嘴巴等部位的轮廓关键点。detector 首先找到人脸,随后 predictor 在每个人脸框内进行详细的关键点定位。

复制代码
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
2、关键计算函数
1、MAR函数(嘴部纵横比)

MAR 函数是表情识别项目中用于量化嘴部张合程度的核心算法。该函数基于 predictor 获取的68个人脸关键点坐标,通过计算嘴部纵横比来判断表情状态。其具体原理是:首先分别计算嘴唇上三个垂直方向的距离(点50到58、51到57、52到56),取其平均值以代表嘴部的平均垂直张开度,接着计算两个嘴角(点48到54)之间的水平宽度,最后将平均垂直张开度除以水平宽度,得到最终的嘴部纵横比值。这个比值能够有效表征嘴部的几何形状变化------比值越高,通常表明嘴部张得越开。

复制代码
def MAR(shape):
    A = euclidean_distances(shape[50].reshape(1, 2), shape[58].reshape(1, 2))
    B = euclidean_distances(shape[51].reshape(1, 2), shape[57].reshape(1, 2))
    C = euclidean_distances(shape[52].reshape(1, 2), shape[56].reshape(1, 2))
    D = euclidean_distances(shape[48].reshape(1, 2), shape[54].reshape(1, 2))
    return ((A + B + C) / 3) / D
2、MJR函数(嘴部与下颌宽度比)

MJR函数是该表情识别项目中的另一个重要特征提取方法,用于计算嘴部与下颌宽度比。其计算逻辑是:首先,通过 euclidean_distances 函数计算两个嘴角(关键点48和54)之间的距离,作为嘴部宽度(M),然后,计算下颌左右两个角点(关键点3和13)之间的距离,作为下颌宽度(J),最终返回二者的比值(M/J)。这个比值的核心作用在于量化嘴角向两侧拉伸的程度------当微笑或大笑时,嘴角会向两侧延展,导致嘴部宽度相对于固定的下颌宽度增加,因此 MJR 的比值会升高。

复制代码
def MJR(shape):
    M = euclidean_distances(shape[48].reshape(1, 2), shape[54].reshape(1, 2))
    J = euclidean_distances(shape[3].reshape(1, 2), shape[13].reshape(1, 2))
    return M / J
3、表情判断逻辑

整个表情判断逻辑基如下:通过分析嘴部张开幅度(mar)和嘴角拉伸程度(mjr)两个关键指标来识别表情状态。默认情况下,系统将表情判定为"正常",当嘴部张开幅度超过0.5的阈值时,由于嘴部明显张开,符合大笑特征,表情被判定为"大笑",若未达到大笑标准但嘴角拉伸程度超过0.45的阈值,则因嘴角明显拉伸符合微笑特征,表情被判定为"微笑"。其中,mar阈值设定为0.5主要针对嘴部大幅张开的典型大笑场景,而mjr阈值设定为0.45则用于捕捉嘴角拉伸形成的微笑状态。

复制代码
result = "正常"
if mar > 0.5:
    result = "大笑"      # 嘴部张开幅度大
elif mjr > 0.45:
    result = "微笑"      # 嘴角拉伸明显

3、实现流程

1、视频流获取

这里使用OpenCV的VideoCapture打开摄像头(0代表默认摄像头),循环读取每一帧图像,实现实时处理。

2、人脸检测与关键点提取

使用人脸检测器(detector)对输入图像帧(frame)进行人脸检测,返回人脸位置信息的列表。随后,对于检测到的每一个人脸区域,通过形状预测器(predictor)进一步定位该人脸上的68个关键点,并将这些关键点的坐标从原始数据结构转换为一个二维NumPy数组。

复制代码
faces = detector(frame, 0)  # 检测人脸
for face in faces:
    shape = predictor(frame, face)  # 获取68个关键点
    shape = np.array([[p.x, p.y] for p in shape.parts()])  # 转换为NumPy数组

3、可视化处理

这里使用凸包(convexHull)绘制嘴部轮廓,同时调用cv2AddChineseText函数在嘴部上方显示识别结果。

4、中文文本显示函数

这里使用代码实现显示中文文本

复制代码
def cv2AddChineseText(image, text, positton, textColor=(0, 255, 0), textSize=30):
    # 核心:使用PIL处理中文字体,解决OpenCV不支持中文的问题
    img_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img_pil)
    font = ImageFont.truetype("simsun.ttc", textSize, encoding="utf-8")
    draw.text(positton, text, font=font, fill=textColor)
    return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

5、完整代码以及结果展示

复制代码
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances
from PIL import Image, ImageDraw, ImageFont


def MAR(shape):
    A = euclidean_distances(shape[50].reshape(1, 2), shape[58].reshape(1, 2))
    B = euclidean_distances(shape[51].reshape(1, 2), shape[57].reshape(1, 2))
    C = euclidean_distances(shape[52].reshape(1, 2), shape[56].reshape(1, 2))
    D = euclidean_distances(shape[48].reshape(1, 2), shape[54].reshape(1, 2))

    return ((A + B + C) / 3) / D


def MJR(sahpe):
    M = euclidean_distances(sahpe[48].reshape(1, 2), sahpe[54].reshape(1, 2))
    J = euclidean_distances(sahpe[3].reshape(1, 2), sahpe[13].reshape(1, 2))
    return M / J


def cv2AddChineseText(image, text, positton, textColor=(0, 255, 0), textSize=30):
    # 将OpenCV图像转换为PIL图像
    img_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img_pil)

    # 使用默认字体,或指定中文字体路径
    try:
        font = ImageFont.truetype("simsun.ttc", textSize, encoding="utf-8")
    except:
        font = ImageFont.load_default()

    # 绘制文本
    draw.text(positton, text, font=font, fill=textColor)

    # 将PIL图像转换回OpenCV格式
    return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)


detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
cap = cv2.VideoCapture(0)
while True:
    ret, frame = cap.read()

    faces = detector(frame, 0)
    for face in faces:
        shape = predictor(frame, face)
        shape = np.array([[p.x, p.y] for p in shape.parts()])
        mar = MAR(shape)
        mjr = MJR(shape)
        result = "正常"
        print('mar', mar, "\tmjr", mjr)
        if mar > 0.5:
            result = "大笑"
        elif mjr > 0.45:
            result = "微笑"

        mouthHull = cv2.convexHull(shape[48:61])
        frame = cv2AddChineseText(frame, result, tuple(mouthHull[0, 0]))
        cv2.drawContours(frame, [mouthHull], -1, (0, 255, 0), 1)
    cv2.imshow('frame', frame)
    if cv2.waitKey(1) == 27:
        break
cap.release()
cv2.destroyAllWindows()

二、疲劳检测

1、实现原理

疲劳检测项目专注于监测人眼状态,通过计算眼睛的纵横比(Eye Aspect Ratio, EAR)来判断眼睛的闭合程度。当EAR值持续低于阈值时,系统判定为疲劳状态,发出警告。

2、核心技术解析

1、EAR计算函数详解

此函数通过分析眼部关键点的几何关系量化眼睛的睁开程度。函数接收一个包含单眼六个关键点坐标的数组,首先计算上眼睑与下眼睑之间的垂直距离:分别计算上眼睑左侧点(索引1)与下眼睑底部点(索引5)的欧氏距离(A),以及上眼睑顶部点(索引2)与下眼睑右侧点(索引4)的欧氏距离(B),以此表征眼睑的垂直分离程度,随后计算左右眼角(索引0与索引3)的水平距离(C),代表眼睛的基本宽度。最终,EAR值通过公式`(A + B) / (2.0 × C)`计算得出,即两个垂直方向距离的平均值与水平距离的比值。当眼睛完全睁开时,垂直距离较大,EAR值保持较高水平,当眼睛逐渐闭合时,垂直距离显著减小,导致EAR值迅速下降并趋近于零。

复制代码
def eye_aspect_ratio(eye):
    A = euclidean_distances(eye[1].reshape(1,2), eye[5].reshape(1,2))
    B = euclidean_distances(eye[2].reshape(1,2), eye[4].reshape(1,2))
    C = euclidean_distances(eye[0].reshape(1,2), eye[3].reshape(1,2))
    ear = ((A+B)/2.0)/C
    return ear
2、疲劳判断逻辑

检测逻辑:设定EAR阈值为0.3(可调整),当检测到EAR低于该阈值时,则判定眼睛处于闭合状态,并递增一个连续低EAR帧计数器。如果计数器累计达到或超过50帧(通常对应约1.5左右的持续时间),则判定为疲劳状态,并在视频帧的指定位置(250,250)叠加"!!!危险!!!"的中文警告文字。若中途EAR值恢复至阈值以上,计数器会立即重置为零。通过累加连续帧数触发警报的机制,有效过滤了正常眨眼等短暂动作,从而防止了误判,确保警报仅在持续、长时间的闭眼事件发生时被触发。

复制代码
COUNTER = 0  # 连续低EAR帧数计数器

if ear < 0.3:
    COUNTER += 1
    if COUNTER >= 50:
        frame = cv2AddChineseText(frame,"!!!危险!!!",(250,250))
else:
    COUNTER = 0

3、实现流程

1、双眼关键点提取

基于标准的面部标志点检测模型(68点模型),通过索引切片的方式分别获取右眼和左眼的特征点坐标:shape36:42 对应右眼的6个关键点(索引36至41),shape42:48 对应左眼的6个关键点(索引42至47)。

复制代码
rightEyePoints = shape[36:42]  # 右眼关键点(索引36-41)
leftEyePoints = shape[42:48]   # 左眼关键点(索引42-47)
2、EAR值计算与融合

计算并融合双眼的眼睛纵横比)值。首先,通过 eye_aspect_ratio 函数分别计算提取出的右眼和左眼关键点所对应的 right_ear 与 left_ear 值。随后,将左右眼的EAR值相加并除以2.0,得到二者的算术平均值 ear。

复制代码
right_ear = eye_aspect_ratio(rightEyePoints)
left_ear = eye_aspect_ratio(leftEyePoints)
ear = (left_ear + right_ear)/2.0  # 取双眼平均值
3、可视化增强

使用凸包填充眼部区域,增强视觉反馈,实时显示EAR数值,便于监控和调试。

复制代码
def drawEye(eye):
    eyeHull = cv2.convexHull(eye)
    cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), -1)  # 填充眼部区域
4、信息显示
复制代码
info = "EAR:{:.2f}".format(ear[0][0])  # 格式化显示EAR值,保留两位小数
frame = cv2AddChineseText(frame, info, (0,30))  # 在画面左上角显示

5、完整代码以及结果展示

复制代码
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances
from PIL import Image, ImageDraw, ImageFont

def eye_aspect_ratio(eye):
    A = euclidean_distances(eye[1].reshape(1,2), eye[5].reshape(1,2))
    B = euclidean_distances(eye[2].reshape(1,2), eye[4].reshape(1,2))
    C = euclidean_distances(eye[0].reshape(1,2), eye[3].reshape(1,2))
    ear = ((A+B)/2.0)/C
    return ear

def cv2AddChineseText(image, text, positton, textColor=(0, 255, 0), textSize=30):
    img_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img_pil)
    try:
        font = ImageFont.truetype("simsun.ttc", textSize, encoding="utf-8")
    except:
        font = ImageFont.load_default()
    draw.text(positton, text, font=font, fill=textColor)
    return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

def drawEye(eye):
    eyeHull = cv2.convexHull(eye)
    cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), -1)

COUNTER = 0
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
cap = cv2.VideoCapture(0)
while True:
    ret, frame = cap.read()
    faces = detector(frame, 0)
    for face in faces:
        shape = predictor(frame, face) 
        shape = np.array([[p.x,p.y]for p in shape.parts()])
        rightEyePoints = shape[36:42] 
        leftEyePoints = shape[42:48]   
        right_ear = eye_aspect_ratio(rightEyePoints)  
        left_ear = eye_aspect_ratio(leftEyePoints)    
        ear = (left_ear + right_ear)/2.0

        if ear < 0.3:
            COUNTER += 1
            if COUNTER >= 50:
                frame = cv2AddChineseText(frame,"!!!危险!!!",(250,250))
        else:
            COUNTER = 0
        drawEye(rightEyePoints)  
        drawEye(leftEyePoints)   
        info = "EAR:{:.2f}".format(ear[0][0])  # ear是2D数组
        frame = cv2AddChineseText(frame,info,(0,30))
    cv2.imshow('frame',frame)
    if cv2.waitKey(1)==27:
        break
cap.release()
cv2.destroyAllWindows()
相关推荐
江畔柳前堤4 小时前
roLabelImg 详细安装教程
开发语言·人工智能·后端·云原生
阿里云大数据AI技术4 小时前
分链路差异化设计的DSP准实时数仓|钛动科技基于阿里云实时计算 Flink 版 + DLF Paimon + EMR Serverless StarRocks 的实践
人工智能·flink
陕西企来客4 小时前
2026年7月AI智能搜索曝光趋势研判
大数据·人工智能·机器学习·ai智能搜索曝光
阿里云大数据AI技术5 小时前
从算力到智能体,面向 Agentic AI 的基础设施演进
人工智能·agent
hangyuekejiGEO5 小时前
GEO技术服务选型指南
大数据·人工智能·python
阿里云大数据AI技术6 小时前
EMR Serverless Spark AI Function 的双维降本实践
人工智能·sql·spark
维基框架6 小时前
GitHub源码处理提速 一趟扫描反而更慢
人工智能·github
冬奇Lab6 小时前
代码库知识库系列(05):向量检索 vs 知识图谱——加了调用图并没有变更好
人工智能
AKAMAI6 小时前
你的源服务器可能是你做出的最昂贵决定
运维·人工智能·云计算
冬奇Lab6 小时前
【无标题】
人工智能·开源