前言
個人半學期的期末報告專案,因為想做的更好,所以自行研究了 Python 結合前端介面技術的幾個框架。
建議先備知識
前端開發經驗。 Python 使用經驗。 OpenCV 使用經驗。
最終成果預覽
VIDEO
Python UI 介面選擇
一開始課堂上是教預設的Qt軟體,但我覺得Qt醜醜的,所以忍不住去嘗試其他框架。
Tkinter
跟以前做window form很像,用程式碼掛載與編排UI物件。
myLabel = Label ( root, text= "Hello world! " )
from tkinter import *
root = Tk()
#創建label widget
myLabel = Label(root, text="Hello world! ")
# show on screen
myLabel.pack()
root.mainloop()
from tkinter import *
root = Tk()
#創建label widget
myLabel = Label(root, text="Hello world! ")
# show on screen
myLabel.pack()
root.mainloop()
Qt Designer
在Anaconda預設功能, Anaconda資料夾下找到designer.exe啟動。
排版檔案會取為.ui附檔名,操作邏輯與Tk類似。
範例: 讀取圖片、變暗處理。
from PyQt5. QtGui import *
from PyQt5. QtWidgets import *
class MyMainWindow ( QMainWindow, Ui_MainWindow ) :
def __init__ ( self, parent=None ) :
super ( MyMainWindow, self ) . __init__ ( parent )
self. loadBtn . clicked . connect ( self. btnLoadfile_clicked )
self. darkenBtn . clicked . connect ( self. btnDarking_clicked )
def btnLoadfile_clicked ( self ) :
img=cv2. imread ( "Baboon.bmp" )
imgBGR = cv2. resize ( img, ( 300 , 300 ) , interpolation=cv2. INTER_CUBIC )
imgRGB = cv2. cvtColor ( imgBGR, cv2. COLOR_BGR2RGB )
height, width = imgRGB. shape [ : 2 ]
qimg = QImage ( imgRGB, width, height, QImage. Format_RGB888 )
qpix = QPixmap. fromImage ( qimg )
def btnDarking_clicked ( self ) :
self. resImg . setPixmap ( qpix )
app = QApplication ( sys. argv )
#CallMyWindow1.py
import cv2
import numpy as np
from PyQt5.QtGui import *
import sys
from PyQt5.QtWidgets import *
from Mywindow3 import *
class MyMainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MyMainWindow, self).__init__(parent)
self.setupUi(self)
self.loadBtn.clicked.connect(self.btnLoadfile_clicked)
self.darkenBtn.clicked.connect(self.btnDarking_clicked)
#讀取圖片 - 按鈕
def btnLoadfile_clicked(self):
global img
img=cv2.imread("Baboon.bmp")
imgBGR = cv2.resize(img, (300, 300), interpolation=cv2.INTER_CUBIC)
imgRGB = cv2.cvtColor(imgBGR, cv2.COLOR_BGR2RGB)
height, width = imgRGB.shape[:2]
qimg = QImage(imgRGB, width, height, QImage.Format_RGB888)
qpix = QPixmap.fromImage(qimg)
self.img.setPixmap(qpix)
#圖片變暗 - 按鈕
def btnDarking_clicked(self):
#........
self.resImg.setPixmap(qpix)
if __name__=="__main__":
app = QApplication(sys.argv)
myWin = MyMainWindow()
myWin.show()
sys.exit(app.exec_())
#CallMyWindow1.py
import cv2
import numpy as np
from PyQt5.QtGui import *
import sys
from PyQt5.QtWidgets import *
from Mywindow3 import *
class MyMainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MyMainWindow, self).__init__(parent)
self.setupUi(self)
self.loadBtn.clicked.connect(self.btnLoadfile_clicked)
self.darkenBtn.clicked.connect(self.btnDarking_clicked)
#讀取圖片 - 按鈕
def btnLoadfile_clicked(self):
global img
img=cv2.imread("Baboon.bmp")
imgBGR = cv2.resize(img, (300, 300), interpolation=cv2.INTER_CUBIC)
imgRGB = cv2.cvtColor(imgBGR, cv2.COLOR_BGR2RGB)
height, width = imgRGB.shape[:2]
qimg = QImage(imgRGB, width, height, QImage.Format_RGB888)
qpix = QPixmap.fromImage(qimg)
self.img.setPixmap(qpix)
#圖片變暗 - 按鈕
def btnDarking_clicked(self):
#........
self.resImg.setPixmap(qpix)
if __name__=="__main__":
app = QApplication(sys.argv)
myWin = MyMainWindow()
myWin.show()
sys.exit(app.exec_())
讓使用者能用前端技術做UI的Python框架,內部使用Bottle Server框架 ,但不像flask能直接host在線上。
from turtle import position
eel. init ( f '{os.path.dirname(os.path.realpath(__file__))}/web' )
eel. start ( 'main.html' ,size= ( 400 , 200 ) ,position= ( 500 , 500 )) #網頁
#eel.start('main.html',mode ='chrome-app') #網頁 (app模式)
from turtle import position
import eel
import os
#eel.init('./web') #資料夾
eel.init(f'{os.path.dirname(os.path.realpath(__file__))}/web')
eel.start('main.html',size=(400,200),position=(500,500)) #網頁
#eel.start('main.html',mode ='chrome-app') #網頁 (app模式)
from turtle import position
import eel
import os
#eel.init('./web') #資料夾
eel.init(f'{os.path.dirname(os.path.realpath(__file__))}/web')
eel.start('main.html',size=(400,200),position=(500,500)) #網頁
#eel.start('main.html',mode ='chrome-app') #網頁 (app模式)
Flask / Django
使用Python編寫的輕量級Web應用框架。
範例: flask基本頁面,但其網頁模板概念跟Django雷同。
return "hello worldd123456"
if __name__ == '__main__' :
app. run ( debug= True , use_reloader= False port= 5000 , host= '0.0.0.0' )
from flask import Flask
app= Flask(__name__);
@app.route('/')
def index():
return "hello worldd123456"
if __name__ =='__main__':
app.run(debug=True , use_reloader=False port=5000 , host='0.0.0.0')
from flask import Flask
app= Flask(__name__);
@app.route('/')
def index():
return "hello worldd123456"
if __name__ =='__main__':
app.run(debug=True , use_reloader=False port=5000 , host='0.0.0.0')
範例: Django網頁
創個views.py檔案,裡面定義Hello頁面內容
from django. http import HttpResponse
return HttpResponse ( "Hello world" )
from django.http import HttpResponse
def Hello(request):
return HttpResponse("Hello world")
from django.http import HttpResponse
def Hello(request):
return HttpResponse("Hello world")
在urls.py定義Hello頁面的網址
from django. contrib import admin
from django. urls import path
#from django.conf.urls import url
from django. urls import include, re_path
path ( 'admin/' , admin. site . urls ) ,
#url(r'^$' , views.Hello)
re_path ( r '^$' , views. Hello ) #re_path使用regex
from django.contrib import admin
from django.urls import path
#from django.conf.urls import url
from django.urls import include, re_path
from . import views
urlpatterns = [
path('admin/', admin.site.urls),
#url(r'^$' , views.Hello)
re_path(r'^$' , views.Hello) #re_path使用regex
]
from django.contrib import admin
from django.urls import path
#from django.conf.urls import url
from django.urls import include, re_path
from . import views
urlpatterns = [
path('admin/', admin.site.urls),
#url(r'^$' , views.Hello)
re_path(r'^$' , views.Hello) #re_path使用regex
]
框架名稱 優點 缺點 Tkinter 像是用Python程式碼撰寫介面的Qt Designer。 邏輯與QtDesigner類似,同樣無法解決程式碼攏長、難做排版等問題。 Flask/Django 較完整且可部屬網頁的web應用框架。 Python與前端邏輯溝通較複雜,需使用ajax等呼叫方式。 eel 旨在於結合前端技術開發桌面應用程式,前後端邏輯串接容易。 我試著包在docker環境下部屬但失敗多次,雖然文獻不多但似乎eel不支援網站部屬。
EEL基本使用
(推薦影片: https://youtu.be/FNPW2ZFksCQ )
python 呼叫js方法
python 方法上加@eel.expose 讓js端可以呼叫。
eel. init ( f '{os.path.dirname(os.path.realpath(__file__))}/web' )
eel. start ( 'main.html' ,mode = 'chrome-app' ) #網頁 (app模式)
import eel
import os
@eel.expose
def print_js():
print("Hi hi")
#eel.init('./web') #資料夾
eel.init(f'{os.path.dirname(os.path.realpath(__file__))}/web')
eel.start('main.html',mode ='chrome-app') #網頁 (app模式)
import eel
import os
@eel.expose
def print_js():
print("Hi hi")
#eel.init('./web') #資料夾
eel.init(f'{os.path.dirname(os.path.realpath(__file__))}/web')
eel.start('main.html',mode ='chrome-app') #網頁 (app模式)
func.js檔案:
function hello(){
eel.print_js()()
}
function hello(){
eel.print_js()()
}
Async方法使用
return random. randint ( 0 , 100 )
eel. init ( f '{os.path.dirname(os.path.realpath(__file__))}/web' )
eel. start ( 'main.html' ,mode = 'chrome-app' ) #網頁 (app模式)
#time.sleep(5) 要sleep就用eel
import random
import eel
import os
@eel.expose
def getnum():
return random.randint(0,100)
#eel.init('./web') #資料夾
eel.init(f'{os.path.dirname(os.path.realpath(__file__))}/web')
eel.start('main.html',mode ='chrome-app') #網頁 (app模式)
eel.sleep(5)
#time.sleep(5) 要sleep就用eel
import random
import eel
import os
@eel.expose
def getnum():
return random.randint(0,100)
#eel.init('./web') #資料夾
eel.init(f'{os.path.dirname(os.path.realpath(__file__))}/web')
eel.start('main.html',mode ='chrome-app') #網頁 (app模式)
eel.sleep(5)
#time.sleep(5) 要sleep就用eel
var a=await eel. getnum ()()
async function doA(){
var a=await eel.getnum()()
alert(a)
}
async function doA(){
var a=await eel.getnum()()
alert(a)
}
按下按鈕後,js端等待python處理結果後出現alert。
< script src= "/eel.js" >< /script >
< link rel= "stylesheet" href= "style.css" >
< script src= "func.js" >< /script >
< button onclick= "doA()" > hello < /button >
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="/eel.js"></script>
<link rel="stylesheet" href="style.css">
<script src="func.js"></script>
</head>
<body>
<h1>Hell world</h1>
<button onclick="doA()">hello</button>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="/eel.js"></script>
<link rel="stylesheet" href="style.css">
<script src="func.js"></script>
</head>
<body>
<h1>Hell world</h1>
<button onclick="doA()">hello</button>
</body>
</html>
JS-EEL圖檔傳輸
跨語言開發首先遇到的問題便是如何傳遞圖片資訊? 由於處理後的圖片需轉換成前端html可顯示的based64編碼,其跟openCV imread讀取進來的圖片格式之間的關係又是如何? 為此我必須先了解從HTML的input 標籤輸入的圖片至python端之間的格式與轉換。我整理出如下圖這個流程:
(1.) 由input標籤開啟的圖片為大型二進位檔物件(Binary Large Object ,blob),代表了一個相當於檔案(原始資料)的不可變物件。
(2.) 需實作js的FileReader物件,將blob轉碼成based64字串。
(3.) 透過eel框架傳送字串至Python端。
(4.) 使用based64處理套件將字串解析成byte陣列。
(5.) 透過numpy將byte陣列轉成uint8陣列。
(6.) 透過cv2套件將uint陣列解碼成圖片,此時得到的檔案形同於imread進來的圖片。
(7.) 使用圖片資料進行影像處理操作。
(8.) 將處理完的圖片回傳至前端作顯示時,逆著遵循前述的轉碼步驟,先將圖片轉成byte陣列,再以based64編碼成字串透過eel框架溝通回前端,由js接收並設定img標籤的src。
JS讀取圖片
使用jQuery偵測input標籤Change事件,搭配fileReader轉成base64字串。
$ ( "#img" ) . on ( "change" , function () {
var file = this . files [ 0 ] ;
const fr = new FileReader () ;
fr. onload = function ( e ) {
preview. src = e. target . result ;
currentFile = e. target . result ;
console. log ( e. target . result ) ;
$("#img").on("change", function () {
var file = this.files[0];
const fr = new FileReader();
fr.onload = function (e) {
preview.src = e.target.result;
currentFile = e.target.result;
console.log(e.target.result);
};
if (file) {
fr.readAsDataURL(file);
} else {
console.log("沒有檔案");
}
console.log(file);
});
$("#img").on("change", function () {
var file = this.files[0];
const fr = new FileReader();
fr.onload = function (e) {
preview.src = e.target.result;
currentFile = e.target.result;
console.log(e.target.result);
};
if (file) {
fr.readAsDataURL(file);
} else {
console.log("沒有檔案");
}
console.log(file);
});
Python接收圖片
將base64字串透過轉碼,轉成opencv可使用的格式。
def data_uri_to_cv2_img ( uri ) :
encoded_data = uri. split ( ',' )[ 1 ]
img = base64. b64decode ( encoded_data )
npimg = np. frombuffer ( img, np. uint8 )
image = cv2. imdecode ( npimg,cv2. IMREAD_COLOR )
def data_uri_to_cv2_img(uri):
encoded_data = uri.split(',')[1]
img = base64.b64decode(encoded_data)
npimg = np.frombuffer(img, np.uint8)
image = cv2.imdecode(npimg,cv2.IMREAD_COLOR)
return image
def data_uri_to_cv2_img(uri):
encoded_data = uri.split(',')[1]
img = base64.b64decode(encoded_data)
npimg = np.frombuffer(img, np.uint8)
image = cv2.imdecode(npimg,cv2.IMREAD_COLOR)
return image
搭配OpenCV
範例: 將圖片轉灰階
function SetImg ( newimg ) {
preview. src = "data:image/png;base64," + newimg
currentFile = preview. src ;
eel.expose(SetImg);
function SetImg(newimg) {
preview.src = "data:image/png;base64," + newimg
}
function Gray() {
currentFile = preview.src;
eel.toGray(currentFile);
}
eel.expose(SetImg);
function SetImg(newimg) {
preview.src = "data:image/png;base64," + newimg
}
function Gray() {
currentFile = preview.src;
eel.toGray(currentFile);
}
def data_uri_to_cv2_img ( uri ) :
encoded_data = uri. split ( ',' )[ 1 ]
img = base64. b64decode ( encoded_data )
npimg = np. frombuffer ( img, np. uint8 )
image = cv2. imdecode ( npimg,cv2. IMREAD_COLOR )
img = data_uri_to_cv2_img ( url )
img=cv2. cvtColor ( img,cv2. COLOR_BGR2GRAY )
eel. init ( f '{os.path.dirname(os.path.realpath(__file__))}/web' )
eel. start ( 'main.html' ,mode = 'chrome-app' ) #網頁 (app模式)
import base64
import random
from cv2 import imread
import eel
import os
import cv2
import numpy as np
def data_uri_to_cv2_img(uri):
encoded_data = uri.split(',')[1]
img = base64.b64decode(encoded_data)
npimg = np.frombuffer(img, np.uint8)
image = cv2.imdecode(npimg,cv2.IMREAD_COLOR)
return image
@eel.expose
def toGray(url):
img = data_uri_to_cv2_img(url)
img=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#TODO:.....回傳
eel.init(f'{os.path.dirname(os.path.realpath(__file__))}/web')
eel.start('main.html',mode ='chrome-app') #網頁 (app模式)
import base64
import random
from cv2 import imread
import eel
import os
import cv2
import numpy as np
def data_uri_to_cv2_img(uri):
encoded_data = uri.split(',')[1]
img = base64.b64decode(encoded_data)
npimg = np.frombuffer(img, np.uint8)
image = cv2.imdecode(npimg,cv2.IMREAD_COLOR)
return image
@eel.expose
def toGray(url):
img = data_uri_to_cv2_img(url)
img=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#TODO:.....回傳
eel.init(f'{os.path.dirname(os.path.realpath(__file__))}/web')
eel.start('main.html',mode ='chrome-app') #網頁 (app模式)
Python傳回圖片
b64_str = cv2. imencode ( '.png' , img )[ 1 ] . tobytes ()
blob = base64. b64encode ( b64_str )
blob = blob. decode ( 'utf-8' )
def img_to_base64(img):
b64_str = cv2.imencode('.png', img)[1].tobytes()
blob = base64.b64encode(b64_str)
blob = blob.decode('utf-8')
return blob
def img_to_base64(img):
b64_str = cv2.imencode('.png', img)[1].tobytes()
blob = base64.b64encode(b64_str)
blob = blob.decode('utf-8')
return blob
直方圖
直方圖常常用在檢視圖片顏色趨勢、集中位置等等。 (一開始使用Chart.js插件)
js 端
eel. expose ( UpdateGarphData ) ;
function UpdateGarphData ( graphID , b,g,r ) {
var labs= [ Array. from ({ length: 255 }) . map (( currentElement, i ) = > i )]
const ctx = document. getElementById ( graphID ) . getContext ( "2d" ) ;
const chart = new Chart ( ctx, {
backgroundColor: "green" ,
// 直方圖
eel.expose(UpdateGarphData);
function UpdateGarphData(graphID , b,g,r) {
var labs= [Array.from({ length: 255 }).map((currentElement, i) => i)]
const ctx = document.getElementById(graphID).getContext("2d");
const chart = new Chart(ctx, {
type: "bar",
data: {
labels:labs[0],
datasets: [
{
label: "b",
data: b,
backgroundColor: "blue",
},
{
label: "g",
data: g,
backgroundColor: "green",
},
{
label: "r",
data: r,
backgroundColor: "red",
},
],
},
});
}
// 直方圖
eel.expose(UpdateGarphData);
function UpdateGarphData(graphID , b,g,r) {
var labs= [Array.from({ length: 255 }).map((currentElement, i) => i)]
const ctx = document.getElementById(graphID).getContext("2d");
const chart = new Chart(ctx, {
type: "bar",
data: {
labels:labs[0],
datasets: [
{
label: "b",
data: b,
backgroundColor: "blue",
},
{
label: "g",
data: g,
backgroundColor: "green",
},
{
label: "r",
data: r,
backgroundColor: "red",
},
],
},
});
}
img = data_uri_to_cv2_img ( url )
b=cv2. calcHist ( [ img ] , [ 0 ] , None, [ 256 ] , [ 0 , 256 ] )
b= b. astype ( "float" ) . flatten () . tolist ()
b=cv2. calcHist ( [ img ] , [ 0 ] , None, [ 256 ] , [ 0 , 256 ] )
g=cv2. calcHist ( [ img ] , [ 1 ] , None, [ 256 ] , [ 0 , 256 ] )
r=cv2. calcHist ( [ img ] , [ 2 ] , None, [ 256 ] , [ 0 , 256 ] )
b= b. astype ( "float" ) . flatten () . tolist ()
g= g. astype ( "float" ) . flatten () . tolist ()
r= r. astype ( "float" ) . flatten () . tolist ()
eel. UpdateGarphData ( "histogram" ,b,g,r )()
@eel.expose
def histGraph(url):
img = data_uri_to_cv2_img(url)
# 單色
if img.ndim !=3:
b=cv2.calcHist( [img], [0], None, [256], [0,256] )
b= b.astype("float").flatten().tolist()
# 彩色
else:
b=cv2.calcHist( [img], [0], None, [256], [0,256] )
g=cv2.calcHist( [img], [1], None, [256], [0,256] )
r=cv2.calcHist( [img], [2], None, [256], [0,256] )
b= b.astype("float").flatten().tolist()
g= g.astype("float").flatten().tolist()
r= r.astype("float").flatten().tolist()
eel.UpdateGarphData("histogram",b,g,r)()
@eel.expose
def histGraph(url):
img = data_uri_to_cv2_img(url)
# 單色
if img.ndim !=3:
b=cv2.calcHist( [img], [0], None, [256], [0,256] )
b= b.astype("float").flatten().tolist()
# 彩色
else:
b=cv2.calcHist( [img], [0], None, [256], [0,256] )
g=cv2.calcHist( [img], [1], None, [256], [0,256] )
r=cv2.calcHist( [img], [2], None, [256], [0,256] )
b= b.astype("float").flatten().tolist()
g= g.astype("float").flatten().tolist()
r= r.astype("float").flatten().tolist()
eel.UpdateGarphData("histogram",b,g,r)()
注意np array無法直接傳輸,所以要轉成一般array。
瀏覽器無法像Python的numpy與的plot套件快速做統計與圖表,加上本身也不支援多線程(Multi-threading),因此當圖片解析度大,需要統計的數變多,前端直方圖生成便會延遲,除了採用異步執行避免程序卡死外,我還試過多個輕量圖表套件如Chart.js、dygraphs、EChart.js,尋找對資料最友善的解決方案,雖然目前前端直方圖生成仍有延遲存在,是日後需想辦法解決的問題(目前已是在Python生成直方圖數據,再傳輸至前端表現,理論上只需處理255*3個顏色資料,我認為會卡的點在於繪製與自動尋找最小至最大值區間)。
等化功能
等化是將像素顏色拉得平均一點。
img = data_uri_to_cv2_img ( url )
g=cv2. equalizeHist ( img ) ;
g [ :,:,i ] =cv2. equalizeHist ( img [ :,:,i ])
@eel.expose
def equalizeHist(url):
img = data_uri_to_cv2_img(url)
g=img.copy()
#單色
if g.ndim!=3:
g=cv2.equalizeHist( img );
#彩色
else:
for i in range(0,3):
g[:,:,i]=cv2.equalizeHist(img[:,:,i])
histGraphByImg(g)
blob = img_to_base64(g)
eel.SetImg(blob)()
@eel.expose
def equalizeHist(url):
img = data_uri_to_cv2_img(url)
g=img.copy()
#單色
if g.ndim!=3:
g=cv2.equalizeHist( img );
#彩色
else:
for i in range(0,3):
g[:,:,i]=cv2.equalizeHist(img[:,:,i])
histGraphByImg(g)
blob = img_to_base64(g)
eel.SetImg(blob)()
操作Hot Key
註冊document事件,並藉由array紀錄各步驟的base64字串。
const previousSaves = [] ;
function BackToPreviouse () {
if ( previousSaves. length > 0 ) {
base_str=previousSaves. pop () ;
if ( nextSaves. length > 0 ) {
base_str=nextSaves. pop () ;
if ( previousSaves. length < maxSaves ){
previousSaves. push ( bs64 ) ;
previousSaves. push ( bs64 ) ;
function SaveNextStep ( bs64 ){
if ( nextSaves. length < maxSaves ){
// ***************** 註冊 hot key *********************
function RegiseterHotKey ( e ) {
if ( e. ctrlKey && e. key === 'z' ) {
if ( e. ctrlKey && e. key === 'x' ) {
document. addEventListener ( 'keyup' , RegiseterHotKey, false ) ;
// ***************** --- *********************
const maxSaves=10;
const previousSaves = [];
const nextSaves = [];
//上一步
function BackToPreviouse() {
if (previousSaves.length > 0) {
base_str=previousSaves.pop();
preview.src = base_str;
SaveNextStep(base_str)
}
}
//下一步
function BackToNext(){
if (nextSaves.length > 0) {
base_str=nextSaves.pop();
SaveStep(base_str);
preview.src = base_str;
}
}
//紀錄步驟
function SaveStep(bs64){
if(previousSaves.length<maxSaves){
previousSaves.push(bs64);
}
else{
//移除第一個
previousSaves.shift();
previousSaves.push(bs64);
}
}
//紀錄返回上一步步驟
function SaveNextStep(bs64){
if(nextSaves.length<maxSaves){
nextSaves.push(bs64);
}
else{
//移除第一個
nextSaves.shift();
nextSaves.push(bs64);
}
}
// ***************** 註冊 hot key *********************
function RegiseterHotKey(e) {
//上一步
if (e.ctrlKey && e.key === 'z') {
BackToPreviouse();
}
//下一步
if (e.ctrlKey && e.key === 'x') {
BackToNext();
}
}
document.addEventListener('keyup', RegiseterHotKey, false);
// ***************** --- *********************
const maxSaves=10;
const previousSaves = [];
const nextSaves = [];
//上一步
function BackToPreviouse() {
if (previousSaves.length > 0) {
base_str=previousSaves.pop();
preview.src = base_str;
SaveNextStep(base_str)
}
}
//下一步
function BackToNext(){
if (nextSaves.length > 0) {
base_str=nextSaves.pop();
SaveStep(base_str);
preview.src = base_str;
}
}
//紀錄步驟
function SaveStep(bs64){
if(previousSaves.length<maxSaves){
previousSaves.push(bs64);
}
else{
//移除第一個
previousSaves.shift();
previousSaves.push(bs64);
}
}
//紀錄返回上一步步驟
function SaveNextStep(bs64){
if(nextSaves.length<maxSaves){
nextSaves.push(bs64);
}
else{
//移除第一個
nextSaves.shift();
nextSaves.push(bs64);
}
}
// ***************** 註冊 hot key *********************
function RegiseterHotKey(e) {
//上一步
if (e.ctrlKey && e.key === 'z') {
BackToPreviouse();
}
//下一步
if (e.ctrlKey && e.key === 'x') {
BackToNext();
}
}
document.addEventListener('keyup', RegiseterHotKey, false);
// ***************** --- *********************
範例:上下一步熱鍵。
JS寫出圖片
源圖與輸出圖。
async function SaveAs () {
"image/*" : [ ".png" , ".gif" , ".jpeg" , ".jpg" ] ,
excludeAcceptAllOption: true ,
const url= decodeURIComponent ( preview. src ) . split ( "," )[ 1 ] ;
const imgBlob = Uint8Array. from ( atob ( url ) , c = > c. charCodeAt ( 0 )) ;
const newHandel = await window. showSaveFilePicker ( opts ) ;
const writableStream = await newHandel. createWritable () ;
await writableStream. write ( imgBlob ) ;
// close the file and write the contents to disk.
await writableStream. close () ;
// Save as
async function SaveAs() {
const opts = {
types: [
{
description: "Images",
accept: {
"image/*": [".png", ".gif", ".jpeg", ".jpg"],
},
},
],
excludeAcceptAllOption: true,
multiple: false,
};
//base64 轉 blob
const url= decodeURIComponent(preview.src).split(",")[1];
const imgBlob = Uint8Array.from(atob(url), c => c.charCodeAt(0));
console.log(imgBlob);
const newHandel = await window.showSaveFilePicker(opts);
const writableStream = await newHandel.createWritable();
// write our file
await writableStream.write(imgBlob);
// close the file and write the contents to disk.
await writableStream.close();
}
// Save as
async function SaveAs() {
const opts = {
types: [
{
description: "Images",
accept: {
"image/*": [".png", ".gif", ".jpeg", ".jpg"],
},
},
],
excludeAcceptAllOption: true,
multiple: false,
};
//base64 轉 blob
const url= decodeURIComponent(preview.src).split(",")[1];
const imgBlob = Uint8Array.from(atob(url), c => c.charCodeAt(0));
console.log(imgBlob);
const newHandel = await window.showSaveFilePicker(opts);
const writableStream = await newHandel.createWritable();
// write our file
await writableStream.write(imgBlob);
// close the file and write the contents to disk.
await writableStream.close();
}
拖拉圖片
$ ( "#preview" ) . draggable () ;
scale += event. deltaY * -0.02 ;
scale = Math. min ( Math. max ( 0.125 , scale ) , 500 ) ;
$ ( "#preview_slider" ) . val ( scale ) . change () ;
const el = document. querySelector ( "#preview" ) ;
el. addEventListener ( "wheel" , zoom ) ;
//preview 畫布拖拉
$(function () {
$("#preview").draggable();
});
//畫布滑鼠滾輪縮放
function zoom(event) {
event.preventDefault();
scale += event.deltaY * -0.02;
// Restrict scale
scale = Math.min(Math.max(0.125, scale), 500);
$("#preview_slider").val(scale).change();
}
let scale = 1;
const el = document.querySelector("#preview");
el.onwheel = zoom;
el.addEventListener("wheel", zoom);
//preview 畫布拖拉
$(function () {
$("#preview").draggable();
});
//畫布滑鼠滾輪縮放
function zoom(event) {
event.preventDefault();
scale += event.deltaY * -0.02;
// Restrict scale
scale = Math.min(Math.max(0.125, scale), 500);
$("#preview_slider").val(scale).change();
}
let scale = 1;
const el = document.querySelector("#preview");
el.onwheel = zoom;
el.addEventListener("wheel", zoom);
JS事件
在修改圖片時觸發事件,如此便能最後續處理,例如圖片縮圖僅需在圖片有更動時才須更新。
$ ( document ) . on ( "mainImageChanged" , function ( e,eventInfo ){
function SetImg ( newimg ) {
preview. src = "data:image/png;base64," + newimg;
$ ( document ) . trigger ( "mainImageChanged" ,preview )
//修改圖片事件:
$(document).on("mainImageChanged",function(e,eventInfo){
console.log(eventInfo);
//.....
})
//修改圖片
function SetImg(newimg) {
preview.src = "data:image/png;base64," + newimg;
//觸發事件
$(document).trigger("mainImageChanged",preview)
}
//修改圖片事件:
$(document).on("mainImageChanged",function(e,eventInfo){
console.log(eventInfo);
//.....
})
//修改圖片
function SetImg(newimg) {
preview.src = "data:image/png;base64," + newimg;
//觸發事件
$(document).trigger("mainImageChanged",preview)
}
顏色通道
圖片RGB拆成可以分別開關的通道,在P圖的時候常會從通道中選一張二值化效果最好的通道做遮罩,不過目前本軟體的通道只能顯示、不能編輯(;´༎ຶД༎ຶ`) (時間不夠..)。
def useChannel ( url, isR, isG, isB ) :
img = data_uri_to_cv2_img ( url )
g [ :, :, 2 ] = img [ :, :, 2 ]
g [ :, :, 0 ] = img [ :, :, 0 ]
g [ :, :, 1 ] = img [ :, :, 1 ]
@eel.expose
def useChannel(url, isR, isG, isB):
isR = bool(isR)
isG = bool(isG)
isB = bool(isB)
img = data_uri_to_cv2_img(url)
g = np.zeros(img.shape)
if isR:
g[:, :, 2] = img[:, :, 2]
if isB:
g[:, :, 0] = img[:, :, 0]
if isG:
g[:, :, 1] = img[:, :, 1]
blob = img_to_base64(g)
eel.SetImg(blob)()
@eel.expose
def useChannel(url, isR, isG, isB):
isR = bool(isR)
isG = bool(isG)
isB = bool(isB)
img = data_uri_to_cv2_img(url)
g = np.zeros(img.shape)
if isR:
g[:, :, 2] = img[:, :, 2]
if isB:
g[:, :, 0] = img[:, :, 0]
if isG:
g[:, :, 1] = img[:, :, 1]
blob = img_to_base64(g)
eel.SetImg(blob)()
在地化
加上為了降低開發環境複雜度,本軟體前端使用較原生的JQuery輔助,沒有用如React.js等完整的web生態系統,能使用的工具較為受限,最後使用行之有名的i18n在地化解決方案應付小需求翻譯。
'zh-TW' : "/i18n/zh-TW.json" ,
function SetLanguage ( lang ){
$.i18n({
//不要打預設語系就會自動抓瀏覽器語言
//locale: "zh-TW",
//locale: "en",
});
$.i18n()
.load({
en: "/i18n/en.json",
'zh-TW': "/i18n/zh-TW.json",
})
.done(function () {
$('body').i18n();
});
function SetLanguage(lang){
$.i18n().locale = lang;
$('body').i18n();
}
$.i18n({
//不要打預設語系就會自動抓瀏覽器語言
//locale: "zh-TW",
//locale: "en",
});
$.i18n()
.load({
en: "/i18n/en.json",
'zh-TW': "/i18n/zh-TW.json",
})
.done(function () {
$('body').i18n();
});
function SetLanguage(lang){
$.i18n().locale = lang;
$('body').i18n();
}
在地化效果。
簡易圖層
將兩張圖疊在一起
< div class = "opt_btns_container" >
< div class = "opt_bar" > 加入圖片 < /div >
< div class = "side_small_preview_container" id= "overlay" >
< img class = "channel_preview_img" id= "newLayerImage"
src= "https://react.semantic-ui.com/images/wireframe/image.png" >
createSelect ( "overlay" , "overlayMethod" , "混合模式" , [
{ val: "normal" , text: "正常" } ,
{ val: "brigher" , text: "變亮" } ,
{ val: "darken" , text: "變暗" } ,
< label for = "layerImgInput" >
< img class = "toggleImg" id= "ct_b" isToggled= "true" onclick= "LoadLayerImage(this)"
src= "./img/eye.png" >< /img >
< input type= "file" id= "layerImgInput" name= "layerImgInput" hidden >
< button class = "opt_apply_btn" onclick= "OverlayerImage()" > 套用 < /button >
<!-- 混和圖片 -->
<div class="opt_btns_container">
<div class="opt_bar">加入圖片</div>
<!-- 欲加入的圖片 -->
<div class="side_small_preview_container" id="overlay">
<img class="channel_preview_img" id="newLayerImage"
src="https://react.semantic-ui.com/images/wireframe/image.png">
<script>
createSelect("overlay", "overlayMethod", "混合模式", [
{ val: "normal", text: "正常" },
{ val: "brigher", text: "變亮" },
{ val: "darken", text: "變暗" },
])
</script>
<!-- 加入按鈕 -->
<label for="layerImgInput">
<img class="toggleImg" id="ct_b" isToggled="true" onclick="LoadLayerImage(this)"
src="./img/eye.png"></img>
</label>
<input type="file" id="layerImgInput" name="layerImgInput" hidden>
<button class="opt_apply_btn" onclick="OverlayerImage()">套用</button>
</div>
</div>
<!-- 混和圖片 -->
<div class="opt_btns_container">
<div class="opt_bar">加入圖片</div>
<!-- 欲加入的圖片 -->
<div class="side_small_preview_container" id="overlay">
<img class="channel_preview_img" id="newLayerImage"
src="https://react.semantic-ui.com/images/wireframe/image.png">
<script>
createSelect("overlay", "overlayMethod", "混合模式", [
{ val: "normal", text: "正常" },
{ val: "brigher", text: "變亮" },
{ val: "darken", text: "變暗" },
])
</script>
<!-- 加入按鈕 -->
<label for="layerImgInput">
<img class="toggleImg" id="ct_b" isToggled="true" onclick="LoadLayerImage(this)"
src="./img/eye.png"></img>
</label>
<input type="file" id="layerImgInput" name="layerImgInput" hidden>
<button class="opt_apply_btn" onclick="OverlayerImage()">套用</button>
</div>
</div>
HTML操作按鈕
function OverlayerImage (){
layer_b64_str =newLayerImage. src ;
eel. overlayImage ( b64_str,layer_b64_str,opt )
//疊圖層
function OverlayerImage(){
b64_str = realImage;
layer_b64_str =newLayerImage.src ;
opt=overlayMethod.value;
SaveStep(b64_str);
eel.overlayImage(b64_str,layer_b64_str,opt)
}
//疊圖層
function OverlayerImage(){
b64_str = realImage;
layer_b64_str =newLayerImage.src ;
opt=overlayMethod.value;
SaveStep(b64_str);
eel.overlayImage(b64_str,layer_b64_str,opt)
}
def overlayImage ( url,layerUrl,opt ) :
img = data_uri_to_cv2_img ( url )
img_overlay = data_uri_to_cv2_img ( layerUrl )
h, w = img_overlay. shape [ : 2 ]
yoff = np. clip ( round (( hh-h ) / 2 ) , 0 ,hh )
xoff =np. clip ( round (( ww-w ) / 2 ) , 0 ,ww )
yMax= np. clip ( yoff+h , 0 , hh )
xMax= np. clip ( xoff+w , 0 , ww )
print ( yoff,xoff , "|" ,yMax,xMax )
result [ yoff:yMax, xoff:xMax ] = img_overlay [ :yMin,:xMin ] #避免取出邊界
blob = img_to_base64 ( result )
eel. SetImg ( blob, false , false )()
def overlayImage(url,layerUrl,opt):
img = data_uri_to_cv2_img(url)
img_overlay = data_uri_to_cv2_img(layerUrl)
h, w = img_overlay.shape[:2]
hh, ww = img.shape[:2]
yoff = np.clip( round((hh-h)/2),0,hh)
xoff =np.clip( round((ww-w)/2),0,ww)
yMax= np.clip(yoff+h , 0 , hh)
xMax= np.clip(xoff+w , 0 , ww)
print(yoff,xoff , "|",yMax,xMax)
xMin= min(ww,w)
yMin= min(hh,h)
result = img.copy()
result[yoff:yMax, xoff:xMax] = img_overlay[:yMin,:xMin] #避免取出邊界
blob = img_to_base64(result)
eel.SetImg(blob, false, false)()
def overlayImage(url,layerUrl,opt):
img = data_uri_to_cv2_img(url)
img_overlay = data_uri_to_cv2_img(layerUrl)
h, w = img_overlay.shape[:2]
hh, ww = img.shape[:2]
yoff = np.clip( round((hh-h)/2),0,hh)
xoff =np.clip( round((ww-w)/2),0,ww)
yMax= np.clip(yoff+h , 0 , hh)
xMax= np.clip(xoff+w , 0 , ww)
print(yoff,xoff , "|",yMax,xMax)
xMin= min(ww,w)
yMin= min(hh,h)
result = img.copy()
result[yoff:yMax, xoff:xMax] = img_overlay[:yMin,:xMin] #避免取出邊界
blob = img_to_base64(result)
eel.SetImg(blob, false, false)()
小優化 — 副檔名溝通
原本以png回傳,但圖片大小因此暴增10倍
b64_str = cv2. imencode ( ".png" , img )[ 1 ] . tobytes ()
blob = base64. b64encode ( b64_str )
blob = blob. decode ( 'utf-8' )
print ( "process Time --- %s seconds ---" % ( time. time () - start_time ))
# 圖片轉base64
def img_to_base64(img):
b64_str = cv2.imencode(".png", img)[1].tobytes()
blob = base64.b64encode(b64_str)
blob = blob.decode('utf-8')
print("process Time --- %s seconds ---" % (time.time() - start_time))
return blob
# 圖片轉base64
def img_to_base64(img):
b64_str = cv2.imencode(".png", img)[1].tobytes()
blob = base64.b64encode(b64_str)
blob = blob.decode('utf-8')
print("process Time --- %s seconds ---" % (time.time() - start_time))
return blob
jpg編碼檔案大小: 120KB
png檔案變成 1MB
解決辦法:互相溝通檔案型態
currentFileExtension =file. type . split ( "/" )[ 1 ] ;
$ ( "#fileSize_text" ) . html ( Math. round ( file. size / 1024 ) + "KB" ) ;
eel. setFileExtension ( currentFileExtension )
if (file) {
fr.readAsDataURL(file);
console.log(file);
currentFileExtension =file.type.split("/")[1];
//更新 footer KB數
$("#fileSize_text").html(Math.round(file.size / 1024) + "KB");
//設定副檔名
eel.setFileExtension(currentFileExtension)
} else {
console.log("沒有檔案");
alert("不支援檔案類型");
}
if (file) {
fr.readAsDataURL(file);
console.log(file);
currentFileExtension =file.type.split("/")[1];
//更新 footer KB數
$("#fileSize_text").html(Math.round(file.size / 1024) + "KB");
//設定副檔名
eel.setFileExtension(currentFileExtension)
} else {
console.log("沒有檔案");
alert("不支援檔案類型");
}
def setFileExtension ( ext ) :
global current_file_extension
current_file_extension= "." +ext
@eel.expose
def setFileExtension(ext):
global current_file_extension
current_file_extension="."+ext
@eel.expose
def setFileExtension(ext):
global current_file_extension
current_file_extension="."+ext
【補充】
Base64 轉法造成卡頓?
原本以為卡頓問題是反覆的base64轉碼造成的。
def data_uri_to_cv2_img ( uri, forceUpdate= false ) :
if image!= "" and not forceUpdate:
img = base64. b64decode ( encoded_data )
npimg = np. frombuffer ( img, np. uint8 )
image = cv2. imdecode ( npimg, cv2. IMREAD_COLOR )
print ( "base 64 decode Time --- %s seconds ---" % ( time. time () - start_time ))
import time
image =""
# base64轉np image
def data_uri_to_cv2_img(uri, forceUpdate=false):
global image
start_time = time.time()
#print(image)
if image!="" and not forceUpdate:
return image
uri_spt = uri.split(',')
if(len(uri_spt)>1):
encoded_data=uri_spt[1]
else:
encoded_data=uri_spt[0]
img = base64.b64decode(encoded_data)
npimg = np.frombuffer(img, np.uint8)
image = cv2.imdecode(npimg, cv2.IMREAD_COLOR)
print("base 64 decode Time --- %s seconds ---" % (time.time() - start_time))
return image
import time
image =""
# base64轉np image
def data_uri_to_cv2_img(uri, forceUpdate=false):
global image
start_time = time.time()
#print(image)
if image!="" and not forceUpdate:
return image
uri_spt = uri.split(',')
if(len(uri_spt)>1):
encoded_data=uri_spt[1]
else:
encoded_data=uri_spt[0]
img = base64.b64decode(encoded_data)
npimg = np.frombuffer(img, np.uint8)
image = cv2.imdecode(npimg, cv2.IMREAD_COLOR)
print("base 64 decode Time --- %s seconds ---" % (time.time() - start_time))
return image
做了點log發現轉碼所需時間不長,主要問題是Chart.js插件效能問題。(javascript – ChartJS is slow – Stack Overflow )
base 64 decode Time --- 0.010005474090576172 seconds ---
base 64 decode Time --- 0.007999897003173828 seconds ---
base 64 decode Time --- 0.008002042770385742 seconds ---
base 64 decode Time --- 0.007974386215209961 seconds ---
base 64 decode Time --- 0.007002353668212891 seconds ---
base 64 decode Time --- 0.0069997310638427734 seconds ---
base 64 decode Time --- 0.00800013542175293 seconds ---
base 64 decode Time --- 0.007972240447998047 seconds ---
base 64 decode Time --- 0.007972240447998047 seconds ---
base 64 decode Time --- 0.010005474090576172 seconds ---
base 64 decode Time --- 0.007999897003173828 seconds ---
base 64 decode Time --- 0.008002042770385742 seconds ---
base 64 decode Time --- 0.007974386215209961 seconds ---
base 64 decode Time --- 0.007002353668212891 seconds ---
base 64 decode Time --- 0.0069997310638427734 seconds ---
base 64 decode Time --- 0.00800013542175293 seconds ---
base 64 decode Time --- 0.007972240447998047 seconds ---
base 64 decode Time --- 0.007972240447998047 seconds ---
base 64 decode Time --- 0.010005474090576172 seconds ---
base 64 decode Time --- 0.007999897003173828 seconds ---
base 64 decode Time --- 0.008002042770385742 seconds ---
base 64 decode Time --- 0.007974386215209961 seconds ---
base 64 decode Time --- 0.007002353668212891 seconds ---
base 64 decode Time --- 0.0069997310638427734 seconds ---
base 64 decode Time --- 0.00800013542175293 seconds ---
base 64 decode Time --- 0.007972240447998047 seconds ---
base 64 decode Time --- 0.007972240447998047 seconds ---
ERROR : WebSocket connection to ‘ws://localhost:8000/eel?page=main.html’ failed: Could not decode a text frame as UTF-8
https://github.com/python-eel/Eel/issues/299
在js呼叫另一個eel方法時,上一個還沒結束產生的ERROR。
打包輸出exe檔案
Eel is a little Python library for making simple Electron-like offline HTML/JS GUI apps, with… (curatedpython.com)
*EelPoriect拼錯字
python -m eel ./src/app. py ./src/web
cd src
python -m eel app.py web
---
python -m eel ./src/app.py ./src/web
cd src
python -m eel app.py web
---
python -m eel ./src/app.py ./src/web