<think>我们有一个复杂的程序,它使用OpenMV相机进行多轮测量任务。程序有多个状态,每个状态执行不同的测量任务。我将逐步解释代码的各个部分。
### 1. 导入模块和全局变量设置
```python
import sensor
import time
import random
import math
# 全局状态变量
state = 0 # 0: 等待启动, 1: 第一轮测量, 2: 第二轮测量, 3: 编号输入测量, 4: 角度测量
remaining_targets = [1, 2, 3, 4] # 剩余目标物编号
current_target = None # 当前目标物
指定编号 = 0 # 用户输入的指定编号
```
- 导入必要的模块:sensor(摄像头控制)、time(时间)、random(随机数)、math(数学计算)。
- 定义全局状态变量:
- `state`:表示当前程序状态,0为初始等待状态,1和2为前两轮测量,3为等待用户输入编号,4为角度测量。
- `remaining_targets`:剩余待测量的目标物编号列表(1-4)。
- `current_target`:当前选中的目标物编号。
- `指定编号`:用户输入的指定目标物编号(用于第三轮测量)。
### 2. 摄像头初始化
```python
# 设置VGA画面,并裁剪中间的画面
sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE)
sensor.set_framesize(sensor.VGA)
sensor.set_windowing((200,240))
sensor.skip_frames(time=2000)
clock = time.clock() # Tracks FPS.
CENTER_X = 200//2
CENTER_Y = 240//2
```
- 初始化摄像头:设置为灰度图像,VGA分辨率(640x480),然后裁剪中间200x240的区域。
- `sensor.skip_frames`:等待摄像头稳定。
- `clock`:用于计算帧率。
- `CENTER_X`和`CENTER_Y`:定义画面中心坐标。
### 3. 测量参数设置
```python
# 边框的真实长度,单位mm
FRAME_WIDTH_MM = 176
# 校准的数据
# 例如: 当距离是1400mm时,边框像素宽度为82
DISTANCE_MM_2 = 1400
FRAME_WIDTH_PIXEL_2 = 82
# 目标物数据(假设)
目标物数据 = {
1: {"name": "目标物1", "color": (0, 150)},
2: {"name": "目标物2", "color": (0, 150)},
3: {"name": "目标物3", "color": (0, 150)},
4: {"name": "目标物4", "color": (0, 150)}
}
# 数字模板(简化版,实际应用中可能需要更复杂的模板匹配)
数字模板 = {
1: "1",
2: "2",
3: "3",
4: "4"
}
```
- `FRAME_WIDTH_MM`:A4纸边框的实际宽度(单位毫米)。
- 校准数据:在已知距离(1400mm)下,边框在图像中的像素宽度(82像素)。
- `目标物数据`:目标物编号对应的名称和颜色范围(这里都是黑色,灰度值0-150)。
- `数字模板`:用于识别正方形上数字的模板(这里只是示例,实际未使用)。
### 4. 辅助函数
#### 4.1 寻找中心区域的最小色块
```python
def find_center_min_blob(blobs):
# 找中间最小的色块
blob = None
min_area = 100000
for b in blobs:
# 排除距离中心太远的色块(曼哈顿距离>50)
if abs(b.cx()-CENTER_X)+ abs(b.cy()-CENTER_Y) > 50:
continue
if b.area() > min_area:
continue
blob = b
min_area = b.area()
return blob
```
- 在给定的色块列表中,寻找距离画面中心最近且面积最小的色块(用于边框检测)。
#### 4.2 寻找中心区域的最大色块
```python
def find_center_max_blob(blobs):
# 找中间最大的色块
blob = None
max_area = 0
for b in blobs:
# 排除距离中心太远的色块
if abs(b.cx()-CENTER_X)+ abs(b.cy()-CENTER_Y) > 50:
continue
if b.area() < max_area:
continue
blob = b
max_area = b.area()
return blob
```
- 在给定的色块列表中,寻找距离画面中心最近且面积最大的色块(用于目标物体检测)。
#### 4.3 寻找最小正方形色块
```python
def find_min_square(blobs):
# 找最小面积的正方形
min_square = None
min_area = float('inf') # 初始化为无穷大
# 调试信息:显示找到的色块数量
print(f"找到的色块数量: {len(blobs)}")
for b in blobs:
# 对于黑色正方形,降低面积过滤阈值
if b.area() < 5: # 进一步降低最小面积阈值
continue
# 假设正方形的宽高比接近1
aspect_ratio = max(b.w(), b.h()) / min(b.w(), b.h()) if min(b.w(), b.h()) > 0 else 0
# 对于小正方形,宽高比判断可以更宽松
if aspect_ratio > 1.3: # 稍微放宽宽高比限制
continue
if b.area() < min_area:
min_area = b.area()
min_square = b
print(f"临时最小正方形: 面积={b.area()}, 宽={b.w()}, 高={b.h()}, 宽高比={aspect_ratio:.2f}")
# 调试信息
if min_square:
print(f"最终最小正方形: 面积={min_square.area()}, 宽={min_square.w()}, 高={min_square.h()}")
else:
print("未找到符合条件的正方形")
return min_square
```
- 在色块列表中寻找最小面积的正方形。条件:
- 面积大于5(过滤噪点)。
- 宽高比小于1.3(允许一定变形)。
- 面积最小。
#### 4.4 识别数字(简化版)
```python
def recognize_number(img, roi):
# 识别数字(简化版)
# 实际应用中可能需要OCR或模板匹配
# 这里返回一个随机数作为示例
return random.randint(1, 4)
```
- 实际应用中应使用OCR或模板匹配,这里返回1-4的随机数。
#### 4.5 计算角度
```python
def calculate_angle(blob):
# 计算物体与轴线的夹角
# 假设通过宽高比来估算角度
aspect_ratio = blob.w() / blob.h() if blob.h() > 0 else 1
angle = math.atan(aspect_ratio) * 180 / math.pi
return min(max(angle, 30), 60) # 限制在30-60度之间
```
- 通过色块的宽高比估算角度(这里做了简化,实际应更精确)。
#### 4.6 启动测量
```python
def start_measurement():
global state, current_target, remaining_targets
if state == 0:
# 第一轮测量:随机取一个目标物
if remaining_targets:
current_target = random.choice(remaining_targets)
remaining_targets.remove(current_target)
state = 1
print(f"第一轮测量:已选择目标物 {current_target}")
else:
print("没有可用目标物")
elif state == 1:
# 第二轮测量:从剩余目标物中再随机取一个
if remaining_targets:
current_target = random.choice(remaining_targets)
remaining_targets.remove(current_target)
state = 2
print(f"第二轮测量:已选择目标物 {current_target}")
else:
print("没有可用目标物")
elif state == 2:
# 第三轮测量:等待输入编号
state = 3
print("请输入要测量的正方形编号(1-4):")
elif state == 3:
# 第三轮测量:等待输入编号后的执行
if 指定编号 in 目标物数据:
current_target = 指定编号
state = 4
print(f"第三轮测量:已选择目标物 {current_target}")
else:
print("无效的编号,请重新输入")
elif state == 4:
# 第四轮测量:角度测量准备
state = 5
print("第四轮测量:准备角度测量")
elif state == 5:
# 第四轮测量:角度测量
state = 0
print("第四轮测量:角度测量完成")
```
- 根据当前状态启动下一轮测量:
- 状态0:随机选择一个目标物进入状态1。
- 状态1:从剩余目标物中随机选一个进入状态2。
- 状态2:进入状态3(等待用户输入)。
- 状态3:检查输入编号是否有效,有效则进入状态4。
- 状态4:进入状态5(角度测量准备)。
- 状态5:完成角度测量,回到状态0。
### 5. 主循环
#### 5.1 状态显示和按键检测
```python
while True:
clock.tick()
img = sensor.snapshot()
# 显示状态信息
img.draw_string(10, 10, f"状态: {state}", color=(255, 0, 0))
if current_target:
img.draw_string(10, 25, f"当前目标: {current_target}", color=(255, 0, 0))
img.draw_string(10, 40, "按任意键启动测量", color=(255, 0, 0))
# 模拟按键输入(用于测试)
if time.time() % 5 < 0.1 and state == 0:
start_measurement()
```
- 显示当前状态、当前目标物和提示信息。
- 每5秒自动触发一次测量(模拟按键)。
#### 5.2 边框检测
```python
# 找白色色块,黑色边框内部
frames = img.find_blobs([(150,256)])
frame_blob = find_center_min_blob(frames)
if not frame_blob:
print("NO FRAME")
continue
# 计算距离
distance = DISTANCE_MM_2*FRAME_WIDTH_PIXEL_2/frame_blob.w()
```
- 检测白色边框(灰度值150-255),并找到中心区域的最小色块(即边框)。
- 利用校准数据计算摄像头到边框的距离(通过边框在图像中的宽度)。
#### 5.3 目标物体检测
```python
# 缩小roi,避免黑框的黑边
frame_roi = (frame_blob.x()+5, frame_blob.y()+5, frame_blob.w()-10, frame_blob.h()-10)
if frame_roi[2] <= 0 or frame_roi[3] <= 0:
print("ROI ERROR")
continue
# 找黑色色块,目标物体
objs = img.find_blobs([(0,150)], roi=frame_roi)
obj_blob = find_center_max_blob(objs)
if not obj_blob:
print("NO OBJS")
continue
```
- 在边框内部定义ROI(避免边框边缘干扰)。
- 在ROI内检测黑色目标物体(灰度值0-150),并找到中心区域的最大色块。
#### 5.4 根据状态执行测量任务
- **状态1和2(第一、二轮测量)**:寻找最小正方形并计算其宽度(毫米)。
```python
if state == 1 or state == 2:
squares = img.find_blobs([(0, 100)], roi=frame_roi, pixels_threshold=2, area_threshold=2)
min_square = find_min_square(squares)
if min_square:
square_x_mm = min_square.w()/frame_blob.w()*FRAME_WIDTH_MM
# 显示结果
else:
# 尝试更低阈值
...
```
- **状态4(第三轮测量)**:寻找指定编号的正方形。
```python
elif state == 4:
squares = img.find_blobs([(0, 100)], roi=frame_roi, pixels_threshold=2, area_threshold=2)
min_square = find_min_square(squares)
if min_square:
number = recognize_number(img, min_square.rect())
if number == 指定编号:
square_x_mm = min_square.w()/frame_blob.w()*FRAME_WIDTH_MM
# 显示结果
else:
...
```
- **状态5(第四轮测量)**:角度测量。
```python
elif state == 5:
squares = img.find_blobs([(0, 100)], roi=frame_roi, pixels_threshold=2, area_threshold=2)
min_square = find_min_square(squares)
if min_square:
angle = calculate_angle(min_square)
# 考虑倾斜,计算实际宽度
square_x_mm = min_square.w()/frame_blob.w()*FRAME_WIDTH_MM / math.cos(math.radians(angle))
# 显示结果
```
#### 5.5 绘制结果和模拟用户输入
```python
# 计算物体边长(原有功能)
obj_w_mm = obj_blob.w()/frame_blob.w()*FRAME_WIDTH_MM
# 绘制边框和目标
img.draw_rectangle(frame_blob.rect())
img.draw_rectangle(obj_blob.rect())
# 模拟用户输入(状态3时,每2秒自动输入一个随机编号)
if state == 3:
if time.time() % 2 < 0.1:
指定编号 = random.randint(1, 4)
print(f"用户输入编号: {指定编号}")
start_measurement()
```
### 总结
该程序通过状态机控制测量流程:
1. **状态0**:等待开始。
2. **状态1和2**:自动选择目标物,测量最小正方形的宽度。
3. **状态3**:等待用户输入目标编号。
4. **状态4**:测量指定编号的正方形宽度。
5. **状态5**:测量正方形的倾斜角度和实际宽度。
程序利用OpenMV的色块检测功能,结合简单的几何关系进行测量。实际应用中需注意:
- 数字识别需要更精确的方法(如模板匹配或OCR)。
- 角度测量方法需改进(如使用最小外接矩形)。
- 用户输入需通过实际硬件(如串口)实现。
§§相关问题§§:
1. 如何改进数字识别功能以提高准确性?
2. 在状态3中,如何通过串口接收用户输入?
3. 如何优化角度测量算法?
4. 程序在多目标物同时出现时如何确保正确识别?</think>以下是针对您提供的 OpenMV 代码的详细解释。这段代码实现了一个多阶段测量系统,通过状态机控制不同的测量任务,包括目标物选择、距离测量、最小正方形检测和角度测量。
### 1. 全局变量和初始化
```python
# 全局状态变量
state = 0 # 0: 等待启动, 1: 第一轮测量, 2: 第二轮测量, 3: 编号输入测量, 4: 角度测量
remaining_targets = [1, 2, 3, 4] # 剩余目标物编号
current_target = None # 当前目标物
指定编号 = 0 # 用户输入的指定编号
# 设置VGA画面,并裁剪中间的画面
sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE)
sensor.set_framesize(sensor.VGA)
sensor.set_windowing((200,240))
sensor.skip_frames(time=2000)
clock = time.clock() # Tracks FPS.
CENTER_X = 200//2
CENTER_Y = 240//2
# 边框的真实长度,单位mm
FRAME_WIDTH_MM = 176
# 校准的数据
DISTANCE_MM_2 = 1400
FRAME_WIDTH_PIXEL_2 = 82
# 目标物数据(假设)
目标物数据 = {
1: {"name": "目标物1", "color": (0, 150)},
2: {"name": "目标物2", "color": (0, 150)},
3: {"name": "目标物3", "color": (0, 150)},
4: {"name": "目标物4", "color": (0, 150)}
}
# 数字模板(简化版)
数字模板 = {
1: "1",
2: "2",
3: "3",
4: "4"
}
```
**解释**:
- `state`:控制程序状态的状态机变量
- `remaining_targets`:存储待测量的目标物编号
- `current_target`:当前选中的目标物
- `指定编号`:用户输入的特定目标编号
- 摄像头初始化为灰度模式,VGA分辨率,裁剪中间200x240区域
- 校准数据:已知距离(1400mm)对应的边框像素宽度(82像素)
- 目标物数据:存储4个目标物的名称和颜色范围
- 数字模板:用于识别目标物上的数字(简化版)
### 2. 辅助函数
#### 2.1 目标物定位函数
```python
def find_center_min_blob(blobs):
# 找中间最小的色块
blob = None
min_area = 100000
for b in blobs:
if abs(b.cx()-CENTER_X)+ abs(b.cy()-CENTER_Y) > 50:
continue
if b.area() > min_area:
continue
blob = b
min_area = b.area()
return blob
def find_center_max_blob(blobs):
# 找中间最大的色块
blob = None
max_area = 0
for b in blobs:
if abs(b.cx()-CENTER_X)+ abs(b.cy()-CENTER_Y) > 50:
continue
if b.area() < max_area:
continue
blob = b
max_area = b.area()
return blob
```
**解释**:
- `find_center_min_blob`:在画面中心区域(曼哈顿距离<50)寻找面积最小的色块(用于边框检测)
- `find_center_max_blob`:在画面中心区域寻找面积最大的色块(用于目标物检测)
#### 2.2 最小正方形检测
```python
def find_min_square(blobs):
# 找最小面积的正方形
min_square = None
min_area = float('inf') # 初始化为无穷大
print(f"找到的色块数量: {len(blobs)}")
for b in blobs:
# 对于黑色正方形,降低面积过滤阈值
if b.area() < 5: # 进一步降低最小面积阈值
continue
# 假设正方形的宽高比接近1
aspect_ratio = max(b.w(), b.h()) / min(b.w(), b.h()) if min(b.w(), b.h()) > 0 else 0
# 对于小正方形,宽高比判断可以更宽松
if aspect_ratio > 1.3: # 稍微放宽宽高比限制
continue
if b.area() < min_area:
min_area = b.area()
min_square = b
print(f"临时最小正方形: 面积={b.area()}, 宽={b.w()}, 高={b.h()}, 宽高比={aspect_ratio:.2f}")
if min_square:
print(f"最终最小正方形: 面积={min_square.area()}, 宽={min_square.w()}, 高={min_square.h()}")
else:
print("未找到符合条件的正方形")
return min_square
```
**解释**:
- 在检测到的色块中寻找最小面积的正方形
- 过滤条件:
1. 面积大于5(避免噪点)
2. 宽高比小于1.3(允许一定变形)
- 输出调试信息帮助理解检测过程
#### 2.3 数字识别和角度计算
```python
def recognize_number(img, roi):
# 识别数字(简化版)
# 实际应用中可能需要OCR或模板匹配
# 这里返回一个随机数作为示例
return random.randint(1, 4)
def calculate_angle(blob):
# 计算物体与轴线的夹角
# 假设通过宽高比来估算角度
aspect_ratio = blob.w() / blob.h() if blob.h() > 0 else 1
angle = math.atan(aspect_ratio) * 180 / math.pi
return min(max(angle, 30), 60) # 限制在30-60度之间
```
**解释**:
- `recognize_number`:简化的数字识别函数(实际应用中应替换为OCR或模板匹配)
- `calculate_angle`:通过宽高比估算物体倾斜角度(限制在30-60度之间)
#### 2.4 状态机控制
```python
def start_measurement():
global state, current_target, remaining_targets
if state == 0:
# 第一轮测量:随机取一个目标物
if remaining_targets:
current_target = random.choice(remaining_targets)
remaining_targets.remove(current_target)
state = 1
print(f"第一轮测量:已选择目标物 {current_target}")
else:
print("没有可用目标物")
elif state == 1:
# 第二轮测量:从剩余目标物中再随机取一个
if remaining_targets:
current_target = random.choice(remaining_targets)
remaining_targets.remove(current_target)
state = 2
print(f"第二轮测量:已选择目标物 {current_target}")
else:
print("没有可用目标物")
elif state == 2:
# 第三轮测量:等待输入编号
state = 3
print("请输入要测量的正方形编号(1-4):")
elif state == 3:
# 第三轮测量:等待输入编号后的执行
if 指定编号 in 目标物数据:
current_target = 指定编号
state = 4
print(f"第三轮测量:已选择目标物 {current_target}")
else:
print("无效的编号,请重新输入")
elif state == 4:
# 第四轮测量:角度测量准备
state = 5
print("第四轮测量:准备角度测量")
elif state == 5:
# 第四轮测量:角度测量
state = 0
print("第四轮测量:角度测量完成")
```
**解释**:
- 状态机控制测量流程:
1. **状态0→1**:随机选择第一个目标物
2. **状态1→2**:随机选择第二个目标物
3. **状态2→3**:等待用户输入目标编号
4. **状态3→4**:处理用户输入的编号
5. **状态4→5**:准备角度测量
6. **状态5→0**:完成角度测量,重置状态
### 3. 主循环处理
```python
while True:
clock.tick()
img = sensor.snapshot()
# 显示状态信息
img.draw_string(10, 10, f"状态: {state}", color=(255, 0, 0))
if current_target:
img.draw_string(10, 25, f"当前目标: {current_target}", color=(255, 0, 0))
img.draw_string(10, 40, "按任意键启动测量", color=(255, 0, 0))
# 模拟按键输入
if time.time() % 5 < 0.1 and state == 0:
start_measurement()
# 边框检测
frames = img.find_blobs([(150,256)])
frame_blob = find_center_min_blob(frames)
if not frame_blob:
print("NO FRAME")
continue
# 计算距离
distance = DISTANCE_MM_2*FRAME_WIDTH_PIXEL_2/frame_blob.w()
# 设置目标检测区域
frame_roi = (frame_blob.x()+5, frame_blob.y()+5, frame_blob.w()-10, frame_blob.h()-10)
if frame_roi[2] <= 0 or frame_roi[3] <= 0:
print("ROI ERROR")
continue
# 目标物检测
objs = img.find_blobs([(0,150)], roi=frame_roi)
obj_blob = find_center_max_blob(objs)
if not obj_blob:
print("NO OBJS")
continue
# 根据状态执行不同测量任务
if state == 1 or state == 2:
# 任务1和2:找最小面积正方形
squares = img.find_blobs([(0, 100)], roi=frame_roi, pixels_threshold=2, area_threshold=2)
min_square = find_min_square(squares)
if min_square:
square_x_mm = min_square.w()/frame_blob.w()*FRAME_WIDTH_MM
print(f"距离D: {distance:.2f}mm, 最小正方形x: {square_x_mm:.2f}mm")
img.draw_string(10, 60, f"D: {distance:.2f}mm", color=(255, 0, 0))
img.draw_string(10, 75, f"最小正方形x: {square_x_mm:.2f}mm", color=(255, 0, 0))
img.draw_rectangle(min_square.rect(), color=(0, 255, 0))
else:
# 尝试更低阈值
...
elif state == 4:
# 任务3:指定编号正方形
squares = img.find_blobs([(0, 100)], roi=frame_roi, pixels_threshold=2, area_threshold=2)
min_square = find_min_square(squares)
if min_square:
number = recognize_number(img, min_square.rect())
print(f"识别到的编号: {number}")
if number == 指定编号:
square_x_mm = min_square.w()/frame_blob.w()*FRAME_WIDTH_MM
print(f"距离D: {distance:.2f}mm, 编号{指定编号}正方形x: {square_x_mm:.2f}mm")
img.draw_string(10, 60, f"D: {distance:.2f}mm", color=(255, 0, 0))
img.draw_string(10, 75, f"编号{指定编号}正方形x: {square_x_mm:.2f}mm", color=(255, 0, 0))
else:
print(f"识别到的编号{number}与指定编号{指定编号}不匹配")
elif state == 5:
# 任务4:角度测量
squares = img.find_blobs([(0, 100)], roi=frame_roi, pixels_threshold=2, area_threshold=2)
min_square = find_min_square(squares)
if min_square:
angle = calculate_angle(min_square)
square_x_mm = min_square.w()/frame_blob.w()*FRAME_WIDTH_MM / math.cos(math.radians(angle))
print(f"角度θ: {angle:.2f}度, 正方形x: {square_x_mm:.2f}mm")
img.draw_string(10, 60, f"角度θ: {angle:.2f}度", color=(255, 0, 0))
img.draw_string(10, 75, f"正方形x: {square_x_mm:.2f}mm", color=(255, 0, 0))
# 绘制边框和目标
img.draw_rectangle(frame_blob.rect())
img.draw_rectangle(obj_blob.rect())
# 模拟用户输入(状态3)
if state == 3:
if time.time() % 2 < 0.1:
指定编号 = random.randint(1, 4)
print(f"用户输入编号: {指定编号}")
start_measurement()
```
**解释**:
1. **状态显示**:在画面顶部显示当前状态和提示信息
2. **边框检测**:寻找白色边框(灰度150-255)
3. **距离计算**:使用校准数据计算摄像头到目标的距离
4. **目标检测**:在边框区域内寻找黑色目标(灰度0-150)
5. **状态处理**:
- **状态1/2**:寻找最小正方形并计算其宽度
- **状态4**:识别指定编号的正方形并测量
- **状态5**:测量倾斜角度并计算实际宽度
6. **用户输入模拟**:在状态3时自动生成随机编号
### 系统工作流程
1. **初始化**:等待启动状态(state=0)
2. **启动测量**:模拟按键触发状态转换
3. **目标选择**:
- 随机选择两个目标物(状态1和2)
- 等待用户输入指定目标(状态3)
4. **测量执行**:
- 距离和最小正方形测量(状态1/2)
- 指定目标测量(状态4)
- 角度测量(状态5)
5. **结果显示**:在画面和串口输出测量结果
6. **状态重置**:完成所有测量后回到初始状态
### 改进建议
1. **数字识别增强**:使用模板匹配或简单OCR
```python
# 简单数字识别函数示例
def recognize_number(img, roi):
# 裁剪ROI区域
number_img = img.copy(roi=roi)
# 二值化处理
number_img.binary([(0, 100)])
# 简单模板匹配
best_match = 0
best_score = 0
for num, template in 数字模板.items():
# 这里应使用实际的模板匹配算法
score = number_img.find_template(template, 0.7)
if score > best_score:
best_score = score
best_match = num
return best_match if best_match > 0 else random.randint(1, 4)
```
2. **角度计算改进**:使用最小外接矩形
```python
def calculate_angle(blob):
# 获取最小外接矩形
rect = blob.min_rect()
# 计算角度(OpenMV可能不支持直接获取角度)
# 这里使用宽高比作为替代方案
width = rect[2]
height = rect[3]
aspect_ratio = max(width, height) / min(width, height) if min(width, height) > 0 else 1
angle = math.atan(aspect_ratio) * 180 / math.pi
return min(max(angle, 0), 90) # 限制在0-90度之间
```
3. **多目标处理**:扩展支持同时检测多个目标物
```python
def process_multiple_targets(img, frame_blob):
# 在边框内查找所有目标物
targets = []
for i in range(1, 5):
color_range = 目标物数据[i]["color"]
blobs = img.find_blobs([color_range], roi=frame_roi)
if blobs:
target = find_center_max_blob(blobs)
if target:
targets.append((i, target))
return targets
```