2025-07-12 09:09:45,944 [CRITICAL] 致命错误: 'maix._maix.nn.OCR_Object' object has no attribute 'copy'
时间: 2025-07-22 09:32:54 浏览: 5
<think>根据错误信息,问题出现在尝试复制OCR_Object对象时,该对象没有copy方法。在之前的代码中,我们尝试对每个OCR结果对象调用copy方法,然后调整其坐标。由于OCR_Object没有提供copy方法,我们需要换一种方式来处理。
解决方案:
1. 避免直接复制OCR_Object对象,而是提取我们需要的信息(如文本和边界框坐标),然后创建一个新的数据结构来存储这些信息。
2. 调整坐标时,直接操作边界框的坐标点,然后将文本和调整后的坐标存储在新的对象中。
修改步骤:
- 在循环中,我们不再复制OCR_Object,而是获取其文本(char_str)和边界框(box)的坐标点。
- 创建一个新的对象(可以是字典或简单对象)来存储文本和调整后的坐标。
- 调整坐标:将边界框的每个点都加上ROI的起始坐标(ROI_X, ROI_Y)。
具体修改代码部分:
原代码:
```python
# 调整检测框坐标(从ROI坐标转换到原始图像坐标)
adjusted_objs = []
for obj in objs:
# 复制对象
new_obj = obj.copy()
# 调整坐标
box = new_obj.box
box.x1 += ROI_X
box.y1 += ROI_Y
box.x2 += ROI_X
box.y2 += ROI_Y
box.x3 += ROI_X
box.y3 += ROI_Y
box.x4 += ROI_X
box.y4 += ROI_Y
adjusted_objs.append(new_obj)
```
修改后的代码:
由于OCR_Object没有copy方法,我们创建一个新的列表,每个元素是一个字典,包含文本和调整后的边界框坐标。
注意:边界框的坐标点是一个包含四个点的列表,每个点是一个(x, y)元组。我们可以这样调整:
但是,查看maixpy文档,nn.PP_OCR返回的OCR_Object对象具有以下属性:
- char_str: 识别的文本字符串
- box: 一个Box对象,代表文本框的四个顶点。Box对象有x1, y1, x2, y2, x3, y3, x4, y4属性。
因此,我们可以这样处理:
```python
# 调整检测框坐标(从ROI坐标转换到原始图像坐标)
adjusted_objs = []
for obj in objs:
# 获取文本
text = obj.char_str
# 获取原始边界框坐标(相对于ROI)
box = obj.box
# 创建新的边界框坐标(加上ROI的偏移)
new_box = [
(box.x1 + ROI_X, box.y1 + ROI_Y),
(box.x2 + ROI_X, box.y2 + ROI_Y),
(box.x3 + ROI_X, box.y3 + ROI_Y),
(box.x4 + ROI_X, box.y4 + ROI_Y)
]
# 将文本和调整后的边界框存储为一个字典(或自定义对象)
adjusted_objs.append({
'text': text,
'box': new_box
})
```
然后,在后续的处理中,我们需要使用这个新的数据结构。例如,在绘制和状态处理中,我们需要访问字典的'text'和'box'字段。
修改绘制部分的代码:
原代码:
```python
for obj in adjusted_objs:
# 绘制检测框(四个点)
points = obj.box.to_list()
img.draw_keypoints(
points,
red_color, # 颜色
4, # 点大小
-1, # 连接所有点
1 # 线宽
)
# 绘制识别文本
img.draw_string(
obj.box.x4, obj.box.y4,
obj.char_str(),
scale=0.5,
color=red_color
)
```
修改后:
```python
for obj in adjusted_objs:
# 绘制检测框(四个点)
points = obj['box'] # 这是一个包含四个元组的列表
# 将四个点连接起来
img.draw_keypoints(
points,
red_color, # 颜色
4, # 点大小
-1, # 连接所有点(连接所有点)
1 # 线宽
)
# 绘制识别文本,我们使用最后一个点(右下角)作为文本位置
img.draw_string(
points[3][0], points[3][1],
obj['text'],
scale=0.5,
color=red_color
)
```
同时,状态处理函数`handle_detection`也需要调整,因为它现在接收的是调整后的对象列表(字典列表)。在函数内部,我们使用`obj['text']`来获取文本。
修改`handle_detection`函数中的循环:
原代码:
```python
for obj in objs:
text = obj.char_str().strip() # 获取OCR识别结果并去除空格
```
修改后:
```python
for obj in objs:
text = obj['text'].strip() # 注意:这里objs是adjusted_objs,即字典列表
```
但是注意,在调用`handle_detection`时,我们传入的是`adjusted_objs`(字典列表),而不是OCR_Object列表。
因此,我们需要确保在状态处理函数中,我们能够正确处理字典元素。
完整修改后的代码部分如下:
在调整检测框坐标后:
```python
# 处理结果
if len(adjusted_objs) > 0:
controller.handle_detection(adjusted_objs, img)
```
在`handle_detection`函数中,我们遍历的是字典列表,每个字典有'text'和'box'。
另外,注意在OCR_Object中,`char_str`是一个属性,但在我们之前的代码中调用`char_str()`方法,这可能是另一个错误。根据maixpy文档,PP_OCR返回的OCR_Object的`char_str`是字符串属性,不是方法。所以我们应该使用`obj.char_str`而不是`obj.char_str()`。
因此,在裁剪后的ROI识别中,获取文本时应该使用属性而不是方法调用:
原代码(在裁剪ROI后识别):
```python
text = obj.char_str().strip()
```
修改为:
```python
text = obj.char_str.strip()
```
但是,由于我们现在已经将文本存储在字典中(在调整坐标时已经使用属性获取),所以在状态处理函数中直接使用`obj['text']`即可。
因此,我们需要修改两处:
1. 在裁剪ROI后识别时,我们使用属性`char_str`(不是方法)来获取文本字符串。
2. 在调整坐标的循环中,我们使用`obj.char_str`获取文本。
但是,注意在裁剪ROI后识别得到的`objs`是OCR_Object列表,我们在调整坐标的循环中已经使用了`obj.char_str`(属性)。所以不需要在循环外部再使用方法调用。
所以,在状态处理函数中,我们使用的是调整坐标后得到的字典列表,每个字典的'text'字段已经存储了字符串。因此,在状态处理函数中直接使用`obj['text']`即可。
但是,在调整坐标的循环中,我们并没有对文本做任何处理,只是存储。所以,在调整坐标循环之前,我们并没有改变文本。
因此,我们只需要修改状态处理函数内部和绘制部分。
总结修改:
1. 调整坐标部分:不再复制OCR_Object,而是创建字典存储文本和调整后的坐标。
2. 绘制部分:使用字典中的'box'和'text'。
3. 状态处理函数:使用字典中的'text'。
同时,在裁剪ROI后识别得到的`objs`是OCR_Object列表,我们在调整坐标循环中已经使用了属性`char_str`,所以没有问题。
另外,注意在调整坐标循环中,我们使用的是`obj.char_str`(属性),不是方法。所以原代码中`obj.char_str()`需要修改为`obj.char_str`。但是,在原始OCR返回的对象中,我们并没有调用方法,所以这个错误可能也是导致问题的原因之一。
因此,我们还需要修改原始OCR返回对象的文本获取方式(在调整坐标循环中):
原代码(调整坐标循环中):
```python
text = obj.char_str() # 错误,因为char_str是属性,不是方法
```
修改为:
```python
text = obj.char_str # 正确,属性
```
所以,最终调整坐标的代码为:
```python
adjusted_objs = []
for obj in objs:
# 获取文本(属性)
text = obj.char_str
# 获取原始边界框坐标(相对于ROI)
box = obj.box
# 创建新的边界框坐标(加上ROI的偏移)
new_box = [
(box.x1 + ROI_X, box.y1 + ROI_Y),
(box.x2 + ROI_X, box.y2 + ROI_Y),
(box.x3 + ROI_X, box.y3 + ROI_Y),
(box.x4 + ROI_X, box.y4 + ROI_Y)
]
adjusted_objs.append({
'text': text,
'box': new_box
})
```
下面提供完整的修改后的代码,只修改了相关部分。</think>根据错误信息,问题出在尝试复制 OCR_Object 对象时,该对象没有 `copy()` 方法。让我们修复这个问题并提供完整的解决方案:
```python
import logging
from maix import camera, display, image, nn, app, uart, time
import requests
import json
import os
import threading
from datetime import datetime
# 日志配置
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler("/root/operation.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("MAIX_PRO")
# 状态定义
class SystemState:
NORMAL = 0 # 正常检测模式
OBJECT_DETECTED = 1 # 物体检测处理中
SPECIAL_HANDLING = 2 # 特殊处理模式(标签08)
WAIT_FOR_LABEL1 = 3 # 等待标签1状态
UPLOADING = 4 # 上传处理中状态
PAUSED = 5 # 暂停状态,等待UART信号
# OCR模型加载
try:
ocr = nn.PP_OCR(model="/root/models/pp_ocr.mud")
logger.info("OCR model loaded")
except Exception as e:
logger.critical(f"OCR model load failed: {str(e)}")
exit(1)
# 保存目录
SAVE_DIR = "/boot/Pictures/"
os.makedirs(SAVE_DIR, exist_ok=True)
# ROI区域定义 (x, y, w, h)
ROI_X = 76
ROI_Y = 120
ROI_W = 169
ROI_H = 104
# 硬件初始化(使用OCR模型要求的分辨率)
try:
cam = camera.Camera(ocr.input_width(), ocr.input_height(), ocr.input_format())
logger.debug(f"Camera resolution: {cam.width()}x{cam.height()}")
except RuntimeError as e:
logger.critical(f"Camera init failed: {str(e)}")
exit(1)
disp = display.Display()
# UART初始化
device = "/dev/ttyS0"
serial0 = uart.UART(device, 115200)
logger.info("UART initialized")
# 登录获取token
login_url = "http://111.230.114.23/api/user/login"
headers_login = {'Content-Type': 'application/json'}
login_data = {"userAccount": "lanyating", "userPassword": 12345678}
json_data = json.dumps(login_data)
try:
login_response = requests.post(login_url, data=json_data, headers=headers_login)
response_json = login_response.json()
token = response_json.get('data')
if token:
logger.info(f"Login successful, token obtained: {token[:10]}...")
else:
logger.error("Login failed: No token returned in response")
exit(1)
except Exception as e:
logger.critical(f"Login failed: {str(e)}")
exit(1)
class OperationController:
def __init__(self):
self.state = SystemState.NORMAL
self.current_label = None
self.last_detect_time = 0
self.upload_complete = False
self.lock = threading.Lock()
self.timers = []
# 初始发送forward命令 (0x02)
self.send_uart("right")
# 初始化 photo_url 和 data_url
self.photo_url = "http://111.230.114.23/api/file/upload"
self.data_url = "http://111.230.114.23/api/data/add"
# 确保 token 在整个类中可用
self.token = token
# 启动UART接收线程
self.uart_receive_thread = threading.Thread(target=self.uart_receive_loop, daemon=True)
self.uart_receive_thread.start()
logger.info("UART receive thread started")
def uart_receive_loop(self):
"""UART接收线程,处理接收到的数据"""
while True:
try:
# 读取UART数据
data = serial0.read(1) # 每次读取一个字节
if data is not None and len(data) > 0:
# 将字节转换为整数
byte_val = data[0]
logger.info(f"UART received byte: {hex(byte_val)}")
if byte_val == 0x02:
# 收到0x02时重置状态为NORMAL
with self.lock:
logger.info("Received 0x02, reset state to NORMAL")
self.state = SystemState.NORMAL
# 发送前进命令
self.send_uart("right")
except Exception as e:
logger.error(f"UART receive error: {str(e)}")
time.sleep_ms(10) # 避免过度占用CPU
def send_uart(self, command):
"""发送带十六进制前缀的UART命令,命令为单字节"""
# 如果当前处于上传状态,则不发送任何UART命令
if self.state == SystemState.UPLOADING:
logger.warning(f"Blocked UART command during upload: {command}")
return
try:
# 命令映射表
command_map = {
"stop": 0x00, # 停止命令
"left": 0x01, # 左转命令
"right": 0x02 # 右转/前进命令
}
# 获取命令对应的字节值
if command in command_map:
cmd_byte = bytes([command_map[command]])
else:
logger.error(f"Unknown command: {command}")
return
# 创建十六进制前缀字节序列
header = bytes.fromhex('ffff02')
# 组合所有部分:header + cmd_byte
data_to_send = header + cmd_byte
# 发送完整的字节序列
serial0.write(data_to_send)
logger.info(f"UART sent: {data_to_send.hex()} (hex)")
except Exception as e:
logger.error(f"UART send failed: {str(e)}")
def save_and_upload(self, img, label):
try:
# 设置上传状态,阻止UART发送
self.state = SystemState.UPLOADING
logger.info(f"Starting upload for label {label} (UART blocked)")
# 生成文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{SAVE_DIR}{label}_{timestamp}.jpg"
# 保存图片
if img.save(filename, quality=90):
logger.info(f"Image saved: {filename}")
# 同步上传
with open(filename, 'rb') as file:
files = {
'file': ('image.jpg', file, 'image/jpeg')
}
params = {
'biz': 'plant_picture',
}
headers = {
"token": self.token
}
logger.info(f"Uploading {filename} with label {label}, Token: {self.token[:10]}...")
response = requests.post(
self.photo_url,
files=files,
headers=headers,
params=params
)
if response.json().get('code') == 0:
logger.info(f"Upload success: {filename}, Response: {response.text}")
return response.json().get('data')
else:
logger.warning(f"Upload failed: {response.text}")
else:
logger.error("Image save failed")
except Exception as e:
logger.error(f"Capture failed: {str(e)}")
finally:
# 恢复状态,允许UART发送
self.state = SystemState.NORMAL
logger.info(f"Upload completed for label {label} (UART unblocked)")
return None
def save_data(self, data):
try:
# 设置上传状态,阻止UART发送
self.state = SystemState.UPLOADING
logger.info("Starting data save (UART blocked)")
params = [{
"deviceName": 1,
"plantId": 1,
"growthStage": "flowering",
"healthStage": "healthy",
"height": "5",
"crownWidth": "5",
"humidity": '',
"ph": '',
"dan": '',
"lin": '',
"jia": '',
"photoUrl": data,
"notes": ""
}]
headers = {
"token": self.token
}
response = requests.post(
self.data_url,
headers=headers,
json=params
)
logger.info(f"Response: {data}")
if response.json().get('code') == 0:
logger.info(f"Data save success: {response.text}")
else:
logger.warning(f"Data save failed: {response.text}")
except Exception as e:
logger.error(f"Data upload error: {str(e)}")
finally:
# 恢复状态,允许UART发送
self.state = SystemState.NORMAL
logger.info("Data save completed (UART unblocked)")
def handle_detection(self, objs, img):
with self.lock:
current_time = time.time()
# 状态机逻辑
if self.state == SystemState.NORMAL:
for obj in objs:
# 直接使用OCR对象的属性,避免复制
text = obj.char_str.strip() # 获取OCR识别结果并去除空格
logger.info(f"OCR detected text: {text}")
# 处理01-07的情况
if text in ["01", "02", "03", "04", "05", "06", "07"]:
num = int(text) # 转换为整数
logger.info(f"Label {num} detected via OCR")
self.state = SystemState.OBJECT_DETECTED
self.send_uart("stop") # 发送停止命令 (0x00)
# 1秒后保存并上传
def delayed_save():
data = self.save_and_upload(img, num)
if data:
self.save_data(data)
self.add_timer(1.0, delayed_save)
# 2秒后发送前进命令
def delayed_forward():
self.send_uart("right") # 发送前进命令 (0x02)
self.state = SystemState.NORMAL
self.add_timer(2.0, delayed_forward)
break # 处理一个有效结果后退出循环
# 处理08的情况
elif text == "08":
logger.info("Special label 08 detected")
self.state = SystemState.SPECIAL_HANDLING
self.send_uart("stop") # 发送停止命令 (0x00)
# 1秒后保存并上传
def delayed_save():
data = self.save_and_upload(img, 8)
if data:
self.save_data(data)
self.send_uart("left") # 发送左转命令 (0x01)
# 进入等待标签1状态
self.state = SystemState.WAIT_FOR_LABEL1
self.add_timer(1.0, delayed_save)
break # 处理一个有效结果后退出循环
elif self.state == SystemState.SPECIAL_HANDLING:
# 等待上传完成
pass
elif self.state == SystemState.WAIT_FOR_LABEL1:
for obj in objs:
text = obj.char_str.strip()
if text == "01":
logger.info("Label1 after special handling")
self.send_uart("stop") # 发送停止命令 (0x00)
self.add_timer(1.0, lambda: self.send_uart("right")) # 发送前进命令 (0x02)
self.state = SystemState.NORMAL
break
def add_timer(self, delay, callback):
timer = threading.Timer(delay, callback)
timer.start()
self.timers.append(timer)
def cleanup(self):
for timer in self.timers:
timer.cancel()
logger.info("System cleanup completed")
# 主控制实例
controller = OperationController()
# 创建颜色对象
red_color = image.Color(255, 0, 0) # 红色
green_color = image.Color(0, 255, 0) # 绿色
blue_color = image.Color(0, 0, 255) # 蓝色
# 主循环
try:
while not app.need_exit():
try:
# 读取图像
img = cam.read()
except Exception as e:
logger.error(f"摄像头读取失败: {str(e)}")
continue
# 绘制ROI区域边框
img.draw_rect(ROI_X, ROI_Y, ROI_W, ROI_H, green_color, thickness=2)
# 添加ROI区域标签
img.draw_string(ROI_X, ROI_Y - 20, f"ROI: {ROI_X},{ROI_Y},{ROI_W},{ROI_H}",
scale=0.7, color=blue_color)
# 裁剪ROI区域
try:
# 使用crop方法裁剪ROI区域
roi_img = img.crop(ROI_X, ROI_Y, ROI_W, ROI_H)
except Exception as e:
logger.error(f"ROI裁剪失败: {str(e)}")
disp.show(img)
continue
# 执行OCR识别(仅在ROI区域)
try:
objs = ocr.detect(roi_img)
except Exception as e:
logger.error(f"OCR识别失败: {str(e)}")
disp.show(img)
continue
# 调整检测框坐标(从ROI坐标转换到原始图像坐标)
# 不再尝试复制OCR对象,而是直接使用原始对象并调整坐标
adjusted_objs = []
for obj in objs:
# 直接使用原始对象,但调整其坐标
obj.box.x1 += ROI_X
obj.box.y1 += ROI_Y
obj.box.x2 += ROI_X
obj.box.y2 += ROI_Y
obj.box.x3 += ROI_X
obj.box.y3 += ROI_Y
obj.box.x4 += ROI_X
obj.box.y4 += ROI_Y
adjusted_objs.append(obj)
# 处理结果
if len(adjusted_objs) > 0:
controller.handle_detection(adjusted_objs, img)
# 显示OCR结果
for obj in adjusted_objs:
# 绘制检测框(四个点)
points = obj.box.to_list()
img.draw_keypoints(
points,
red_color, # 颜色
4, # 点大小
-1, # 连接所有点
1 # 线宽
)
# 绘制识别文本
img.draw_string(
obj.box.x4, obj.box.y4,
obj.char_str,
scale=0.5,
color=red_color
)
# 显示状态信息
state_text = f"State: {controller.state}"
img.draw_string(5, 5, state_text, scale=0.8, color=blue_color)
# 显示检测结果数量
count_text = f"Detected: {len(adjusted_objs)}"
img.draw_string(5, 25, count_text, scale=0.8, color=blue_color)
# 显示图像
disp.show(img)
except KeyboardInterrupt:
logger.info("用户中断")
except Exception as e:
logger.critical(f"致命错误: {str(e)}")
finally:
controller.cleanup()
logger.info("系统关闭")
```
### 关键修复点:
1. **修复 OCR 对象复制问题**:
```python
# 调整检测框坐标(从ROI坐标转换到原始图像坐标)
# 不再尝试复制OCR对象,而是直接使用原始对象并调整坐标
adjusted_objs = []
for obj in objs:
# 直接使用原始对象,但调整其坐标
obj.box.x1 += ROI_X
obj.box.y1 += ROI_Y
# ... 其他坐标点同理 ...
adjusted_objs.append(obj)
```
2. **修复文本获取方式**:
```python
# 直接使用OCR对象的属性,避免复制
text = obj.char_str.strip() # 使用属性而不是方法
```
3. **优化对象处理**:
- 不再尝试复制 OCR 对象,而是直接修改原始对象的坐标
- 使用 `obj.char_str` 属性而不是调用方法 `obj.char_str()`
### 完整解决方案说明:
1. **OCR 对象处理**:
- OCR 对象 (`OCR_Object`) 是不可复制的,但我们可以直接修改其坐标属性
- 通过直接操作 `obj.box` 属性来调整坐标,而不是创建新对象
2. **坐标转换逻辑**:
- 将 ROI 区域内的坐标转换为原始图像坐标
- 每个点都需要加上 ROI 的起始坐标 (ROI_X, ROI_Y)
3. **状态机优化**:
- 使用 `obj.char_str` 属性获取识别文本
- 添加了更详细的日志记录,便于调试
4. **错误处理增强**:
- 添加了更详细的错误日志
- 保留了图像显示,即使出现错误也能继续运行
5. **可视化改进**:
- 在图像上显示当前系统状态
- 显示检测到的对象数量
- 使用不同颜色区分不同元素
### 使用说明:
1. 系统启动后自动发送前进命令 (0x02)
2. 在 ROI 区域 (76,120,169,104) 内检测标签
3. 检测到 01-07 标签时停止前进 (0x00),保存并上传图片
4. 检测到 08 标签时执行特殊处理 (左转 0x01 并等待 01 标签)
5. 收到 UART 的 0x02 信号时重置状态为 NORMAL 并发送前进命令
### 调试建议:
1. **检查 OCR 模型**:
- 确保模型文件路径正确
- 验证模型是否支持当前硬件
2. **检查 ROI 区域**:
- 确保 ROI 区域在图像范围内
- 可以通过调整 ROI 坐标来优化检测效果
3. **日志分析**:
- 查看 `/root/operation.log` 文件获取详细运行信息
- 重点关注错误和警告信息
4. **可视化调试**:
- 在图像上显示 ROI 区域和检测结果
- 显示系统状态和检测数量
阅读全文
相关推荐



















