Python OpenCV 等待按鍵事件 cv2.waitKey

本篇介紹如何在 Python OpenCV 中使用 cv2.waitKey 等待按鍵事件,進一步的處理鍵盤事件,例如像離開程式這樣的事件處理。

使用範例

如果遇到 ImportError: No module named 'cv2' 這個錯誤訊息的話,請安裝 python 的 OpenCV 模組,參考這篇安裝吧!。

在影像處理中經常需要取得鍵盤輸入事件,之後在更進一步地去處理這些按鍵對應的程式邏輯,
在 python opencv 中 要補捉鍵盤事件的話可以使用 cv2.waitKey(),它會在給定的時間內監聽鍵盤事件,
給訂的時間到了 cv2.waitKey() 回傳按下按鍵的數字,沒有按鍵按下的話會回傳-1,
那我們以播放影片為例,對播放影片功能不熟悉的可以回去看我之前的文章
這邊以使用者會按下 Ese 鍵與 Enter 鍵的情況為例,

python3-opencv-waitKey.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import cv2

cap = cv2.VideoCapture('vtest.avi')
play = 1
while cap.isOpened():
if (play):
ret, frame = cap.read()
# if frame is read correctly ret is True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
cv2.imshow('frame', frame)
key = cv2.waitKey(90)
if key == ord('q') or key == 27: # Esc
print('break')
break
elif key == 13: # Enter
print('play / pause')
play = play ^ 1
else:
print(key)
cap.release()
cv2.destroyAllWindows()

另外其他常見的按鍵有下列,按鍵值為我在Ubuntu下測試的結果:
Esc: 27
Enter: 13
Up: 82
Down: 84
Left: 81
Right: 83
Space: 32
Backspace: 8
Delete: 255
Home: 80
End: 87
PageUp: 85
PageDown: 86
PrintScreen: 在 ubuntu 中會被系統攔截去

程式啟動後可以在影片播放中按下 Enter 鍵,就會暫停播放,再按一次 Enter 鍵就會繼續播放,
下圖就是暫停播放的樣子,

好啦!這篇教學就到這篇囉~
下一篇教學是如何顯示camera攝影機串流影像

參考
键盘事件监听 - Python-OpenCV基础入门
http://www.1zlab.com/wiki/python-opencv-tutorial/opencv-waitkey-keyboards/
python - Using other keys for the waitKey() function of opencv - Stack Overflow
https://stackoverflow.com/questions/14494101/using-other-keys-for-the-waitkey-function-of-opencv
windows - How to detect ESCape keypress in Python? - Stack Overflow
https://stackoverflow.com/questions/5137238/how-to-detect-escape-keypress-in-python

其它相關文章推薦
Python OpenCV 彩色轉灰階(RGB/BGR to GRAY)
Python OpenCV 彩色轉HSV(RGB/BGR to HSV)
Python OpenCV 彩色轉YCbCr(RGB/BGR to YCbCr)
Python OpenCV 灰階轉彩色(Gray to RGB/BGR)
Python OpenCV 影像二值化 Image Thresholding
Python OpenCV 影像平滑模糊化 blur
Python OpenCV 影像邊緣偵測 Canny Edge Detection
Python OpenCV 垂直vconcat 和水平hconcat 影像拼接
Python OpenCV resize 圖片縮放
Python 新手入門教學懶人包
小專案 Python OpenCV 圖片轉字元圖畫

Python Flask render_template 模板

本篇 ShengYu 介紹 Python Flask render_template 模板,之前已經學習過基本的 Flask 後,利用 return string 將內容渲染在網頁上,但是實際上很少將整個 HTML 寫在 string 裡,這樣 Python 原始碼將變得很龐大難以維護,本篇介紹 render_template 來做樣板的渲染。接下來就來學習用模板來渲染網頁。

Flask render_template

先以一個簡單的 Flask 範例來學習模板,建立好一個 index.html 並放置在 templates 資料夾下,

flask-templete/templates/index.html
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Flask</title>
</head>
<body>
<h1>Flask</h1>
<p>Hello World</p>
</body>
</html>

專案資料夾的目錄如下,

1
2
3
4
flask-templete
├── main.py
└── templates
└── index.html

在 Python 中產生 HTML 是件麻煩的事,因為要處理 HTML Escape 轉義的事情,但 Flask 使用 Jinja2 做為模板引擎自動地處理了這些事情,Flask 是使用 render_template() 函式來渲染模板,在使用前先記得 from flask import render_template 匯入模組,新增一個 @app.route('/') 裝飾器綁訂 index() 函式然後使用 render_template('index.html') 來渲染這個 index.html,當有 / 請求時,Flask 會先到 templates 資料夾去找相對應的 html,本範例為 index.html,如果找不到的話會出現 jinja2.exceptions.TemplateNotFound 的錯誤訊息字樣,如果有開啟 debug 模式 debug=True 可以在瀏覽器上看到錯誤訊息。

flask-templete/main.py
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
return render_template('index.html')

if __name__ == "__main__":
app.run(host='0.0.0.0', debug=True)

瀏覽器開啟的畫面如下,

Flask render_template + 傳遞參數

在之前我們介紹過,要將網址 URL 當成參數使用的話可以透過下面這種寫法,在 route() 裡的變數要用左右箭號 <> 來包住,預設為字串,Flask 總共的支援的類型有 str、int、float、path,分別的寫法為 @app.route('/<str:xxx>')@app.route('/<int:xxx>')@app.route('/<float:xxx>')@app.route('/<path:xxx>'),以下示範取得 URL 的 /user/<username> 並且將 username 變數回應回去。

python3-flask-url-args.py
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask

app = Flask(__name__)

@app.route('/user/<username>')
def index(username):
return '<p>Hi ' + username + '</p>'

if __name__ == "__main__":
app.run(debug=True)

瀏覽器開啟的畫面如下,

但如果想從 render_template() 傳遞參數進 index.html 的話,就用在 index.html 用特殊的語法表示參數,語法為雙大括號包住參數名稱,如下例 index.html 的 username

flask-templete2/templates/index.html
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flask</title>
</head>
<body>
<h1>Flask</h1>
<p>Hi {{ username }}</p>
</body>
</html>

瀏覽 http://127.0.0.1:5000/ 會將 username 變數初始化成 shengyu 再傳給 render_template()

flask-templete2/main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
username = 'shengyu'
return render_template('index.html', username=username)

if __name__ == "__main__":
app.run(debug=True)

瀏覽器開啟的畫面如下,

那是不是可以結合前面的例子將網址 URL 當成參數傳遞到 render_template 呢?,當然可以!以下面例子為例,
瀏覽 http://127.0.0.1:5000/user/shengyu 將導向給 index(username) 函式,之後在函式內將 username 傳給 render_template()

flask-templete3/main.py
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/user/<username>')
def index(username):
return render_template('index.html', username=username)

if __name__ == "__main__":
app.run(debug=True)

結果可以看到 Hi shengyu

但是這樣的話會發現首頁 http://127.0.0.1:5000/ 無法存取,綜合兩種方式變成以下例子,
index() 的參數裡指定 user 等於 unknown 這樣兩種網址都可以存取了,

flask-templete4/main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
@app.route('/user/<username>')
def index(username='unknown'):
return render_template('index.html', username=username)

if __name__ == "__main__":
app.run(debug=True)

瀏覽器開啟的畫面如下,

樣板裡的條件判斷

這邊將介紹樣板裡的條件判斷寫法,條件判斷寫法搭配組合總共有 if… endif、if… else… endif、if..elif… endif 這幾種,以下示範 if… else… endif 條件判斷的寫法,

flask-templete5/templates/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flask</title>
</head>
<body>
<h1>Flask</h1>
{% if username %}
<p>Hi {{ username }}</p>
{% else %}
<p>Hello World</p>
{% endif %}
</body>
</html>

index(username='unknown') username 的初始值改成 None index(username=None)

flask-templete5/main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
@app.route('/user/<username>')
def index(username=None):
return render_template('index.html', username=username)

if __name__ == "__main__":
app.run(debug=True)

樣板裡的迴圈

這邊將介紹樣板裡的迴圈寫法,

flask-templete6/templates/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flask</title>
</head>
<body>
<h1>Flask</h1>
{% for i in range(0, number) %}
<p>Hello World, n = {{ i }}</p>
{% endfor %}
</body>
</html>

要在 @app.route() 裡聲明 num 是 int,否則預設會視為字串,

flask-templete6/main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
@app.route('/<int:num>')
def index(num=1):
return render_template('index.html', number=num)

if __name__ == "__main__":
app.run(debug=True)

瀏覽器開啟 http://127.0.0.1:5000/5 的畫面如下,

其它相關文章推薦
下一篇要介紹

Python Flask 建立簡單的網頁

本篇介紹如何用 Python 與 Flask 來建立一個簡單的網站,要學習建立一個網頁最間單的方式就是從Hello World開始,接下來介紹怎麼用Flask建立一個簡單的Hello World網頁。

安裝 Flask 套件

如果是第一次寫 Flask 程式,肯定沒有安裝過 Flask,
輸入下列指令來安裝 Flask 吧!

1
pip3 install Flask

或到以下相關網站下載安裝包
PyPI 的 Flask:https://pypi.org/project/Flask/

簡單的 Flask 網頁伺服器

以下為最簡單的 Flask 網頁伺服器的程式碼,注意 Python 檔案名稱不要使用 flask.py 避免跟 Flask 模組衝突,
一開始要匯入 flask 模組 import flaskfrom flask import Flask,接著建立一個app實體
__name__ 為 python 內建的關鍵字,代表目前執行的模組,
index() 函式表示網頁首頁時,這邊會回傳一個 Hello World 的字串,index() 函式名稱可以隨便取,
@app.route('/')@ 為裝飾器(Decorator)的意思,它是以函式為基礎,提供附加的功能。
@app.route('/') 的意思為定義一個路由,綁定後面的 index() 函式來處理 / URL 的邏輯,表示使用者用瀏覽器連線到網站 / 根目錄路徑時會被導到 index() 這個函式,

所以這個程式的工作是接收到使用者連線到到網站根目錄路徑時,
被 Flask 導到 index() 這個函式作處理,最後回傳 Hello World 的字串,

python3-flask-hellowrold.py
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
return "Hello World"

if __name__ == '__main__':
app.run()

啟動程式後會看見下列的輸出,表示網頁伺服器已經就緒了,
接下來就可以直接在瀏覽器,開啟http://127.0.0.1:5000/這個URL網址,

URL:全名為Uniform Resource Locator,中文為網址,是網站在網際網路上的門牌號碼,我們通常利用瀏覽器輸入URL網址藉此對網頁伺服器發出請求資料。

1
2
3
4
5
6
7
$ ./python3-flask-hellowrold.py 
* Serving Flask app "python3-flask-hellowrold" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

瀏覽器打開http://127.0.0.1:5000/這個URL網址後可以看見直接顯示一個Hello World的頁面,

或者在 console 輸入 curl 指令

1
$ curl http://127.0.0.1:5000/

而console的輸出會多了一行連線紀錄,如下所示,

1
127.0.0.1 - - [02/Nov/2020 21:03:39] "GET / HTTP/1.1" 200 -

這是因為瀏覽器發送了一個HTTP請求到127.0.0.1:5000這個地址去,也就是我們這個python程式,而我們這個python程式收到這個請求後最後回傳了Hello World這個字串,而瀏覽器將這個字串顯示出來

只要這個程式沒有關閉,這個網頁伺服器就會一直開著,一直服務每個HTTP連線進來的請求,並回傳Hello World字串,直到按下CTRL+C程式才會結束,
當這個網頁伺服器結束後再用瀏覽器去連,就會發現這個網站掛掉了連不上這個網站,意味著要讓這個網站一直運行的話,這個程式就要一直開著不能被關閉,
另外這個網站只能限制本機端使用者才能存取這個網站,換句話說你用別台電腦連這台電腦的IP加上5000的網頁是存取不到的,原因在於127.0.0.1這個IP只限制本機端(localhost)存取,若要讓別台電腦或外網存取的話,要再修改一下,這部分之後會提到,

開啟 Debug mode 偵錯模式

在剛剛輸出中發現Debug mode: off,如果要開啟Debug mode的話,
app = Flask(__name__)之後加入下列程式碼即可開啟Debug mode,

1
app.config["DEBUG"] = True

這麼輸出會變成

1
2
3
4
5
6
7
8
9
* Serving Flask app "python3-flask-hellowrold" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 259-438-846

開啟 Debug mode 的話,程式碼有更動的話 server 會自動 reload,另外有任何錯誤的話都能在瀏覽器上看到錯誤的訊息以及 Traceback 等等的資訊。

更改網頁回傳的字串

如果要更改Hello World這個字串,就直接修改原本的index()函式的回傳字串存檔後再重新執行,
可以改成下列這樣子試試,看看結果會變成怎樣,

python3-flask-hellowrold2.py
1
2
3
@app.route('/')
def index():
return "<h1>This is index page!</h1>"

接著用瀏覽器重新整理或重新打開該網址的話就會發現字串改變了,

新增頁面

那如果我想要存取http://127.0.0.1:5000/about這個頁面也想要顯示個不同的字串,那要如何修改程式呢?

在修改程式之前我們試著先去存取這個網頁,會發現瀏覽器顯示Not Found字串,代表http://127.0.0.1:5000/about這個網址目前是沒有處理的,

好,那接下來我們開始來處理http://127.0.0.1:5000/about這個網址,

python3-flask-hellowrold3.py
1
2
3
@app.route('/about')
def about():
return "This is about page"

我們在原本的程式加上about函式,並且在函式修飾器上添加路徑,
表示連線到http://127.0.0.1:5000/about這個網址請轉到about()函式來處理,最後結果如下所示,

透過 URL 來傳參數

接著我們要透過 URL 來傳遞參數,以下範例示範路由傳遞參數的作法來傳遞 username,

python3-flask-url-args.py
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask

app = Flask(__name__)

@app.route('/user/<username>')
def index(username):
return '<p>Hi ' + username + '</p>'

if __name__ == "__main__":
app.run(debug=True)

結果可以看到 Hi shengyu

關於 URL 來傳遞參數更詳細的用法可以參考這篇

額外補充

在Console的輸出上有段警告文字,

1
2
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.

意思是如果要部署這個網站的話,請用產品等級的Python WSGI server,這裡官方建議用Waitress,有興趣的人可以參考下面連結,
Deploy to Production — Flask Documentation (1.1.x)
https://flask.palletsprojects.com/en/1.1.x/tutorial/deploy/

參考
第 20 天:Flask:基礎網頁製作 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
https://ithelp.ithome.com.tw/articles/10222132
Python Web Flask 實戰開發教學 - 簡介與環境建置
https://blog.techbridge.cc/2017/06/03/python-web-flask101-tutorial-introduction-and-environment-setup/
輕鬆學習 Python:使用 Flask 創建 Web API. 如何以 Web API 分享資料
https://medium.com/datainpoint/flask-web-api-quickstart-3b13d96cccc2
小狐狸事務所: Python 學習筆記 : 用 Flask 架站 (一) 請求處理
http://yhhuang1966.blogspot.com/2019/08/python-flask.html
python - Simple Flask Application Not Working - Stack Overflow
https://stackoverflow.com/questions/46401131/simple-flask-application-not-working

其它相關文章推薦
下一篇要介紹 flask render_template 樣板的用法

Python 動手寫個批次縮圖工具

本篇 ShengYu 將介紹如何使用 Python 寫個批次縮圖的小工具,接下來介紹怎麼使用 Python 的 PIL 模組來完成這個小專案吧。

安裝 PIL

基本上新版本的 Python 應該都有內建 PIL,如果還未安裝 PIL 的話請參考這篇

用法

batch-resize-photo-tool.py 使用方式為 ./batch-resize-photo-tool.py <input-dir> <output-dir>,例如:

1
$ ./batch-resize-photo-tool.py ./input/ ./output/

程式碼

以下為部份原始碼,會偵測圖片 exif 中的 Orientation, 並且將它轉正,這樣大大的幫助我們去每張手動去旋轉,

batch-resize-photo-tool.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import os
import shutil
from PIL import Image

if __name__ == '__main__':
if len(sys.argv) <= 2:
print('no argument')
print(sys.argv[0] + ' <input-dir> <output-dir>')
sys.exit()
ext = ['jpg','JPG','jpeg','png']

indir = sys.argv[1]
outdir = sys.argv[2]

files = os.listdir(indir)

for file in files:
if file.split('.')[-1] in ext:
print('input=' + indir + file)
resize_by_ratio(indir+file, outdir+file, 20.834)

另外還有個功能是可以指定寬度優先與高度優先的功能,下次有機會我再介紹。

其它相關文章推薦
Python 旋轉圖片 rotate
Python 縮放圖片 resize
Python 裁切裁剪圖片 crop
Python 圖片模糊化 blur
Python 在圖片上繪製文字

Python OpenCV 使用 PyQt5 顯示影像圖片

本篇介紹如何使用 Python 與 PyQt5 來讀取並顯示 OpenCV 的影像圖片,在本篇將會學習到如何使用 PyQt5 寫一個簡單的視窗程式,
並且添加按鈕事件,把圖片影像顯示在視窗上,

本篇會學習到的內容有以下幾點,

  • 建立一個 PyQt5 的視窗程式
  • 新增 PyQt5 的按鈕事件
  • 將 OpenCV 的影像顯示在 PyQt5 視窗程式上
  • 新增 PyQt5 的滑鼠事件

本篇需要的基礎知識有以下幾點,

以下為我的環境,

  • Ubuntu 16.04
  • Python 3.5.2

Ubuntu 用 apt 安裝 PyQt5,

1
sudo apt-get install python3-pyqt5

接下來開始進入主題吧!

PyQt5 顯示 OpenCV 影像圖片的視窗程式

這邊將開始學習怎麼建立一個 PyQt5 的視窗程式,並且在視窗顯示的同時也將影像圖片也顯示出來,
首先,建立一個 PyQt5 的 Dialog 視窗程式,這部份在我之前的文章介紹過,所以先跳過不介紹,
之後在程式啟動時,使用 cv2.imread() 將影像圖片讀取進來,
再把 OpenCV 影像格式轉成 PyQt5 可以顯示的格式,也就是轉成 QImage RGB888,
最後再把轉換後的影像顯示在 QLabel 上,

python3-opencv-image-pyqt.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import cv2
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QApplication, QDialog, QGridLayout, QLabel

class MyDialog(QDialog):
def __init__(self):
super().__init__()
self.initUI()
self.showImage()

def initUI(self):
self.resize(400, 300)
self.label = QLabel()

layout = QGridLayout(self)
layout.addWidget(self.label, 0, 0, 4, 4)

def showImage(self):
self.img = cv2.imread('lena.jpg', -1)
if self.img.size == 1:
return
height, width, channel = self.img.shape
bytesPerline = 3 * width
self.qImg = QImage(self.img.data, width, height, bytesPerline, QImage.Format_RGB888).rgbSwapped()
self.label.setPixmap(QPixmap.fromImage(self.qImg))

if __name__ == '__main__':
a = QApplication(sys.argv)
dialog = MyDialog()
dialog.show()
sys.exit(a.exec_())

將這個程式執行起來後,就可以看見 lena.jpg 這張圖片被讀取進來並顯示在視窗上啦,結果如下圖所示,

PyQt5 顯示 OpenCV 影像圖片的視窗程式(加入讀取圖片的按鈕事件)

上個範例已經學會最基本的顯示圖片的視窗程式囉!但是能不能讓使用者用開啟檔案對話框去選擇要顯示的圖片呢?

好!那接下來就來學習看看這功能怎麼作,以上個範例作為延伸來加入按鈕事件,讓使用者按下該按鈕後彈出開啟檔案對話框,
讓使用者去要顯示的影像圖片,如果選取的圖片路徑確定有效後,
便將該圖片利用 cv2.imread() 讀取進來,最後再顯示出來,
來看看程式怎麼寫吧!

python3-opencv-image-pyqt-2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import cv2
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog, QGridLayout, QLabel, QPushButton

class MyDialog(QDialog):
def __init__(self):
super().__init__()
self.initUI()

def initUI(self):
self.resize(400, 300)
self.label = QLabel()
self.btnOpen = QPushButton('Open Image', self)

layout = QGridLayout(self)
layout.addWidget(self.label, 0, 0, 4, 4)
layout.addWidget(self.btnOpen, 4, 0, 1, 1)

self.btnOpen.clicked.connect(self.openSlot)

def openSlot(self):
filename, _ = QFileDialog.getOpenFileName(self, 'Open Image', 'Image', '*.png *.jpg *.bmp')
if filename is '':
return
self.img = cv2.imread(filename, -1)
if self.img.size == 1:
return
self.showImage()

def showImage(self):
height, width, channel = self.img.shape
bytesPerline = 3 * width
self.qImg = QImage(self.img.data, width, height, bytesPerline, QImage.Format_RGB888).rgbSwapped()
self.label.setPixmap(QPixmap.fromImage(self.qImg))

if __name__ == '__main__':
a = QApplication(sys.argv)
dialog = MyDialog()
dialog.show()
sys.exit(a.exec_())

把程式執行起來,這次我們選別的 fruits.jpg 圖片來顯示看看,結果如下圖所示,

PyQt5 顯示 OpenCV 影像圖片的視窗程式(加入影像處理的按鈕事件)

到目前為止已經學會怎麼用開啟檔案對話框來選取想要顯示的圖片了,
那是不是可以再加一個按鈕事件來作一些影像處理呢?

我們就以影像模糊化這個功能為例,對於 OpenCV 圖片模糊化不熟悉的可以回去看我之前的文章
這邊就直接新增好 processSlot 按鈕事件後,並在按鈕事件裡對影像使用 cv2.blur() 模糊化處理,
之後再將處理後的影像更新在視窗上,這樣就完成了囉!
來看看程式怎麼寫吧!

python3-opencv-image-pyqt-3.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import cv2
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog, QGridLayout, QLabel, QPushButton

class MyDialog(QDialog):
def __init__(self):
super().__init__()
self.initUI()

def initUI(self):
self.resize(400, 300)
self.label = QLabel()
self.btnOpen = QPushButton('Open Image', self)
self.btnProcess = QPushButton('Blur Image', self)
self.btnSave = QPushButton('Save Image', self)
self.btnSave.setEnabled(False)

layout = QGridLayout(self)
layout.addWidget(self.label, 0, 0, 4, 4)
layout.addWidget(self.btnOpen, 4, 0, 1, 1)
layout.addWidget(self.btnProcess, 4, 1, 1, 1)
layout.addWidget(self.btnSave, 4, 2, 1, 1)

self.btnOpen.clicked.connect(self.openSlot)
self.btnProcess.clicked.connect(self.processSlot)
self.btnSave.clicked.connect(self.saveSlot)

def openSlot(self):
filename, _ = QFileDialog.getOpenFileName(self, 'Open Image', 'Image', '*.png *.jpg *.bmp')
if filename is '':
return
self.img = cv2.imread(filename, -1)
if self.img.size == 1:
return
self.showImage()
self.btnSave.setEnabled(True)

def saveSlot(self):
filename, _ = QFileDialog.getSaveFileName(self, 'Save Image', 'Image', '*.png *.jpg *.bmp')
if filename is '':
return
cv2.imwrite(filename, self.img)

def processSlot(self):
self.img = cv2.blur(self.img, (7, 7))
self.showImage()

def showImage(self):
height, width, channel = self.img.shape
bytesPerline = 3 * width
self.qImg = QImage(self.img.data, width, height, bytesPerline, QImage.Format_RGB888).rgbSwapped()
self.label.setPixmap(QPixmap.fromImage(self.qImg))

if __name__ == '__main__':
a = QApplication(sys.argv)
dialog = MyDialog()
dialog.show()
sys.exit(a.exec_())

把程式執行起來,這次我們一樣選擇水果的圖片來試看看,最後模糊化的結果如下圖顯示,

PyQt5 顯示 OpenCV 影像圖片的視窗程式(加入滑鼠滾輪事件來處理縮放圖片)

對圖片作影像處理時,特別常用到縮放大小、裁剪圖片,
在圖形gui上常常使用滑鼠來完成這些操作,
所以滑鼠事件也是學習的重點之一,這個範例就來示範用滑鼠來縮放大小,
對於 OpenCV 縮放大小不熟悉的可以回去看我之前的文章
這邊我們直接建立一個ImageLabel類別並繼承QLabel
並且在ImageLabel上覆寫wheelEventmouseMoveEventmousePressEvent
並且在裡面實作我們自己對圖片縮放大小的邏輯,
mouseMoveEventmousePressEvent 在本範例並沒有使用到,這會在另一篇文章介紹,
這邊先關注wheelEvent,也就是滑鼠的滾輪事件,透過一些方法計算出滾輪滾動一格/一步numSteps時,進而對圖片縮放,
-1則縮小,1則放大,最後藉由cv2.resize()來完成圖片縮放,再顯示到畫面上,
來看看程式怎麼寫吧!

python3-opencv-image-pyqt-4.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import cv2
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog, QGridLayout, QLabel, QPushButton

class ImageLabel(QLabel):
scale = 1.0
def showImage(self, img):
height, width, channel = img.shape
bytesPerline = 3 * width
self.qImg = QImage(img.data, width, height, bytesPerline, QImage.Format_RGB888).rgbSwapped()
self.setPixmap(QPixmap.fromImage(self.qImg))

def mousePressEvent(self,event):
self.x = event.x()
self.y = event.y()
#print(str(self.x) + ' ' + str(self.y))

def mouseMoveEvent(self, event):
self.x = event.x()
self.y = event.y()
#print(str(self.x) + ' ' + str(self.y))

def wheelEvent(self, event):
numDegrees = event.angleDelta() / 8
numSteps = numDegrees / 15
#print(numSteps.y())
height, width, _ = self.img.shape
if numSteps.y() == -1:
if (self.scale >= 0.1):
self.scale -= 0.05
else:
if (self.scale <= 2.0):
self.scale += 0.05
#print(self.scale)
height2 = int(height * self.scale)
width2 = int(width * self.scale)
img2 = cv2.resize(self.img, (width2, height2), interpolation=cv2.INTER_AREA)
self.showImage(img2)

class MyDialog(QDialog):
def __init__(self):
super().__init__()
self.initUI()

def initUI(self):
self.resize(400, 300)
self.label = ImageLabel()
#self.label.setMouseTracking(True)
self.btnOpen = QPushButton('Open Image', self)
self.btnProcess = QPushButton('Blur Image', self)
self.btnSave = QPushButton('Save Image', self)
self.btnSave.setEnabled(False)

layout = QGridLayout(self)
layout.addWidget(self.label, 0, 0, 4, 4)
layout.addWidget(self.btnOpen, 4, 0, 1, 1)
layout.addWidget(self.btnProcess, 4, 1, 1, 1)
layout.addWidget(self.btnSave, 4, 2, 1, 1)

self.btnOpen.clicked.connect(self.openSlot)
self.btnProcess.clicked.connect(self.processSlot)
self.btnSave.clicked.connect(self.saveSlot)

def openSlot(self):
filename, _ = QFileDialog.getOpenFileName(self, 'Open Image', 'Image', '*.png *.jpg *.bmp')
if filename is '':
return
self.label.img = cv2.imread(filename, -1)
if self.label.img.size == 1:
return
self.label.showImage(self.label.img)
height, width, _ = self.label.img.shape
self.label.setFixedSize(width, height)
self.btnSave.setEnabled(True)

def saveSlot(self):
filename, _ = QFileDialog.getSaveFileName(self, 'Save Image', 'Image', '*.png *.jpg *.bmp')
if filename is '':
return
cv2.imwrite(filename, self.label.img)

def processSlot(self):
self.label.img = cv2.blur(self.label.img, (7, 7))
self.label.showImage(self.label.img)

if __name__ == '__main__':
a = QApplication(sys.argv)
dialog = MyDialog()
dialog.show()
sys.exit(a.exec_())

把程式執行起來,這次我們一樣選擇水果的圖片來試看看,用滑鼠滾輪來放大縮小,
會發現圖片會隨著滑鼠的滾輪轉動而放大或鎖小,放大後的結果如下圖顯示,

參考
python - Show an OpenCV image with PyQt5 - Stack Overflow
https://stackoverflow.com/questions/57204782/show-an-opencv-image-with-pyqt5
QWheelEvent — Qt for Python
https://doc.qt.io/qtforpython/PySide2/QtGui/QWheelEvent.html
PyQt5番外篇(1):PyQt5与Opencv的小小融合 - 知乎
https://zhuanlan.zhihu.com/p/31810054

其它相關文章推薦
Python OpenCV 彩色轉灰階(RGB/BGR to GRAY)
Python OpenCV 彩色轉YCbCr(RGB/BGR to YCbCr)
Python OpenCV 灰階轉彩色(Gray to RGB/BGR)
Python OpenCV 影像二值化 Image Thresholding
Python OpenCV 影像平滑模糊化 blur
Python OpenCV 影像邊緣偵測 Canny Edge Detection
Python OpenCV resize 圖片縮放
Python OpenCV 畫矩形 rectangle
Python OpenCV 畫多邊形 polylines

Python chatroom 多人聊天室

本篇介紹如何使用 Python 來撰寫 chatroom 多人聊天室的程式,在上一篇介紹了怎麼撰寫一個基本的 TCP socket 通訊程式,本篇開始介紹如何使用先前幾篇學習到的 python tcp socket 基礎來打造一個多人聊天室,

在多人聊天室的通訊程式設計上,通常有多執行緒(muti-threading)與多路復用(select)的兩種設計方式,以下將會介紹,

server 多人聊天室伺服器端 (多執行緒版本)

使用多執行緒來寫多人聊天室伺服器端,每連上一個客戶端便開一個執行緒服務 clientthread() 這個客戶連線,

python-chatroom-server.py
1
2
3
4
5
6
7
8
9
10
11
while True:
conn, addr = server.accept()

list_of_clients.append(conn)

print(addr[0] + " connected")

threading.Thread(target = clientthread, args = (conn, addr))

conn.close()
server.close()

以下為 clientthread() 函式內容,簡單說就是將收到的訊息廣播給已連線的所有客戶端,
如果有人離線的話則處理離線的邏輯,

python-chatroom-server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def clientthread(conn, addr):

# sends a message to the client whose user object is conn
conn.send("Welcome to this chatroom!".encode("utf-8"))

while True:
try:
message = conn.recv(2048)
if len(message) != 0:
print("<" + addr[0] + "> " + message.decode("utf-8"))

# Calls broadcast function to send message to all
message_to_send = "<" + addr[0] + "> " + message.decode("utf-8")
broadcast(message_to_send, conn)

else:
print('server closed connection.')
remove(conn)

except:
continue

client 多人聊天室客戶端 (select版本)

使用 select,不但能同時接收訊息外,也能接收到鍵盤的輸入訊息,並把鍵盤輸入的訊息傳輸出去,
這兩個一收一送的動作,透過多路復用 select() 函式可以達到同時運作的功效,

python-chatroom-client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
while True:
read_sockets, write_socket, error_socket = select.select(sockets_list, [], [])

for socks in read_sockets:
if socks == server:
message = socks.recv(2048)
print(message.decode("utf-8"))
else:
message = sys.stdin.readline()
server.send(message.encode("utf-8"))
sys.stdout.write("<You>")
sys.stdout.write(message)
sys.stdout.flush()
server.close()

其它相關文章推薦
下一篇會介紹如何打造一個gui圖形介面的多人聊天室
Python TCP Socket Server/Client 網路通訊程式

Python 檢查 list 列表是否為空

本篇介紹如何在 python 檢查 list 列表是否為空,

使用 not operator 運算子

使用 not operator 運算子來檢查 list 列表是否為空,
同時也是官方推薦的作法,

1
2
3
4
5
6
7
8
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
mylist = [] # or mylist = list()
print(type(mylist))
print(mylist)

if not mylist:
print('mylist is empty')

結果輸出:

1
2
3
<class 'list'>
[]
mylist is empty

使用 len 判斷長度

使用 len() 函式來檢查 list 列表是否為空,

1
2
3
4
5
6
7
8
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
mylist = list() # or mylist = []
print(type(mylist))
print(len(mylist))

if len(mylist) == 0:
print('mylist is empty')

結果輸出:

1
2
3
<class 'list'>
0
mylist is empty

參考
python - How do I check if a list is empty? - Stack Overflow
https://stackoverflow.com/questions/53513/how-do-i-check-if-a-list-is-empty

其它相關文章推薦
如果你想學習 Python 相關技術,可以參考看看下面的文章,
Python 新手入門教學懶人包
Python list 串列用法與範例

Python socketserver 伺服器端網路通訊程式

本篇介紹如何使用 Python 3 提供的 socketserver 類別來撰寫 Server 伺服器端的程式,在上一篇介紹了怎麼撰寫一個基本的 TCP socket 通訊程式
這一篇將專門介紹 server 伺服器端的撰寫,為什麼呢?因為通常伺服器端的程式是比客戶端的程式還要難的,
所以 python 提供了 socketserver 這個模組來簡化這個開發的難易度,
而 python 的 socketserver 模組提供了 TCPServer,UDPServer,UnixStreamServer,UnixDatagramServer 這4種,
接下來的內容將會先以 TCP 解說為優先,本篇學習了 socketserver 基礎概念後下篇會以本篇為基礎撰寫一個簡單的聊天程式,

使用 socketserver.TCPServer

以下使用 socketserver.TCPServer 的範例,client 端可以沿用之前的範例,
範例中直接實例化 socketserver.TCPServerMyTCPHandler 繼承 socketserver.BaseRequestHandler
socketserver.BaseRequestHandler 是用來處理接收資料時事件處理,不能直接產生實例,需要繼承 socketserver.BaseRequestHandler 後才能實例化,
handle() 裡撰寫接收後的程式邏輯即可,以這個範例為例,接收到一個 request 後,就開始一個迴圈來交換傳遞資料,

另外補充一下python 2與python 3的差異處,
python 2 是 SocketServer class 與 import SocketServer
python 3 是 socketserver class 與 import socketserver

python3-socketserver-echo-server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):

def handle(self):
print('connected by ' + str(self.client_address))
while True:
indata = self.request.recv(1024).strip()
if len(indata) == 0: # connection closed
self.request.close()
print('client closed connection.')
break
print('recv: ' + indata.decode())

outdata = 'echo ' + indata.decode()
self.request.sendall(outdata.encode())

if __name__ == '__main__':
HOST, PORT = "0.0.0.0", 7000

# Create the server, binding to localhost on port 7000
socketserver.TCPServer.allow_reuse_address = True
server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

print('server start at: %s:%s' % (HOST, PORT))
try:
server.serve_forever()
except:
print("closing the server.")
server.server_close()
raise

多執行緒的伺服器端程式

上述的例子一次最多只能服務一個客戶端,這篇介紹用 socketserver.ThreadingMixIn 的方式達成每個連線都建立一個新的執行緒來服務,
進而達成多客戶端連線溝通的例子,
在範例中的 ThreadedTCPServer 是多重繼承 socketserver.ThreadingMixInsocketserver.TCPServer

python3-socketserver-asynchronous-server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socketserver, sys, threading
from time import ctime

class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
cur = threading.current_thread()
print('[%s] Client connected from %s and [%s] is handling with him.' % (ctime(), self.request.getpeername(), cur.name))
while True:
indata = self.request.recv(1024).strip()
if len(indata) == 0: # connection closed
self.request.close()
print('client closed connection.')
break
print('recv: ' + indata.decode())

outdata = 'echo ' + indata.decode()
self.request.send(outdata.encode())

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
daemon_threads = True
allow_reuse_address = True

if __name__ == '__main__':
HOST, PORT = '0.0.0.0', 7000
server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
ip, port = server.server_address
print('server start at: %s:%s' % (HOST, PORT))
try:
server.serve_forever()
except KeyboardInterrupt:
sys.exit(0)

參考
21.21. socketserver — A framework for network servers — Python 3.5.10 documentation
https://docs.python.org/3.5/library/socketserver.html#examples
例項講解Python中SocketServer模組處理網路請求的用法 | 程式前沿
https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/368565/
Python使用SocketServer模組編寫基本伺服器程式的教程 | 程式前沿
https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/368387/
Python socketserver — A framework for network servers | My.APOLLO
https://myapollo.com.tw/zh-tw/python-socketserver/
20.17. SocketServer — A framework for network servers — Python 2 documentation
https://docs.python.org/2/library/socketserver.html
socketserver — A framework for network servers — Python 3 documentation
https://docs.python.org/3/library/socketserver.html

其他相關參考
c# - Why does my python TCP server need to bind to 0.0.0.0 and not localhost or it’s IP address? - Stack Overflow
https://stackoverflow.com/questions/38256851/why-does-my-python-tcp-server-need-to-bind-to-0-0-0-0-and-not-localhost-or-its
Python 3 TypeError: Can’t convert ‘bytes’ object to str implicitly - Mkyong.com
https://mkyong.com/python/python-3-typeerror-cant-convert-bytes-object-to-str-implicitly/

其它相關文章推薦
Python TCP Socket Server/Client 網路通訊程式

C/C++ 整數乘法溢位

今天要介紹寫程式時應該要避免的 integer multiplication overflow 整數乘法溢位問題,

故事是某天我在做時間單位轉換時發生了一個小意外,
想要將 us 轉成 ms 時間單位,程式碼如下,

1
2
3
4
5
6
7
8
uint64_t us = 12345678;
uint32_t ms;

ms = us / 1000;
uint64_t us2 = ms * 1000;
cout << us << \n;
cout << ms << \n;
cout << us2 << \n;

乍看之下,好像沒什麼問題,但總是有問題不會發生在當下,而是在某個時機發生了XD,

怎麼說呢,讓我們換個例子試試

1
2
3
4
5
6
7
8
uint64_t us = 43xxxxxxx;
uint32_t ms;

ms = us / 1000;
uint64_t us2 = ms * 1000;
cout << us << \n;
cout << ms << \n;
cout << us2 << \n;

不難發現,程式執行結果不符合我們的預期,

其實問題會發生在這一行,

1
uint64_t us2 = ms * 1000;

ms 這個變數是uint32_t 大小的,其實是用uint32_t這個大小去乘1000,乘完放在uint32_t,最後再複製到uint64_t,如果ms變數裡面放的數字乘出來的結果會超過uint32_t的話,悲劇就會發生了,發生了「乘法溢位」。

所以我們應該怎麼改?
答案是,先把ms從uint32_t擴大轉換widening conversions(promotion)成uint64_t大小,這樣在計算時就會以uint64_t去作計算,

1
uint64_t us2 = uint64_t(ms) * 1000;

所以要把握一個原則,「乘法時要小心溢位」

參考
小心 int 乘法溢出! - IT閱讀
https://www.itread01.com/articles/1506858849.html
Check for integer overflow on multiplication - GeeksforGeeks
https://www.geeksforgeeks.org/check-integer-overflow-multiplication/
c - Catch and compute overflow during multiplication of two large integers - Stack Overflow
https://stackoverflow.com/questions/1815367/catch-and-compute-overflow-during-multiplication-of-two-large-integers
類型轉換與類型安全 | Microsoft Docs
https://docs.microsoft.com/zh-tw/cpp/cpp/type-conversions-and-type-safety-modern-cpp?view=msvc-160
Type conversions - C++ Tutorials
http://www.cplusplus.com/doc/tutorial/typecasting/
Type Conversion in C++ - GeeksforGeeks
https://www.geeksforgeeks.org/type-conversion-in-c/

其它相關文章推薦
C/C++ 新手入門教學懶人包
C++ virtual 的兩種用法
C/C++ 字串反轉 reverse
C/C++ call by value傳值, call by pointer傳址, call by reference傳參考 的差別
C++ 類別樣板 class template
std::sort 用法與範例
std::find 用法與範例
std::queue 用法與範例
std::map 用法與範例

vim 使用 ctags 與 cscope 的開發環境

vim 要 trace code 時必裝兩大套件,trace code 效率大幅提升,這兩個套件叫 ctagscscope
簡單說就是「跳至定義處」這功能要裝 ctags,要「查找誰呼叫這個函式」這功能要裝 cscope,
使用這兩個套件打造出來的這樣開發環境幾乎可以媲美付費的 source insight。

ctags 安裝設定

Ubuntu/Debian 安裝 ctags 可以透過 apt 安裝

1
apt-get install exuberant-ctags

最簡單無腦的 ctags 產生方式,. 表示當前目錄或不指定也可以,
ctags 還能指定其他參數,這部份以後有機會在介紹,

1
ctags -R .

Vim 加入 ctags 產生出來的 tags 檔案

1
:set tags=./tags

cscope 安裝設定

Ubuntu/Debian 安裝 cscope 可以透過 apt 安裝

1
apt-get install cscope

最簡單無腦的 cscope 產生方式

1
cscope -Rbq

常用參數說明如下,
R: Recurse directories for files (目錄下遞迴建立索引)
b: Build the cross-reference only
q: Build an inverted index for quick symbol searching (建立 cscope.in.out 和 cscope.po.out,以便增快搜尋速度)
k: Kernel Mode - don’t use /usr/include for #include files (不索引 /usr/include)

Vim 加入 cscope 產生出來的 cscope.out 檔案

1
:cs add cscope.out

參考
vim附件:cscope+ctag 使用筆記 @ CONY的世界 :: 痞客邦 ::
https://angledark0123.pixnet.net/blog/post/51919594
[Linux] - 將vim打造成source insight | Ivan’s Blog
https://ivan7645.github.io/2016/07/12/vim_to_si/

其它相關文章推薦
Visual Studio Code 常用快捷鍵