MJPEG で画像を取得する

i-PRO の監視カメラ i-PRO mini (WV-S7130W) を入手したので、MJPEG で映像取得して遊んでみます。

 

 

 

製品紹介ページ:

 

i-PRO mini

 

 

1. MJPEG 表記仕様

[概要]

MJPEG で接続するための表記を以下に記載します。

「ネットワークカメラCGIコマンドインターフェース仕様書 統合版」[1] で下記に記載されている情報を元に加筆などしています。

 

http://<user-id>:<user-password>@<カメラのIPアドレス>/nphMotionJpeg?Resolution=<解像度>&Quality=<品質>&Framerate=<フレームレート>

 

(例) http://admin:password@192.168.0.10/nphMotionJpeg?Resolution=1920x1080&Quality=Standard&Framerate=15

 

 

注意事項

 

 

 

2. i-PRO カメラと MJPEG 接続して映像を表示してみる

[概要]

とりあえず映像を取得してPC画面に表示するまでをやってみます。

 

[評価環境1]

言語 : Python, 3.10.4
OS : Windows 11 home, 21H2

 

[評価環境2]

言語 : Python, 3.8.10
OS : Ubuntu(WSL), 20.04

 

 

2-1. 方法1

まずは簡単な方法から。RTSP のコードとほとんど同じ内容で実現できました。

 

[プログラム]

[プログラムソース "connect_with_mjpeg_1_1.py"]

  1. '''
  2. [abstract]
  3. i-PRO mini (WV-S7130W) と接続して映像を表示してみる
  4. [library install]
  5. pip install opencv-python
  6. '''
  7.  
  8. import cv2
  9.  
  10. user_id = "user-id" # ご使用のカメラ設定に合わせて変更
  11. user_pw = "password" # ご使用のカメラ設定に合わせて変更
  12. host = "192.168.0.10" # ご使用のカメラ設定に合わせて変更
  13. winname = "VIDEO" # ウィンドウタイトル
  14. resolution = "1920x1080" # 解像度
  15. framerate = 15 # フレームレート
  16.  
  17. # URL
  18. url = f"http://{user_id}:{user_pw}@{host}/cgi-bin/nphMotionJpeg?Resolution={resolution}&Quality=Standard&Framerate={framerate}"
  19. cap = cv2.VideoCapture(url)
  20.  
  21. while(True):
  22. try:
  23. ret, frame = cap.read()
  24. if ret == True:
  25. # 使っているPC画面に適当に収まる表示大きさへリサイズ
  26. frame2 = cv2.resize(frame, (1280, 760))
  27. cv2.imshow(winname, frame2)
  28.  
  29. # コンソール上で ctrl-c を押すとプログラムを終了する。
  30. # GUI 上で ctrl-c してもプログラムを終了できない。
  31. cv2.waitKey(1) # 注意: imshow() 関数は、この cv2.waitkey() が無いと画面表示してくれない!
  32.  
  33. except KeyboardInterrupt:
  34. print("KeyboardInterrupt")
  35. break
  36.  
  37. cap.release()
  38. cv2.destroyAllWindows()

 

上記プログラムを動かしてみます。実行はこんな感じで行います。

python connect_with_mjpeg_1_1.py

 

Windows環境で複数の Python バージョンをインストールしている場合、下図のような感じで実行バージョンを指定することもできます。
こちらはバージョン 3.10 の Python で実行を指示する例です。

py -3.10 connect_with_mjpeg_1_1.py

 

上記プログラムを動かした様子を動画で示します。

こんなに簡単なプログラムでとても快適な映像表示を実現することができました。

[注意] 上記でも記載しましたが、カメラ側の設定でストリーム(1)~(4)を Off にすることで滑らかな映像表示を実現できました。On のままでもプログラム自体は動作しますが、5fps 程度の映像となりました。

[動画] MJPEG でカメラと接続して映像表示してみた様子

 

本プログラムでは下記ライブラリを使用しています。概要だけ記載します。

ライブラリ名 概要
cv2 画像処理などで有名な OpenCV です。

 

 

2-2. 方法2

下記方法でも MJPEG で接続して映像表示できます。参考記事[2]を参考に作成してみました。

 

[プログラム]

[プログラムソース "connect_with_mjpeg_1_2.py"]

  1. '''
  2. [Abstract]
  3. Try connecting to an i-PRO camera with MJPEG(Motion JPEG).
  4. MJPEG(Motion JPEG) で i-PRO カメラと接続します
  5.  
  6. [Details]
  7. Let's try first.
  8. まずはやってみる
  9.  
  10. [library install]
  11. pip install opencv-python
  12. '''
  13.  
  14. import cv2
  15. import numpy as np
  16. import urllib.request as rq
  17.  
  18. user_id = "user-id" # Change to match your camera setting
  19. user_pw = "password" # Change to match your camera setting
  20. host = "192.168.0.10" # Change to match your camera setting
  21. winname = "VIDEO" # Window title
  22. resolution = "1920x1080" # Resolution
  23. framerate = 15 # Frame rate
  24.  
  25. # URL
  26. url = f"http://{host}/cgi-bin/nphMotionJpeg?Resolution={resolution}&Quality=Standard&Framerate={framerate}"
  27.  
  28. '''
  29. [abstract]
  30. IP カメラと認証処理を行います。
  31.  
  32. [params]
  33. uri: mjpeg 開始 CGI コマンド
  34. user: IPカメラの user-id
  35. passwd: IPカメラの user-password
  36. '''
  37. def set_digest_auth(uri, user, passwd):
  38. pass_mgr = rq.HTTPPasswordMgrWithDefaultRealm()
  39. pass_mgr.add_password(realm=None, uri=uri, user=user, passwd=passwd)
  40. auth_handler = rq.HTTPDigestAuthHandler(pass_mgr)
  41. opener = rq.build_opener(auth_handler)
  42. rq.install_opener(opener)
  43.  
  44.  
  45. set_digest_auth(url, user_id, user_pw)
  46. stream = rq.urlopen(url)
  47.  
  48. bytes = bytes()
  49. while True:
  50. try:
  51. bytes += stream.read(1024)
  52. a = bytes.find(b'\xff\xd8') # SOI スタートマーカ (Start of Image) 0xFFD8
  53. b = bytes.find(b'\xff\xd9') # EOI エンドマーカ (End of Image) 0xFFD9
  54. if a != -1 and b != -1:
  55. jpg = bytes[a:b+2]
  56. bytes = bytes[b+2:]
  57.  
  58. # binary データを ndarray 型へ変換
  59. img_buf = np.frombuffer(jpg, dtype=np.uint8)
  60.  
  61. # バイナリデータをデコードして画像データにする
  62. frame = cv2.imdecode(img_buf, cv2.IMREAD_UNCHANGED)
  63.  
  64. # 使っているPC画面に適当に収まる表示大きさへリサイズ
  65. frame2 = cv2.resize(frame, (1280, 760))
  66.  
  67. # 映像表示
  68. cv2.imshow(winname, frame2)
  69. cv2.waitKey(1) # 注意: imshow() 関数は、この cv2.waitkey() が無いと画面表示してくれない!
  70.  
  71. except KeyboardInterrupt:
  72. # コンソール上で ctrl-c を押すとプログラムを終了する。
  73. # GUI 上で ctrl-c してもプログラムを終了できない。
  74. print("KeyboardInterrupt")
  75. break
  76.  
  77. cv2.destroyAllWindows()

 

本プログラムでは下記ライブラリを使用しています。概要だけ記載します。

ライブラリ名 概要
cv2 画像処理などで有名な OpenCV です。
numpy 数値計算を行うライブラリです。
urllib ネットワーク通信などの処理を行います。

 

 


3.プログラムを改善する

[概要]

前章で作成したプログラムはとても簡単に作成できましたが、いろいろと課題がありました。
とりあえず下記3つの課題を解決してみます。

 

課題1
プログラムを起動するたびにウィンドウ位置が変わる。場合によっては画面外へ表示する場合もあって不便。
適当に画面内に収まる場所に表示してほしい。
⇨ 指定する場所にウィンドウを表示するようにします。

 

課題2
プログラムを終了するのが大変。
ウィンドウ右上の×を押すとウィンドウがいったん消えるが、すぐに再表示されて終われない。
⇨ ウィンドウ右上の✕ボタンでプログラムを終了できるようにします。

 

課題3
同様に、任意のキー入力でプログラムを終了できるとうれしい。
⇨ "z" キー押下でプログラムを終了できるようにします。

 

 

 

[評価環境1]

言語 : Python, 3.10.4
OS : Windows 11 home, 21H2

 

[評価環境2]

言語 : Python, 3.8.10
OS : Ubuntu(WSL), 20.04

 

 

[プログラム]

 

[プログラムソース "connect_with_mjpeg_2.py"]

  1. '''
  2. [abstract]
  3. i-PRO mini (WV-S7130W) と接続して映像を表示してみる
  4.  
  5. [library install]
  6. pip install opencv-python
  7. '''
  8.  
  9. import cv2
  10.  
  11. user_id = "user-id" # ご使用のカメラ設定に合わせて変更
  12. user_pw = "password" # ご使用のカメラ設定に合わせて変更
  13. host = "192.168.0.10" # ご使用のカメラ設定に合わせて変更
  14. winname = "VIDEO" # ウィンドウタイトル
  15. resolution = "1920x1080" # 解像度
  16. framerate = 15 # フレームレート
  17.  
  18. # URL
  19. url = f"http://{user_id}:{user_pw}@{host}/cgi-bin/nphMotionJpeg?Resolution={resolution}&Quality=Standard&Framerate={framerate}"
  20. cap = cv2.VideoCapture(url)
  21.  
  22. #
  23. windowInitialized = False
  24.  
  25. # Exception 定義
  26. BackendError = type('BackendError', (Exception,), {})
  27.  
  28. '''
  29. [Abstract]
  30. 対象ウィンドウが存在するかを確認する。
  31. [Param]
  32. winname : ウィンドウタイトル
  33. [Return]
  34. True : 対象ウィンドウは存在する
  35. False : 対象ウィンドウは存在しない
  36. [Exception]
  37. BackendError : バックエンドで使用している Qt でエラー発生
  38. '''
  39. def IsWindowVisible(winname):
  40. try:
  41. ret = cv2.getWindowProperty(winname, cv2.WND_PROP_VISIBLE)
  42. if ret == -1:
  43. raise BackendError('Use Qt as backend to check whether window is visible or not.')
  44.  
  45. return bool(ret)
  46.  
  47. except cv2.error:
  48. return False
  49.  
  50.  
  51. while(True):
  52. try:
  53. ret, frame = cap.read()
  54. if ret == True:
  55. frame2 = cv2.resize(frame, (1280, 760))
  56. cv2.imshow(winname, frame2)
  57.  
  58. if windowInitialized==False:
  59. # ウィンドウ表示位置が安定しないので、最初の起動時のみ表示場所を指定
  60. cv2.moveWindow(winname, 100, 100)
  61. windowInitialized = True
  62.  
  63. # "z" キーを押されていたら終了
  64. k = cv2.waitKey(1) # 注意: imshow() 関数は、この cv2.waitkey() が無いと画面表示してくれない!
  65. if k == ord("z"):
  66. break
  67. # 指定ウィンドウが無かったら終了
  68. if not IsWindowVisible(winname):
  69. break
  70.  
  71. except KeyboardInterrupt:
  72. print("KeyboardInterrupt")
  73. break
  74.  
  75. cap.release()
  76. cv2.destroyAllWindows()

 

 

 

4. OpenCV で顔検知を加えてみる

MJPEG の実装でも OpenCV による顔検知を実装してみます。

MJPEG 接続では映像情報は受け身です。このため高解像度、高フレームレートの映像を処理したとき、OpenCV の処理が追いつくかが心配な部分です。

 

[評価環境1]

言語 : Python, 3.10.4
OS : Windows 11 home, 21H2

 

[評価環境2]

言語 : Python, 3.8.10
OS : Ubuntu(WSL), 20.04

 

4-1. まずは単純にやってみる

とにかくまずはやってみます。

映像を受信するたびの OpenCV で毎回認識処理を行ってみます。

 

[プログラムソース "connect_with_mjpeg_3_1.py"]

  1. '''
  2. [abstract]
  3. i-PRO mini (WV-S7130W) と接続して映像を表示してみる。
  4. ここでは opencv を使って顔検知を追加してみます。
  5.  
  6. [library install]
  7. pip install opencv-python
  8. '''
  9.  
  10. import cv2
  11.  
  12. user_id = "user-id" # ご使用のカメラ設定に合わせて変更
  13. user_pw = "password" # ご使用のカメラ設定に合わせて変更
  14. host = "192.168.0.10" # ご使用のカメラ設定に合わせて変更
  15. winname = "VIDEO" # ウィンドウタイトル
  16. resolution = "1920x1080" # 解像度
  17. framerate = 15 # フレームレート
  18.  
  19. # URL
  20. url = f"http://{user_id}:{user_pw}@{host}/cgi-bin/nphMotionJpeg?Resolution={resolution}&Quality=Standard&Framerate={framerate}"
  21.  
  22. # Exception 定義
  23. BackendError = type('BackendError', (Exception,), {})
  24.  
  25. '''
  26. [Abstract]
  27. 対象ウィンドウが存在するかを確認する。
  28. [Param]
  29. winname : ウィンドウタイトル
  30. [Return]
  31. True : 対象ウィンドウは存在する
  32. False : 対象ウィンドウは存在しない
  33. [Exception]
  34. BackendError : バックエンドで使用している Qt でエラー発生
  35. '''
  36. def IsWindowVisible(winname):
  37. try:
  38. ret = cv2.getWindowProperty(winname, cv2.WND_PROP_VISIBLE)
  39. if ret == -1:
  40. raise BackendError('Use Qt as backend to check whether window is visible or not.')
  41.  
  42. return bool(ret)
  43.  
  44. except cv2.error:
  45. return False
  46.  
  47. '''
  48. [Abstract]
  49. 顔検知して認識結果を返す
  50. [Param]
  51. cascade : OpenCV の CascadeClassifierオブジェクト
  52. image : OpenCV 形式の画像
  53. [Return]
  54. 認識結果
  55. '''
  56. def DetectFaces(cascade, image):
  57. # 顔検出のためにグレイスケール画像に変換
  58. img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
  59.  
  60. # 顔を画像から検出
  61. face_list = cascade.detectMultiScale(img_gray, minSize=(100, 100))
  62.  
  63. # 検出結果を返す
  64. return face_list
  65.  
  66.  
  67. '''
  68. [Abstract]
  69. 検出された顔枠情報リストを使って、image 上に赤枠を描画する。
  70. [Param]
  71. image : OpenCV 形式の画像
  72. face_list : 検出された顔枠情報リスト
  73. [Return]
  74. 無し
  75. '''
  76. def DrawFaceRectangles(image, face_list):
  77. # 検出した顔の数だけ赤枠を描画
  78. if len(face_list) != 0:
  79. for (pos_x, pos_y, w, h) in face_list:
  80. print(f"pos_x = {pos_x}, pos_y = {pos_y}, w = {w}, h = {h}")
  81. cv2.rectangle(image, (pos_x, pos_y), (pos_x + w, pos_y + h), (0,0,255), thickness=5)
  82.  
  83.  
  84. '''
  85. [Abstract]
  86. main 関数
  87. '''
  88. if __name__ == '__main__':
  89.  
  90. cap = cv2.VideoCapture(url)
  91.  
  92. #
  93. windowInitialized = False
  94.  
  95. # 顔を識別するためのファイル
  96. cascade_file = "haarcascade_frontalface_alt2.xml" # 顔
  97. #cascade_file = "haarcascade_eye.xml" # 目?
  98. #cascade_file = "haarcascade_eye_tree_eyeglasses.xml" # 目?
  99. cascade = cv2.CascadeClassifier(cascade_file)
  100.  
  101. while(True):
  102. try:
  103. ret, frame = cap.read()
  104. if ret == True:
  105. # 顔検知
  106. face_list = DetectFaces(cascade, frame)
  107.  
  108. # 検出した顔枠を描画
  109. DrawFaceRectangles(frame, face_list)
  110.  
  111. # PC画面サイズに合わせて適当にリサイズ後、表示
  112. frame2 = cv2.resize(frame, (1280, 760))
  113. cv2.imshow(winname, frame2)
  114.  
  115. if windowInitialized==False:
  116. # ウィンドウ表示位置が安定しないので、最初の起動時のみ表示場所を指定
  117. cv2.moveWindow(winname, 100, 100)
  118. windowInitialized = True
  119.  
  120. # "z" キーを押されていたら終了
  121. k = cv2.waitKey(1) # 注意: imshow() 関数は、この cv2.waitkey() が無いと画面表示してくれない!
  122. if k == ord("z"):
  123. break
  124. # 指定ウィンドウが無かったら終了
  125. if not IsWindowVisible(winname):
  126. break
  127.  
  128.  
  129. except KeyboardInterrupt:
  130. print("KeyboardInterrupt")
  131. break
  132.  
  133. cap.release()
  134. cv2.destroyAllWindows()

 

結果:

私のゲーミングPCではこれでもそこそこ動作しました。思ったより動く、という感想です。

が、それでもだんだん映像が遅れていきます。
顔検知処理と描画の部分をコメントアウトすると、映像表示の遅れはなくなります。
やはり顔検知処理は PC にとって結構重たい処理のようです。

ちょっと残念。何か改善策を考えてみたいところです。

 

 

4-2. 顔検知部分を別プロセスの処理にしてみる

そこで、顔検知部分を別タスクに分離することで、映像受信と映像デコード処理を止めずにできるだけ顔検知をやってみる、という感じにプログラムを修正してみます。

別タスクというと一般的なプログラムでは "スレッド" というテクニックを使いますが、どうやら CPython と呼ばれるプラットフォームの場合はスレッドは複数の処理を同時に実行してくれないらしいです。そこで、ここでは別プロセスを起動し、キューと呼ばれるIOで情報をやり取りしてみます。

 

[プログラムソース "connect_with_mjpeg_3_2.py"]

  1. '''
  2. [Abstract]
  3. i-PRO mini (WV-S7130W) と接続して映像を表示してみる。
  4. ここでは opencv を使って顔検知を追加してみます。
  5.  
  6. [Library install]
  7. pip install opencv-python
  8.  
  9. [OpenCV]
  10. 下記URLからファイル "haarcascade_frontalface_alt2.xml" を入手すること
  11. https://github.com/opencv/opencv/tree/master/data/haarcascades
  12. '''
  13.  
  14. import cv2
  15. import multiprocessing as mp
  16. from queue import Empty
  17.  
  18.  
  19. user_id = "user-id" # ご使用のカメラ設定に合わせて変更
  20. user_pw = "password" # ご使用のカメラ設定に合わせて変更
  21. host = "192.168.0.10" # ご使用のカメラ設定に合わせて変更
  22. winname = "VIDEO" # ウィンドウタイトル
  23. resolution = "1920x1080" # 解像度
  24. framerate = 15 # フレームレート
  25.  
  26. # URL
  27. url = f"http://{user_id}:{user_pw}@{host}/cgi-bin/nphMotionJpeg?Resolution={resolution}&Quality=Standard&Framerate={framerate}"
  28.  
  29. # 顔を識別するためのファイル
  30. cascade_file = "haarcascade_frontalface_alt2.xml" # 顔
  31. #cascade_file = "haarcascade_eye.xml" # 目?
  32. #cascade_file = "haarcascade_eye_tree_eyeglasses.xml" # 目?
  33. cascade = cv2.CascadeClassifier(cascade_file)
  34.  
  35.  
  36. # Exception 定義
  37. BackendError = type('BackendError', (Exception,), {})
  38.  
  39. '''
  40. [Abstract]
  41. 対象ウィンドウが存在するかを確認する。
  42. [Param]
  43. winname : ウィンドウタイトル
  44. [Return]
  45. True : 対象ウィンドウは存在する
  46. False : 対象ウィンドウは存在しない
  47. [Exception]
  48. BackendError : バックエンドで使用している Qt でエラー発生
  49. '''
  50. def IsWindowVisible(winname):
  51. try:
  52. ret = cv2.getWindowProperty(winname, cv2.WND_PROP_VISIBLE)
  53. if ret == -1:
  54. raise BackendError('Use Qt as backend to check whether window is visible or not.')
  55.  
  56. return bool(ret)
  57.  
  58. except cv2.error:
  59. return False
  60.  
  61.  
  62. '''
  63. [Abstract]
  64. 顔検知して認識結果を返す
  65. [Param]
  66. cascade : OpenCV の CascadeClassifierオブジェクト
  67. image : OpenCV 形式の画像
  68. [Return]
  69. 検出結果
  70. '''
  71. def DetectFaces(cascade, image):
  72. # 顔検出のためにグレイスケール画像に変換
  73. img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
  74.  
  75. # 検出した顔の位置情報を取得
  76. face_list = cascade.detectMultiScale(img_gray, minSize=(100, 100))
  77.  
  78. return face_list
  79.  
  80.  
  81. '''
  82. [Abstract]
  83. 顔検知タスク
  84. [Param]
  85. q1 : [i] 顔検知する画像を保存する Queue
  86. q2 : [o] 顔検知した結果を保存する Queue
  87. [Return]
  88. 無し
  89. '''
  90. # def DetectFacesProcess(cascade, q1, q2):
  91. def DetectFacesProcess(q1, q2):
  92. while True:
  93. try:
  94. image = q1.get(True, 10)
  95.  
  96. # 終了処理: q1.get から取得したものが int で -1 なら終了
  97. if type(image) == int:
  98. if image == -1:
  99. break
  100.  
  101. # 顔検知
  102. face_list = DetectFaces(cascade, image)
  103.  
  104. q2.put(face_list)
  105. except Empty: # timeout of q1.get()
  106. print("Timeout happen.")
  107.  
  108. print("Finish DetectFacesProcess()")
  109.  
  110.  
  111. '''
  112. [Abstract]
  113. 検出された顔枠情報リストを使って、image 上に赤枠を描画する。
  114. [Param]
  115. image : OpenCV 形式の画像
  116. face_list : 検出された顔枠情報リスト
  117. [Return]
  118. 無し
  119. '''
  120. def DrawFaceRectangles(image, face_list):
  121. # 検出した顔の数だけ赤枠を描画
  122. if len(face_list) != 0:
  123. for (pos_x, pos_y, w, h) in face_list:
  124. print(f"pos_x = {pos_x}, pos_y = {pos_y}, w = {w}, h = {h}")
  125. cv2.rectangle(frame, (pos_x, pos_y), (pos_x + w, pos_y + h), (0,0,255), thickness=5)
  126.  
  127.  
  128. '''
  129. [Abstract]
  130. main 関数
  131. '''
  132. if __name__ == '__main__':
  133.  
  134. cap = cv2.VideoCapture(url)
  135.  
  136. #
  137. windowInitialized = False
  138.  
  139. q1 = mp.Queue()
  140. q2 = mp.Queue()
  141.  
  142. # "cannot pickle object" というエラーが出て解決できなかったので、args に cascade を加えるのを断念
  143. # 合わせて cascade をグローバル変数に。
  144. p = mp.Process(target=DetectFacesProcess, args=(q1, q2))
  145. # p = mp.Process(target=DetectFacesProcess, args=(cascade, q1, q2))
  146. p.daemon = True
  147. p.start()
  148.  
  149. init = False
  150.  
  151. while(True):
  152. try:
  153. ret, frame = cap.read()
  154. if ret == True:
  155. # 顔検知
  156. if (q1.qsize() <= 1) and (q2.qsize() <= 1):
  157. q1.put(frame)
  158.  
  159. if q2.qsize() != 0:
  160. face_list = q2.get()
  161. init = True
  162.  
  163. if init == True:
  164. # 検出した顔枠を描画
  165. DrawFaceRectangles(frame, face_list)
  166.  
  167. # PC画面サイズに合わせて適当にリサイズ後、表示
  168. frame2 = cv2.resize(frame, (1280, 760))
  169. cv2.imshow(winname, frame2)
  170.  
  171. if windowInitialized==False:
  172. # ウィンドウ表示位置が安定しないので、最初の起動時のみ表示場所を指定
  173. cv2.moveWindow(winname, 100, 100)
  174. windowInitialized = True
  175.  
  176. # "z" キーを押されていたら終了
  177. k = cv2.waitKey(1) # 注意: imshow() 関数は、この cv2.waitkey() が無いと画面表示してくれない!
  178. if k == ord("z"):
  179. break
  180. # 指定ウィンドウが無かったら終了
  181. if not IsWindowVisible(winname):
  182. break
  183.  
  184.  
  185. except KeyboardInterrupt:
  186. print("KeyboardInterrupt")
  187. break
  188.  
  189. # Terminate process p
  190. q1.put(-1)
  191. # Waiting for process p to finish
  192. p.join()
  193.  
  194. print("Finish main()")
  195. cap.release()
  196. cv2.destroyAllWindows()

 

結果:

期待する動作をしてくれるようになりました。

プロセス起動の引数として cascade を一緒に渡したかったのですが、"cannot pickle object" というエラーを発生して実現できませんでした。残念ながら cascade をグローバル変数へ変更することで問題を回避しています。
対応策がわかったら記事をアップデートしたいと思います。

 

 

[動画] OpenCV で顔検知してみた様子

 

 

5. 連番の JPEG ファイルで保存する

受信した画像を 1 から始まる連番のファイル名 (image_NNNNNN.jpg) で JPEG ファイルとして保存してみます。

 

 

[評価環境]

言語 : Python, 3.10.4
OS : Windows 11 home, 21H2

 

[プログラムソース "connect_with_mjpeg_4.py"]

  1. '''
  2. [Abstract]
  3. MJPEG(Motion JPEG) で i-PRO カメラと接続します
  4.  
  5. [Details]
  6. 受信した JPEG 画像をファイル保存します。
  7. ファイル名の末尾に6ケタの番号を付けて連番で保存します。
  8.  
  9. [Library install]
  10. pip install opencv-python
  11. '''
  12. import cv2
  13. import numpy as np
  14. import os
  15. import urllib.request as rq
  16.  
  17. user_id = "user-id" # ご使用のカメラ設定に合わせて変更
  18. user_pw = "password" # ご使用のカメラ設定に合わせて変更
  19. host = "192.168.0.10" # ご使用のカメラ設定に合わせて変更
  20. winname = "VIDEO" # ウィンドウタイトル
  21. resolution = "1920x1080" # 解像度
  22. framerate = 5 # フレームレート
  23. pathOut = 'image' # 画像ファイル保存フォルダ
  24.  
  25. # URL
  26. url = f"http://{host}/cgi-bin/nphMotionJpeg?Resolution={resolution}&Quality=Standard&Framerate={framerate}"
  27.  
  28. # Exception 定義
  29. BackendError = type('BackendError', (Exception,), {})
  30.  
  31. '''
  32. [Abstract]
  33. 対象ウィンドウが存在するかを確認する。
  34. [Param]
  35. winname : ウィンドウタイトル
  36. [Return]
  37. True : 対象ウィンドウは存在する
  38. False : 対象ウィンドウは存在しない
  39. [Exception]
  40. BackendError : バックエンドで使用している Qt でエラー発生
  41. '''
  42. def IsWindowVisible(winname):
  43. try:
  44. ret = cv2.getWindowProperty(winname, cv2.WND_PROP_VISIBLE)
  45. if ret == -1:
  46. raise BackendError('Use Qt as backend to check whether window is visible or not.')
  47.  
  48. return bool(ret)
  49.  
  50. except cv2.error:
  51. return False
  52.  
  53.  
  54. '''
  55. [abstract]
  56. IP カメラと認証処理を行います。
  57.  
  58. [params]
  59. uri: mjpeg 開始 CGI コマンド
  60. user: IPカメラの user-id
  61. passwd: IPカメラの user-password
  62. '''
  63. def set_digest_auth(uri, user, passwd):
  64. pass_mgr = rq.HTTPPasswordMgrWithDefaultRealm()
  65. pass_mgr.add_password(realm=None, uri=uri, user=user, passwd=passwd)
  66. auth_handler = rq.HTTPDigestAuthHandler(pass_mgr)
  67. opener = rq.build_opener(auth_handler)
  68. rq.install_opener(opener)
  69.  
  70.  
  71. '''
  72. [Abstract]
  73. バイナリデータを指定ファイル名で保存する
  74. [Param]
  75. data : 保存するバイナリデータ
  76. filename : ファイル名
  77. '''
  78. def SaveBinaryData(data, filename):
  79. fout = open(filename, 'wb')
  80. fout.write(data)
  81. fout.close()
  82.  
  83.  
  84. '''
  85. [Abstract]
  86. main 関数
  87. '''
  88. if __name__ == '__main__':
  89.  
  90. windowInitialized = False
  91. count = 0
  92. if not os.path.exists(pathOut):
  93. os.mkdir(pathOut)
  94.  
  95. set_digest_auth(url, user_id, user_pw)
  96. stream = rq.urlopen(url)
  97.  
  98. bytes = bytes()
  99. while True:
  100. bytes += stream.read(1024)
  101. a = bytes.find(b'\xff\xd8') # SOI スタートマーカ (Start of Image) 0xFFD8
  102. b = bytes.find(b'\xff\xd9') # EOI エンドマーカ (End of Image) 0xFFD9
  103. if a != -1 and b != -1:
  104. jpg = bytes[a:b+2]
  105. bytes = bytes[b+2:]
  106.  
  107. # ファイル保存
  108. count += 1
  109. filename = os.path.join(pathOut, 'image_{:06d}.jpg'.format(count))
  110. SaveBinaryData(jpg, filename)
  111.  
  112. # binary データを ndarray 型へ変換
  113. img_buf = np.frombuffer(jpg, dtype=np.uint8)
  114.  
  115. # バイナリデータをデコードして画像データ(OpenCV形式)にする
  116. frame = cv2.imdecode(img_buf, cv2.IMREAD_UNCHANGED)
  117.  
  118. # リサイズ
  119. frame2 = cv2.resize(frame, (1280, 760))
  120.  
  121. # 映像表示
  122. cv2.imshow(winname, frame2)
  123.  
  124. if windowInitialized==False:
  125. # ウィンドウ表示位置が安定しないので、最初の起動時のみ表示場所を指定
  126. cv2.moveWindow(winname, 100, 100)
  127. windowInitialized = True
  128.  
  129. # "z" キーを押されていたら終了
  130. if cv2.waitKey(1) & 0xFF == ord('z'):
  131. break
  132.  
  133. # 指定ウィンドウが無かったら終了
  134. if not IsWindowVisible(winname):
  135. break
  136.  
  137. cv2.destroyAllWindows()

 

 

 

 

6. 映像切断時の再接続処理を追加

ここまでのプログラムは、カメラとの接続が30秒以上切断すると接続が復活しませんでした。
OpenCV の read() 関数のタイムアウトは30秒となっているようです。30秒以内に接続が復活していれば自動的に再接続してくれるのですが、30秒を超えると自動的には復活しません。

"connect_with_rtsp_2.py" を元に再接続処理を追加してこの問題を解決してみたいと思います。

 

ポイント

 

NOTE

 

 

[評価環境]

言語 : Python, 3.10.4
OS : Windows 11 home, 21H2

 

 

[プログラムソース "connect_with_mjpeg_5.py"]

  1. '''
  2. [Abstract]
  3. Try connecting to an i-PRO camera with RTSP.
  4. RTSP で i-PRO カメラと接続してみる。
  5.  
  6. [Details]
  7. Add reconnection when video is disconnected to "connect_with_mjpeg_2.py".
  8. 映像切断時の再接続処理を"connect_with_mjpeg_2.py"へ追加する。
  9.  
  10. [Library install]
  11. pip install opencv-python
  12. '''
  13.  
  14. from nturl2path import url2pathname
  15. import cv2
  16.  
  17. user_id = "user-id" # Change to match your camera setting
  18. user_pw = "password" # Change to match your camera setting
  19. host = "192.168.0.10" # Change to match your camera setting
  20. winname = "VIDEO" # Window title
  21. resolution = "1920x1080" # Resolution
  22. framerate = 15 # Frame rate
  23. url = f"http://{user_id}:{user_pw}@{host}/cgi-bin/nphMotionJpeg?Resolution={resolution}&Quality=Standard&Framerate={framerate}"
  24.  
  25. #
  26. windowInitialized = False
  27.  
  28. # Exception definition.
  29. BackendError = type('BackendError', (Exception,), {})
  30.  
  31. '''
  32. [Abstract]
  33. Check if the target window exists.
  34. 対象ウィンドウが存在するかを確認する。
  35. [Param]
  36. winname : Window title
  37. [Return]
  38. True : exist
  39. 存在する
  40. False : not exist
  41. 存在しない
  42. [Exception]
  43. BackendError :
  44. '''
  45. def IsWindowVisible(winname):
  46. try:
  47. ret = cv2.getWindowProperty(winname, cv2.WND_PROP_VISIBLE)
  48. if ret == -1:
  49. raise BackendError('Use Qt as backend to check whether window is visible or not.')
  50.  
  51. return bool(ret)
  52.  
  53. except cv2.error:
  54. return False
  55.  
  56.  
  57. '''
  58. [Abstract]
  59. main function.
  60. '''
  61. if __name__ == '__main__':
  62.  
  63. cap = cv2.VideoCapture(url)
  64.  
  65. while True:
  66. try:
  67. ret, frame = cap.read()
  68. if ret == True:
  69. # Please modify the value to fit your PC screen size.
  70. frame2 = cv2.resize(frame, (1280, 760))
  71. # Display video.
  72. cv2.imshow(winname, frame2)
  73.  
  74. if windowInitialized==False:
  75. # Specify window position only once at startup.
  76. cv2.moveWindow(winname, 100, 100)
  77. windowInitialized = True
  78. else:
  79. print("cap.read() return False.")
  80. # The timeout period seems to be 30 seconds.
  81. # And there seems to be no API to change the timeout value.
  82.  
  83. # Reconnect
  84. cap.release()
  85. cap = cv2.VideoCapture(url)
  86.  
  87. # Press the "z" key to finish.
  88. k = cv2.waitKey(1)
  89. if k == ord("z"):
  90. break
  91. # Exit the program if there is no specified window.
  92. if not IsWindowVisible(winname):
  93. break
  94. except KeyboardInterrupt:
  95. # Press'[ctrl] + [c]' on the console to exit the program.
  96. print("KeyboardInterrupt")
  97. break
  98.  
  99. cap.release()
  100. cv2.destroyAllWindows()

 

 

 

 

7. GUIで映像表示してみる(tkinter)

ここまでのプログラムは全てOpenCVが作成するウィンドウ表示でした。
ここでは独自の GUI を作成してここに映像表示する例を示します。

GUI 表示の実現方法もいろいろありますが、ここでは Python 標準の tkinter を使用してみます。

 

tkinter のインストール方法は環境により異なるようです。各人の環境にあった方法をインターネットで調べて実施してください。

 

ポイント

 

記事「RTSPで画像を取得する」中で既に GUI 版を作成済みなので、プログラム "connect_with_rtsp_6_3.py" をベースに変更箇所のみをわかるように以下で記載します。ほとんど同じ内容で実現できます。

 

[評価環境]

言語 : Python, 3.10.4
  Tcl/Tk, 8.6
OS : Windows 11 home, 21H2

 

[プログラムソース "connect_with_mjpeg_6.py"]

  1. '''
  2. [Abstract]
  3. Try connecting to an i-PRO camera with MJPEG.
  4. MJPEG で i-PRO カメラと接続してみる。
  5.  
  6. [Details]
  7. Display the video with GUI using tkinter.
  8. Add menus and buttons to make it look like a GUI app.
  9. tkinter を使ったGUIで映像を表示します。
  10. メニューとボタンを追加してGUIアプリらしくします。
  11.  
  12. BGR → RGB
  13. numpy.ndarray → PIL.Image → ImageTk.PhotoImage
  14. (1) BGR → RGB
  15. (2) numpy.ndarray → PIL.Image
  16. (3) PIL.Image → ImageTk.PhotoImage
  17.  
  18. [Library install]
  19. pip install opencv-python
  20. '''
  21.  
  22. import cv2
  23. import time
  24. import tkinter as tk
  25. from tkinter import messagebox
  26. from PIL import Image, ImageTk, ImageOps
  27. import multiprocessing as mp
  28.  
  29.  
  30. user_id = "user-id" # Change to match your camera setting
  31. user_pw = "password" # Change to match your camera setting
  32. host = "192.168.0.10" # Change to match your camera setting
  33. winname = "VIDEO" # Window title
  34. resolution = "1920x1080" # Resolution
  35. framerate = 15 # Frame rate
  36. url = f"http://{user_id}:{user_pw}@{host}/cgi-bin/nphMotionJpeg?Resolution={resolution}&Quality=Standard&Framerate={framerate}"
  37.  
  38.  
  39. class Application(tk.Frame):
  40. def __init__(self, master = None):
  41. super().__init__(master)
  42. self.pack()
  43.  
  44. # Window settings.
  45. self.master.title("Display i-PRO camera with tkinter") # Window title
  46. self.master.geometry("800x600+100+100") # Window size, position
  47.  
  48. # Event registration for window termination.
  49. self.master.protocol("WM_DELETE_WINDOW", self.on_closing_window)
  50.  
  51. # Create menu.
  52. menubar = tk.Menu(self.master)
  53. self.master.configure(menu=menubar)
  54. filemenu = tk.Menu(menubar)
  55. menubar.add_cascade(label='File', menu=filemenu)
  56. filemenu.add_command(label='Quit', command = self.on_closing_window)
  57.  
  58. # Create button_frame
  59. self.button_frame = tk.Frame(self.master, padx=10, pady=10, relief=tk.RAISED, bd=2)
  60. self.button_frame.pack(side = tk.BOTTOM, fill=tk.X)
  61.  
  62. # Create quit_button
  63. self.quit_button = tk.Button(self.button_frame, text='Quit', width=10, command = self.on_closing_window)
  64. self.quit_button.pack(side=tk.RIGHT)
  65. # Create canvas.
  66. self.canvas = tk.Canvas(self.master)
  67.  
  68. # Add mouse click event to canvas.
  69. self.canvas.bind('<Button-1>', self.canvas_click)
  70.  
  71. # Place canvas.
  72. self.canvas.pack(expand = True, fill = tk.BOTH)
  73.  
  74. # Create image receiving process and queue
  75. self.imageQueue = mp.Queue()
  76. self.request = mp.Value('i', 0) # -1 : Exit ReceiveImageProcess.
  77. # 0 : Normal.
  78. # 1 : Connect camera.
  79. # 2 : Release camera.
  80. self.p = mp.Process(target=ReceiveImageProcess, args=(self.imageQueue, self.request))
  81. self.p.start()
  82.  
  83. # Raise a video display event (disp_image) after 500m
  84. self.disp_id = self.after(500, self.disp_image)
  85.  
  86. def on_closing_window(self):
  87. ''' Window closing event. '''
  88.  
  89. if messagebox.askokcancel("QUIT", "Do you want to quit?"):
  90. # Request terminate process self.p.
  91. self.request.value = -1
  92.  
  93. # Waiting for process p to finish
  94. time.sleep(1)
  95.  
  96. # Flash buffer.
  97. # The program cannot complete p.join() unless the imageQueue is emptied.
  98. for i in range(self.imageQueue.qsize()):
  99. pil_image = self.imageQueue.get()
  100.  
  101. # Wait for process p to be terminated.
  102. self.p.join()
  103. self.master.destroy()
  104. print("Finish Application.")
  105.  
  106. def canvas_click(self, event):
  107. ''' Event handling with mouse clicks on canvas '''
  108.  
  109. if self.disp_id is None:
  110. # Connect camera.
  111. self.request.value = 1
  112. # Display image.
  113. self.disp_image()
  114.  
  115. else:
  116. # Release camera.
  117. self.request.value = 2
  118. # Cancel scheduling
  119. self.after_cancel(self.disp_id)
  120. self.disp_id = None
  121.  
  122. def disp_image(self):
  123. ''' Display image on Canvas '''
  124.  
  125. # If there is data in the imageQueue, the program receives the data and displays the video.
  126. num = self.imageQueue.qsize()
  127. if num > 0:
  128. if (num > 5):
  129. num -= 1
  130. for i in range(num):
  131. cv_image = self.imageQueue.get()
  132.  
  133. # (2) Convert image from ndarray to PIL.Image.
  134. pil_image = Image.fromarray(cv_image)
  135.  
  136. # Get canvas size.
  137. canvas_width = self.canvas.winfo_width()
  138. canvas_height = self.canvas.winfo_height()
  139.  
  140. # Resize the image to the size of the canvas without changing the aspect ratio.
  141. # アスペクトを維持したまま画像を Canvas と同じサイズにリサイズ
  142. pil_image = ImageOps.pad(pil_image, (canvas_width, canvas_height))
  143.  
  144. # (3) Convert image from PIL.Image to PhotoImage
  145. # PIL.Image から PhotoImage へ変換する
  146. self.photo_image = ImageTk.PhotoImage(image=pil_image)
  147.  
  148. # Display image on the canvas.
  149. self.canvas.create_image(
  150. canvas_width / 2, # Image display position (center of the canvas)
  151. canvas_height / 2,
  152. image=self.photo_image # image data
  153. )
  154. else:
  155. pass
  156.  
  157. # Raise a video display event (disp_image) after 1ms.
  158. self.disp_id = self.after(1, self.disp_image)
  159.  
  160.  
  161. def ReceiveImageProcess(imageQueue, request):
  162. '''
  163. Receive Image Process.
  164.  
  165. Args:
  166. imageQueue [o] This process stores the received image data in the imageQueue.
  167. request [i] Shared memory for receiving requests from the main process.
  168. -1: Terminate process.
  169. 0: Nothing.
  170. 1: Connect camera.
  171. 2: Release camera connection.
  172. Returns:
  173. None
  174. Raises
  175. None
  176. '''
  177.  
  178. # Connect camera.
  179. cap = cv2.VideoCapture(url)
  180.  
  181. while True:
  182. if cap != None:
  183. # Get frame.
  184. ret, frame = cap.read()
  185.  
  186. if ret == True:
  187. # (1) Convert image from BGR to RGB.
  188. cv_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
  189.  
  190. if imageQueue.qsize() < 10:
  191. imageQueue.put(cv_image)
  192.  
  193. else:
  194. print("cap.read() return False.")
  195. # The timeout period seems to be 30 seconds.
  196. # And there seems to be no API to change the timeout value.
  197. time.sleep(1)
  198.  
  199. # Reconnect
  200. cap.release()
  201. cap = cv2.VideoCapture(url)
  202. else:
  203. time.sleep(0.1)
  204. # Check process termination request.
  205. if request.value == -1:
  206. # Terminate process.
  207. cap.release()
  208. request.value = 0
  209. break
  210.  
  211. # Check connect request.
  212. if request.value == 1:
  213. cap = cv2.VideoCapture(url)
  214. request.value = 0
  215.  
  216. # Check release request.
  217. if request.value == 2:
  218. cap.release()
  219. cap = None
  220. request.value = 0
  221.  
  222. print("Terminate SaveImageProcess().")
  223.  
  224.  
  225. if __name__ == "__main__":
  226. root = tk.Tk()
  227. app = Application(master = root)
  228. app.mainloop()

 

 

 

ライセンス

本ページの情報は、特記無い限り下記 MIT ライセンスで提供されます。

MIT License

Copyright (c) 2022 Kinoshita Hidetoshi

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

 

 

参考

 

 


 

記載

2022-05-14 - 「7. GUIで映像表示してみる(tkinter)」を追加
  - 「6. 映像切断時の再接続処理を追加 」を追加
2022-04-23 - 「5. 連番の JPEG ファイルで保存する」を追加
2022-04-17 - ライセンスを追加
2022-04-16 - 新規作成

 

Programming Items トップページ

プライバシーポリシー