【徵文賞-動態網頁】優選|使用前端技術製作自己的修圖軟體吧(Python + OpenCV + eel) – 林慶佳

前言

個人半學期的期末報告專案,因為想做的更好,所以自行研究了 Python 結合前端介面技術的幾個框架。

建議先備知識

  1. 前端開發經驗。
  2. Python 使用經驗。
  3. OpenCV 使用經驗。

最終成果預覽

Python UI 介面選擇

一開始課堂上是教預設的Qt軟體,但我覺得Qt醜醜的,所以忍不住去嘗試其他框架。

Tkinter

跟以前做window form很像,用程式碼掛載與編排UI物件。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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()
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類似。

範例: 讀取圖片、變暗處理。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#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_())
#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_())

Eel

讓使用者能用前端技術做UI的Python框架,內部使用Bottle Server框架,但不像flask能直接host在線上。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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模式)
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雷同。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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')
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頁面內容

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from django.http import HttpResponse
def Hello(request):
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頁面的網址

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 ]
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端可以呼叫。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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模式)
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檔案:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function hello(){
eel.print_js()()
}
function hello(){ eel.print_js()() }
function hello(){
    eel.print_js()()
}

Async方法使用

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
async function doA(){
var a=await eel.getnum()()
alert(a)
}
async function doA(){ var a=await eel.getnum()() alert(a) }
async function doA(){
    var a=await eel.getnum()()
    alert(a)

}

按下按鈕後,js端等待python處理結果後出現alert。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<!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>
<!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字串。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$("#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); });
$("#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可使用的格式。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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

範例: 將圖片轉灰階

視訊播放器
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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); }
eel.expose(SetImg);
function SetImg(newimg) {
  preview.src = "data:image/png;base64," + newimg
}

function Gray() {
  currentFile = preview.src;
  eel.toGray(currentFile);
}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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模式)
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傳回圖片

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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 端

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 直方圖
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", }, ], }, }); }
// 直方圖
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",
        },
      ],
    },

  });
}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@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)()
@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個顏色資料,我認為會卡的點在於繪製與自動尋找最小至最大值區間)。

等化功能

等化是將像素顏色拉得平均一點。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@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)()
@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字串。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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); // ***************** --- *********************
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寫出圖片

源圖與輸出圖。
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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(); }
// 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();
}

拖拉圖片

視訊播放器
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//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);
//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事件

在修改圖片時觸發事件,如此便能最後續處理,例如圖片縮圖僅需在圖片有更動時才須更新。

視訊播放器
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//修改圖片事件:
$(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) }
//修改圖片事件:
$(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圖的時候常會從通道中選一張二值化效果最好的通道做遮罩,不過目前本軟體的通道只能顯示、不能編輯(;´༎ຶД༎ຶ`)  (時間不夠..)。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@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)()
@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在地化解決方案應付小需求翻譯。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$.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(); }
$.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();
}
在地化效果。

簡易圖層

將兩張圖疊在一起
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<!-- 混和圖片 -->
<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>
<!-- 混和圖片 -->
        <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操作按鈕

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//疊圖層
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) }
//疊圖層
function OverlayerImage(){
  b64_str = realImage;
  layer_b64_str =newLayerImage.src ;
  opt=overlayMethod.value;
  SaveStep(b64_str);
  eel.overlayImage(b64_str,layer_b64_str,opt)

}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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)()
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倍

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 圖片轉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
# 圖片轉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

解決辦法:互相溝通檔案型態

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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("不支援檔案類型"); }
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("不支援檔案類型");
  }
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@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
@eel.expose
def setFileExtension(ext):
    global current_file_extension
    current_file_extension="."+ext

【補充】

Base64 轉法造成卡頓?

原本以為卡頓問題是反覆的base64轉碼造成的。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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拼錯字

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
cd src
python -m eel app.py web

---
python -m eel ./src/app.py ./src/web
分享
PHP Code Snippets Powered By : XYZScripts.com