hiroi-sora / PaddleOCR-json

OCR离线图片文字识别命令行windows程序,以JSON字符串形式输出结果,方便别的程序调用。提供各种语言API。由 PaddleOCR C++ 编译。
Apache License 2.0
825 stars 109 forks source link

关于 kill 进程的疑惑 #66

Closed librix closed 9 months ago

librix commented 11 months ago

想请问一下进程该如何结束。

我使用如下代码创建子进程:

def OCR_text_threading(srcfile):
    argument = {'config_path': "models/config_chinese.txt"}
    ocr = GetOcrApi("./PaddleOCR-json_v.1.3.0/PaddleOCR-json.exe", argument)
    try:
        res = ocr.run(srcfile)
    except Exception as e:
        print(f'OCR_text_threading 循环失败: {e}')
        res = 0
    ocr.ret.kill()
    # ocr.exit()
    if res['code'] == 100:
        textBlocksNew = tbpu.run_merge_line_h_m_paragraph(res['data'])
        s = ""
        for i in range(len(textBlocksNew)):
            s1 = textBlocksNew[i]['text']
            s += s1
        print(s)
        clipboard.copy(s)

def OCR_text(srcfile):
    print(srcfile)
    try:
        my_thread = threading.Thread(target=OCR_text_threading, args=(srcfile,))
        print(my_thread)
        my_thread.start()
    except Exception as e:
        print(f'Thread 循环失败: {e}')
    return

某图片路径为:Z:\0722pz\0680_20230727110609_00.jpg

单次执行可成功。但是二次执行会爆出找不到文件的错误,重启重新识别正常:

<Thread(Thread-3 (OCR_text_threading), initial)>
Exception in thread Thread-3 (OCR_text_threading):
Traceback (most recent call last):
  File "F:\language\miniconda3\lib\threading.py", line 1016, in _bootstrap_inner
    self.run()
  File "F:\language\miniconda3\lib\threading.py", line 953, in run
    self._target(*self._args, **self._kwargs)
  File "E:\MyCode\python\picture-crop\main - v0.3.py", line 429, in OCR_text_threading
    ocr = GetOcrApi("./PaddleOCR-json_v.1.3.0/PaddleOCR-json.exe", argument)
  File "E:\MyCode\python\picture-crop\PPOCR_api.py", line 199, in GetOcrApi
    return PPOCR_pipe(exePath, argument)
  File "E:\MyCode\python\picture-crop\PPOCR_api.py", line 35, in __init__
    self.ret = subprocess.Popen(  # 打开管道
  File "F:\language\miniconda3\lib\subprocess.py", line 971, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "F:\language\miniconda3\lib\subprocess.py", line 1440, in _execute_child
    hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
FileNotFoundError: [WinError 2] 系统找不到指定的文件。
Exception ignored in: <function PPOCR_pipe.__del__ at 0x000001D24FCD6A70>
Traceback (most recent call last):
  File "E:\MyCode\python\picture-crop\PPOCR_api.py", line 122, in __del__
    self.exit()
  File "E:\MyCode\python\picture-crop\PPOCR_api.py", line 103, in exit
    self.ret.kill()  # 关闭子进程
AttributeError: 'PPOCR_pipe' object has no attribute 'ret'

ocr.ret.kill() 似乎不起作用。该如何结束进程?

hiroi-sora commented 11 months ago

我尝试使用你的代码原文,它运行正常,并没有复现异常。

image

针对你的问题,我有几点想法:

  1. 你这问题并不是由 ocr.ret.kill() 所引起,这只是表象。根本原因是 GetOcrApi() 执行异常,导致 ocr 对象的 self.ret 属性未构造,因此无法调用 self.ret.kill()
  2. 至于为什么 GetOcrApi() 执行异常,大概率是传入的路径(即 ./PaddleOCR-json_v.1.3.0/PaddleOCR-json.exe ) 有问题。这是个相对路径,它的正确性依赖于你python代码的工作目录。可能第一次执行时,工作目录确实是正确的。但是后续操作中,你代码的某些模块修改了当前工作目录,或者受到多线程异步机制的影响,导致第二次执行OCR时相对路径不正确。
  3. 你可以在每次开始OCR前,执行下述代码,看看工作目录是否被改变了:
    print("当前工作目录:", os.getcwd())
  4. 你可以尝试传入exe的绝对路径,看看多次调用是否正确。只需要改GetOcrApi的第一个路径就行了,不用改argumentconfig_path
    ocr = GetOcrApi("绝对路径/PaddleOCR-json.exe", argument)
hiroi-sora commented 11 months ago

另外,不建议你每执行一次OCR,就要 GetOcrApi 构造一个ocr对象然后kill掉。因为引擎初始化需要一定时间。

更高效的流程是,程序开始时用 GetOcrApi 构造一个对象,然后之后每次调用都使用这同一个对象。这样可以避免重复初始化。直到要退出程序时,才kill掉该对象。

或者当需要更改参数(模型库路径)时,再删掉旧对象,构造新对象。

librix commented 11 months ago

是的,您的直觉非常准确。 我回顾了代码,因为糟糕的代码水平,在使用函数修改图片的时候,为了方便保存操作,发现 os.chdir(outerpath) 改变了 os.getcwd()的值。

    os.chdir(outerpath)   # 改变 工作路径
    #切割文件名: base.son.....
    base1 = str(os.path.basename(srcfile)[:-4])
    date2 = str(time.strftime("%Y%m%d%H%M%S", time.gmtime()))
    Suffix = str(os.path.basename(srcfile).split(".")[-1])
    outfile = base1 +"_"+ date2

    for id, img_c in enumerate(img_cropped):
        # print(len(enumerate(img_cropped)))
        outfile_i = outfile + "_" + "{:02d}".format(id) + "." + Suffix
        cv2.imwrite(outfile_i, img_c)
        print("write pic in ", outfile_i)
    print(os.path.basename(srcfile) + " splits 完毕。")

感谢你的慷慨~

关于您的「用 GetOcrApi 构造一个对象」建议,我觉得很有道理,正在修改中~

librix commented 11 months ago

关于您的「用 GetOcrApi 构造一个对象」建议,我觉得很有道理,正在修改中~

因为程序启动比较慢,所以新建了一个进程:

import tbpu
from PPOCR_api import GetOcrApi
import threading

def OCR_text_Thread(srcfile):
    argument = {'config_path': "models/config_chinese.txt"}
    ocr = GetOcrApi("./PaddleOCR-json_v.1.3.0/PaddleOCR-json.exe", argument)
    res = ocr.run(srcfile)
    # ocr.exit()
    if res['code'] == 100:
        textBlocksNew = tbpu.run_merge_line_h_m_paragraph(res['data'])
        s = ""
        for i in range(len(textBlocksNew)):
            s1 = textBlocksNew[i]['text']
            s += s1
        print(s)
        clipboard.copy(s)
    ocr.exit()
    return

def OCR_text(srcfile):
    print(srcfile)
    my_thread = threading.Thread(target=OCR_text_Thread, args=(srcfile,))
    my_thread.start()

if __name__ == '__main__':
    isOCR = False
    while 1:
        if not isOCR:
            print("当前工作目录:", os.getcwd())
            OCR_text(srcfile)
            isOCR = True

因为识别过程也很慢,因此识别过程也独立进程,设立两个进程: 一个初始化Thread,一个正常Thread2,Thread2调用Thread1产生的ocr对象:

import tbpu
from PPOCR_api import GetOcrApi
import threading

def OCR_text_Thread1(srcfile):
    global ocr
    argument = {'config_path': "models/config_chinese.txt"}
    ocr = GetOcrApi("./PaddleOCR-json_v.1.3.0/PaddleOCR-json.exe", argument)
    res = ocr.run(srcfile)
    # ocr.exit()
    if res['code'] == 100:
        textBlocksNew = tbpu.run_merge_line_h_m_paragraph(res['data'])
        s = ""
        for i in range(len(textBlocksNew)):
            s1 = textBlocksNew[i]['text']
            s += s1
        print(s)
        clipboard.copy(s)
    return

def OCR_text_init(srcfile):
    my_thread = threading.Thread(target=OCR_text_Thread1, args=(srcfile,))
    my_thread.start()

#######################################################

def OCR_text_Thread2(srcfile):
    global ocr
    res = ocr.run(srcfile)
# ocr.exit()
    if res['code'] == 100:
        textBlocksNew = tbpu.run_merge_line_h_m_paragraph(res['data'])
        s = ""
        for i in range(len(textBlocksNew)):
            s1 = textBlocksNew[i]['text']
            s += s1
        print(s)
        clipboard.copy(s)
    return

def OCR_text(srcfile):
    my_thread = threading.Thread(target=OCR_text_Thread2, args=(srcfile,))
    my_thread.start()

if __name__ == '__main__':
    print_hi('World!')
    isOCR = False
    isOCRinit = False
    while 1:
        if not isOCRinit:
            print("当前工作目录:", os.getcwd())
            OCR_text_init(srcfile)
            isOCRinit = True
            isOCR = True
        if not isOCR:
            print("当前工作目录:", os.getcwd())
            OCR_text(srcfile)
            isOCR = True
    cv2.destroyAllWindows()  # close the window
    global ocr
    ocr.exit()

这样似乎速度有所提高。

但是,偶尔,比如切换图片太快,会出现这个问题:

Exception in thread Thread-2 (OCR_text_Thread2):
Traceback (most recent call last):
  File "F:\language\miniconda3\lib\threading.py", line 1016, in _bootstrap_inner
    self.run()
  File "F:\language\miniconda3\lib\threading.py", line 953, in run
    self._target(*self._args, **self._kwargs)
  File "E:\MyCode\python\picture-crop\main - v0.3.py", line 458, in OCR_text_Thread2
    res = ocr.run(srcfile)
NameError: name 'ocr' is not defined. Did you mean: 'oct'?

另外 ocr.exit() 放在哪里会比较合适呢?

librix commented 11 months ago

我尝试了一些方法,感觉这样是比较好的结果:


import tbpu
from PPOCR_api import GetOcrApi
import threading

def OCR_text_get_word(srcfile):
    global ocr
    try:
        res = ocr.run(srcfile)
        # ocr.exit()
        if res['code'] == 100:
            textBlocksNew = tbpu.run_merge_line_h_m_paragraph(res['data'])
            s = ""
            for i in range(len(textBlocksNew)):
                s1 = textBlocksNew[i]['text']
                s += s1
            print(s)
            clipboard.copy(s)
    except Exception as e:
        print(f' OCR_text_get_word 失败: {e}')

def OCR_text_Thread_init(srcfile):
    global ocr
    argument = {'config_path': "models/config_chinese.txt"}
    ocr = GetOcrApi("./PaddleOCR-json_v.1.3.0/PaddleOCR-json.exe", argument)
    OCR_text_get_word(srcfile)
    return

def OCR_text_init(srcfile):
    print(srcfile)
    my_thread = threading.Thread(target=OCR_text_Thread_init, args=(srcfile,))
    my_thread.start()

def OCR_text(srcfile):
    print(srcfile)
    my_thread = threading.Thread(target=OCR_text_get_word, args=(srcfile,))
    my_thread.start()

if __name__ == '__main__':
    print_hi('World!')
    isOCR = False
    isOCRinit = False
    while 1:
        if not isOCRinit:
            print("当前工作目录:", os.getcwd())
            OCR_text_init(srcfile)
            isOCRinit = True
            isOCR = True
        if not isOCR:
            print("当前工作目录:", os.getcwd())
            OCR_text(srcfile)
            isOCR = True
    cv2.destroyAllWindows()  # close the window
    global ocr
    if 'ocr' in locals():
        ocr.ret.kill()

但是会爆出 OCR_text_get_word 失败: name 'ocr' is not defined 的错误。 而将try改成判断if 'ocr' in locals():, OCR 引擎则不会执行。

hiroi-sora commented 11 months ago

关于 ocr.exit() 我补充一点,刚刚忘了。这玩意它在一般情况下是不需要手动调用的,因为在程序退出时或者ocr对象析构的时候会自动调用。请阅读文档 这部分

然后,关于你的新代码,我提一些建议:

  1. 没有必要用两个子线程来分别负责初始化和调用OCR,变量在两个线程传来传去很容易出问题。主线程负责交互,一个子线程负责管理OCR,就够了。
  2. 引擎对象(即ocr)应该隐藏在对应的子线程中,没有必要作为全局变量暴露到外界和主线程。
  3. 建议你使用面向对象的方式来编程,将需要进行子线程异步操作的代码封装到一个类中。
  4. 使用适当的异步锁机制来保证多线程安全。

以下是一个简单的示例,可以参考一下。

task.py

import threading
import os
from PPOCR_api import GetOcrApi

class TaskClass:
    def __init__(self):
        self.ocr_api = None  # ocr对象
        self.task_thread = None  # 工作线程
        self.task_list = []  # 工作队列,存放待处理的path
        self.task_lock = threading.RLock()  # 工作队列的异步锁

    # 调用一次该方法,执行一次OCR
    def run(self, path): 
        # 上锁→加入待处理队列→解锁
        self.task_lock.acquire() 
        self.task_list.append(path)
        self.task_lock.release()
        # 工作线程已经在运行了,就无需重复建立线程
        if self.task_thread:
            return
        # 未在运行,则建立线程
        self.task_thread = threading.Thread(target=self.__task_thread_run)
        self.task_thread.start()

    # 工作线程内容
    def __task_thread_run(self):
        # 未初始化引擎,则初始化
        if not self.ocr_api:
            exe_path = r"./PaddleOCR-json_v.1.3.0/PaddleOCR-json.exe"
            self.ocr_api = GetOcrApi(exe_path)
            print("OCR初始化完成!")

        # 循环,直到任务队列为空
        while True:
            self.task_lock.acquire() 
            if len(self.task_list) == 0:
                print("所有任务处理完毕!")
                self.task_lock.release()
                self.task_thread = None
                return
            # 从任务队列中取一个任务来执行
            path = self.task_list.pop(0)
            self.task_lock.release()

            print(f"当前任务路径:{path}")
            res = self.ocr_api.run(path)
            print(f"当前任务结果:{res}")

Task = TaskClass()  # 模块单例

main.py

from task import Task 

Task.run("图片1")
Task.run("图片2")
Task.run("图片3")
librix commented 11 months ago

最大的问题是我还无法理解面向对象~感谢您的思路,我会继续尝试~~