You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用OpenCV追踪RGB灯带单个LED的颜色变化?

问题描述

我正在开发一个基于OpenCV的程序,用于追踪RGB灯带中单个LED的颜色变化,核心需求是记录颜色变化的精确时间点。目前已完成单个LED的分割,但遇到两个瓶颈:

  • 从每个LED中心提取RGB值时,结果始终为白色(255,255,255)
  • 从bounding rectangle的角落提取时,像素值波动过大

相关素材说明:

  • 已完成掩码和分割后的灯带帧截图
  • 相机原始视频流2份
  • 各类待处理帧截图5份

参考源代码:

import cv2
import numpy as np
import csv
import datetime
import os
              
lower = np.array([0, 0, 230])
upper = np.array([179, 120, 255])
pixels = [[],[],[],[],[],[],[],[],[],[],[]]

def sliderCallback(x):
    global lower
    global upper

    lower[0] = cv2.getTrackbarPos('Hue Min', 'Trackbars')
    upper[0] = cv2.getTrackbarPos('Hue Max', 'Trackbars')
    lower[1] = cv2.getTrackbarPos('Saturation Min', 'Trackbars')
    upper[1] = cv2.getTrackbarPos('Saturation Max', 'Trackbars')
    lower[2] = cv2.getTrackbarPos('Value Min', 'Trackbars')
    upper[2] = cv2.getTrackbarPos('Value Max', 'Trackbars')

def getPos(event,x,y,flags,param):
    if(event == cv2.EVENT_LBUTTONDOWN):
        print(event, x, y)

def getPixelNumber(x):
    n = 1
    temp = 640 / 11
    while(1):
        if((temp * n) > x):
            return n;
        else:
            n += 1

def storeToCSV(pixel_list):
    try:
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        filename = f"pixels.csv"

        write_header = not os.path.exists(filename)

        with open(filename, mode='a', newline='') as file:
            writer = csv.writer(file)

            if write_header:
                header = ['Timestamp']
                for i, pixel in enumerate(pixel_list, 1):
                    header.extend([f'Pixel_{i}_B', f'Pixel_{i}_G', f'Pixel_{i}_R'])
                writer.writerow(header)

            row = [timestamp]
            for pixel in pixel_list:
                b, g, r = pixel
                row.extend([b, g, r])
            writer.writerow(row)
    except:
        pass



cv2.namedWindow('Trackbars')
cv2.createTrackbar('Hue Min', 'Trackbars', 0, 179, sliderCallback)
cv2.createTrackbar('Hue Max', 'Trackbars', 0, 179, sliderCallback)
cv2.createTrackbar('Saturation Min', 'Trackbars', 0, 255, sliderCallback)
cv2.createTrackbar('Saturation Max', 'Trackbars', 0, 255, sliderCallback)
cv2.createTrackbar('Value Min', 'Trackbars', 0, 255, sliderCallback)
cv2.createTrackbar('Value Max', 'Trackbars', 0, 255, sliderCallback)

cv2.namedWindow('image')
cv2.setMouseCallback('image',getPos)

cap = cv2.VideoCapture(0)


while True:
    ret, frame = cap.read()
    if ret:
        frame = frame[180:260, :]
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        mask = cv2.inRange(hsv, lower, upper)
        #mask = cv2.bitwise_not(mask)
        adjusted = cv2.bitwise_and(frame, frame, mask=mask);
        (h, s, v) = cv2.split(adjusted)
        contours, hierarchy = cv2.findContours(v, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

        contourCount = 0
        for c in contours:
            area = cv2.contourArea(c)
            if(area > 100):
                contourCount += 1
                x, y, h, w = cv2.boundingRect(c)
                pixel = frame[y - int(h / 10), x - int(w / 10)]
                pixels[getPixelNumber(x) - 1] = pixel
                cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
                cv2.drawContours(frame, c, contourIdx=-1, color=(255, 0, 0), thickness=3)
        cv2.imshow('image', frame)
        cv2.imshow('mask', mask)
        cv2.imshow('adjusted', adjusted)
        storeToCSV(pixels)
        
        

    key = cv2.waitKey(10) & 0xff
    
    if key == 27:  # ESC
        break
    
cv2.destroyAllWindows()
cap.release()
解决方案

一、异常原因分析

  1. 中心取色为白色:当前取色点计算错误,y - int(h / 10)会取到LED区域外的背景(灯带周围可能是高亮反光或白色背景),而非LED实际发光区域。
  2. 角落取色波动大:bounding rectangle角落属于LED边缘,颜色本身不均匀,且易混入背景噪声,导致数值不稳定。

二、可靠提取与变化判断方案

1. 优化LED颜色提取逻辑

  • 用区域均值替代单点取色:提取LED的bounding rectangle区域,计算该区域的BGR通道平均值,能有效过滤单点噪声,避免取到背景。
  • 质心取色备选:通过cv2.moments()计算轮廓质心,确保取色点落在LED发光区域内部,适合对精度要求高的场景。

2. 增强轮廓筛选

除了面积阈值,加入宽高比判断(LED通常为近似方形),过滤误检测的噪声轮廓:

aspect_ratio = w / float(h)
if 0.8 < aspect_ratio < 1.2:
    # 处理有效LED轮廓

3. 颜色变化判断策略

  • 颜色差值阈值:计算当前颜色与上一帧对应LED颜色的欧氏距离(BGR空间),超过设定阈值则判定为变化。
  • 稳定性校验:连续2-3帧检测到变化才记录时间点,避免单次噪声误判。

三、修改后的完整代码

import cv2
import numpy as np
import csv
import datetime
import os

# 初始化HSV阈值(可通过Trackbar调整)
lower = np.array([0, 0, 230])
upper = np.array([179, 120, 255])
# 存储每个LED的上一帧颜色,用于变化检测
prev_pixels = [[0,0,0] for _ in range(11)]
# 颜色变化阈值(可根据实际情况调整)
COLOR_CHANGE_THRESHOLD = 30

def sliderCallback(x):
    global lower, upper
    lower[0] = cv2.getTrackbarPos('Hue Min', 'Trackbars')
    upper[0] = cv2.getTrackbarPos('Hue Max', 'Trackbars')
    lower[1] = cv2.getTrackbarPos('Saturation Min', 'Trackbars')
    upper[1] = cv2.getTrackbarPos('Saturation Max', 'Trackbars')
    lower[2] = cv2.getTrackbarPos('Value Min', 'Trackbars')
    upper[2] = cv2.getTrackbarPos('Value Max', 'Trackbars')

def getPos(event,x,y,flags,param):
    if event == cv2.EVENT_LBUTTONDOWN:
        print(f"点击坐标:({x}, {y})")

def getPixelNumber(x):
    n = 1
    temp = 640 / 11
    while True:
        if temp * n > x:
            return n
        n += 1

def storeToCSV(timestamp, pixel_list, changed_indices):
    try:
        filename = "pixels.csv"
        write_header = not os.path.exists(filename)
        with open(filename, mode='a', newline='') as file:
            writer = csv.writer(file)
            if write_header:
                header = ['Timestamp']
                for i in range(1, 12):
                    header.extend([f'Pixel_{i}_B', f'Pixel_{i}_G', f'Pixel_{i}_R', f'Pixel_{i}_Changed'])
                writer.writerow(header)
            row = [timestamp]
            for idx, pixel in enumerate(pixel_list):
                b, g, r = pixel
                changed = 1 if (idx+1) in changed_indices else 0
                row.extend([b, g, r, changed])
            writer.writerow(row)
    except Exception as e:
        print(f"写入CSV失败:{e}")

# 创建Trackbar窗口
cv2.namedWindow('Trackbars')
cv2.createTrackbar('Hue Min', 'Trackbars', 0, 179, sliderCallback)
cv2.createTrackbar('Hue Max', 'Trackbars', 179, 179, sliderCallback)
cv2.createTrackbar('Saturation Min', 'Trackbars', 0, 255, sliderCallback)
cv2.createTrackbar('Saturation Max', 'Trackbars', 255, 255, sliderCallback)
cv2.createTrackbar('Value Min', 'Trackbars', 230, 255, sliderCallback)
cv2.createTrackbar('Value Max', 'Trackbars', 255, 255, sliderCallback)

cv2.namedWindow('image')
cv2.setMouseCallback('image', getPos)

cap = cv2.VideoCapture(0)

while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    # 裁剪ROI,只保留灯带区域
    frame_roi = frame[180:260, :]
    hsv = cv2.cvtColor(frame_roi, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, lower, upper)
    adjusted = cv2.bitwise_and(frame_roi, frame_roi, mask=mask)
    _, v_channel = cv2.split(cv2.cvtColor(adjusted, cv2.COLOR_BGR2HSV))
    
    # 查找轮廓
    contours, hierarchy = cv2.findContours(v_channel, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    current_pixels = [[0,0,0] for _ in range(11)]
    changed_leds = []
    
    for c in contours:
        area = cv2.contourArea(c)
        # 筛选有效LED轮廓:面积+宽高比
        if 100 < area < 1000:
            x, y, w, h = cv2.boundingRect(c)
            aspect_ratio = w / float(h)
            if 0.8 < aspect_ratio < 1.2:
                # 提取LED区域并计算颜色均值(抗噪性更强)
                led_roi = frame_roi[y:y+h, x:x+w]
                avg_color = np.mean(led_roi, axis=(0,1)).astype(int)
                b, g, r = avg_color
                
                # 对应LED编号
                led_idx = getPixelNumber(x) - 1
                current_pixels[led_idx] = [b, g, r]
                
                # 判断颜色是否变化
                prev_b, prev_g, prev_r = prev_pixels[led_idx]
                color_diff = np.sqrt((b-prev_b)**2 + (g-prev_g)**2 + (r-prev_r)**2)
                if color_diff > COLOR_CHANGE_THRESHOLD:
                    changed_leds.append(led_idx + 1)
                    prev_pixels[led_idx] = [b, g, r]
                
                # 绘制标记
                cv2.rectangle(frame_roi, (x, y), (x+w, y+h), (0,255,0), 2)
                # 绘制质心点
                M = cv2.moments(c)
                if M["m00"] != 0:
                    cX = int(M["m10"] / M["m00"])
                    cY = int(M["m01"] / M["m00"])
                    cv2.circle(frame_roi, (cX, cY), 2, (0,0,255), -1)
    
    # 获取当前时间戳
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    # 写入CSV
    storeToCSV(timestamp, current_pixels, changed_leds)
    
    # 显示窗口
    cv2.imshow('image', frame_roi)
    cv2.imshow('mask', mask)
    
    key = cv2.waitKey(10) & 0xff
    if key == 27:  # ESC退出
        break

cv2.destroyAllWindows()
cap.release()

四、关键改进点说明

  1. 取色方式优化:改用LED区域颜色平均值,彻底解决中心取色为白色、角落取色波动大的问题。
  2. 颜色变化检测:通过BGR空间欧氏距离判断变化,加入阈值过滤噪声,同时记录变化的LED编号。
  3. 轮廓筛选增强:增加宽高比判断,减少误检测的噪声轮廓。
  4. CSV记录优化:新增颜色变化标记列,方便后续分析。

内容的提问来源于stack exchange,提问作者404_Sleep_Not_Found

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 16:07:02