Mac下Python多线程调用OpenCV触发NSInternalInconsistencyException错误求助
macOS下使用threading和cv2预览摄像头时触发NSInternalInconsistencyException异常的解决方法
问题描述
我用Python的threading和cv2库写了一段摄像头预览代码:
class CamThread(threading.Thread): def __init__(self, previewname, camid): threading.Thread.__init__(self) self.previewname = previewname self.camid = camid def run(self): print("Starting " + self.previewname) previewcam(self.previewname, self.camid) # Function to preview the camera. def previewcam(previewname, camid): cv2.namedWindow(previewname) cam = cv2.VideoCapture(camid) if cam.isOpened(): rval, frame = cam.read() else: rval = False while rval: cv2.imshow(previewname, frame) rval, frame = cam.read() key = cv2.waitKey(20) if key == 27: # Press ESC to exit/close each window. break cv2.destroyWindow(previewname)
运行后立刻抛出未捕获异常:
Python[58828:4239666] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!' *** First throw call stack: ( 0 CoreFoundation 0x00007ff8089e6773 __exceptionPreprocess + 242 1 libobjc.A.dylib 0x00007ff808746bc3 objc_exception_throw + 48 2 CoreFoundation 0x00007ff808a0f056 -[NSException raise] + 9 3 AppKit 0x00007ff80b3cb0a4 -[NSWindow(NSWindow_Theme) _postWindowNeedsToResetDragMarginsUnlessPostingDisabled] + 321 4 AppKit 0x00007ff80b3b7054 -[NSWindow _initContent:styleMask:backing:defer:contentView:] + 1288 5 AppKit 0x00007ff80b3b6b46 -[NSWindow initWithContentRect:styleMask:backing:defer:] + 42 6 AppKit 0x00007ff80b697aa7 -[NSWindow initWithContentRect:styleMask:backing:defer:screen:] + 50 7 cv2.cpython-38-darwin.so 0x000000010d828795 cvNamedWindow + 677 8 cv2.cpython-38-darwin.so 0x000000010d825555 _ZN2cv11namedWindowERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEEi + 53 9 cv2.cpython-38-darwin.so 0x000000010c802d5b _ZL23pyopencv_cv_namedWindowP7_objectS0_S0_ + 235 10 Python3 0x0000000105998c83 cfunction_call_varargs + 115 11 Python3 0x0000000105998709 _PyObject_MakeTpCall + 377 12 Python3 0x0000000105a7d67c call_function + 636 13 Python3 0x0000000105a7a239 _PyEval_EvalFrameDefault + 28121 14 Python3 0x000000010599903b function_code_fastcall + 171 15 Python3 0x0000000105a7d54f call_function + 335 16 Python3 0x0000000105a7a2da _PyEval_EvalFrameDefault + 28282 17 Python3 0x000000010599903b function_code_fastcall + 171 18 Python3 0x0000000105a7d54f call_function + 335 19 Python3 0x0000000105a7a215 _PyEval_EvalFrameDefault + 28085 20 Python3 0x000000010599903b function_code_fastcall + 171 21 Python3 0x0000000105a7d54f call_function + 335 22 Python3 0x0000000105a7a215 _PyEval_EvalFrameDefault + 28085 23 Python3 0x000000010599903b function_code_fastcall + 171 24 Python3 0x000000010599ba33 method_vectorcall + 323 25 Python3 0x0000000105998a24 PyVectorcall_Call + 100 26 Python3 0x0000000105b1fecb t_bootstrap + 75 27 Python3 0x0000000105ad06d9 pythread_wrapper ) libc++abi: terminating with uncaught exception of type NSException zsh: abort
这是Apple系统相关的错误,完全不知道怎么解决,有人碰到过吗?
问题原因
macOS的AppKit框架硬性要求:所有UI相关操作(比如创建窗口、更新窗口内容)必须在主线程执行。你的代码里把cv2.namedWindow、cv2.imshow这些创建和刷新窗口的操作放到了子线程中,直接违反了这个规则,所以触发了NSInternalInconsistencyException异常。
解决方案
把UI操作全部移到主线程,子线程只负责读取摄像头帧数据,再通过线程安全的方式把数据传递给主线程显示。可以用Python自带的queue模块实现线程间的数据交互:
修改后的代码
import threading import cv2 from queue import Queue class CamThread(threading.Thread): def __init__(self, camid, frame_queue): threading.Thread.__init__(self) self.camid = camid self.frame_queue = frame_queue self.running = True def run(self): print("Starting camera thread") cam = cv2.VideoCapture(self.camid) if not cam.isOpened(): print(f"Failed to open camera {self.camid}") self.running = False return while self.running: rval, frame = cam.read() if rval: # 只保留最新的一帧,避免队列堆积 if not self.frame_queue.empty(): try: self.frame_queue.get_nowait() except Queue.Empty: pass self.frame_queue.put(frame) else: break cam.release() print("Camera thread stopped") def main(): previewname = "Camera Preview" camid = 0 frame_queue = Queue(maxsize=1) # 启动摄像头读取子线程 cam_thread = CamThread(camid, frame_queue) cam_thread.start() # 主线程负责所有UI操作 cv2.namedWindow(previewname) while True: if not frame_queue.empty(): frame = frame_queue.get() cv2.imshow(previewname, frame) key = cv2.waitKey(20) if key == 27: # 按ESC键退出 cam_thread.running = False break cv2.destroyWindow(previewname) cam_thread.join() if __name__ == "__main__": main()
代码说明
- 子线程
CamThread仅负责读取摄像头帧,把帧存入线程安全的Queue,不涉及任何UI操作。 - 主线程单独处理窗口创建、帧显示和按键事件,完全符合macOS的UI线程规则。
- 队列设置
maxsize=1,确保只保留最新的一帧,防止内存占用过高。
如果只是单摄像头预览,也可以直接去掉线程,在主线程完成所有操作,这种方式更简单:
import cv2 def main(): previewname = "Camera Preview" camid = 0 cv2.namedWindow(previewname) cam = cv2.VideoCapture(camid) if cam.isOpened(): rval, frame = cam.read() else: rval = False while rval: cv2.imshow(previewname, frame) rval, frame = cam.read() key = cv2.waitKey(20) if key == 27: break cam.release() cv2.destroyWindow(previewname) if __name__ == "__main__": main()
内容的提问来源于stack exchange,提问作者bobthebuilder
相关产品推荐
相关产品推荐

