用Python快速實現一個垃圾分類APP|附帶微信小程式

最近北京開始實行垃圾分類,導致大家對垃圾的研究熱度突然漲高,垃圾們也紛紛表示從來沒有獲得過這麼高的關注度。其實,上海市去年已經開始實行,網上已經有不少成熟的教程了,像什麼《垃圾分類從入門到精通》、《深入淺出垃圾分類》、《垃圾分類你應該掌握的10條基本原則》。這種教程如果我們親自去學顯然不符合程式設計師的個性,作為一個程式設計師,我們應該把這事兒交給機器來做,這樣才能省下更多的時間投入到996中。

扯了這麼多廢話,下面言歸正傳,今天這篇文章主要介紹如何利用現有的工具來實現一個垃圾分類的應用。這個想法是我昨天才有的,今天用了不到一天的時間就完成了,主要做了三個核心內容:

  • 對比現有垃圾分類服務,挑選一個合適並編碼實現
  • 開發桌面版垃圾分類APP
  • 開發垃圾分類微信小程式

上面這三部分第一部分是後端的活兒,其他兩部分都是前端的活兒,所以,我在這三塊沒有太多經驗,基本上是面向搜索引擎編程。雖然我的主業是做大數據的,但我確實想做這樣一個比較有意思的項目,畢竟一個不會後端的前端不是一個好的大數據工程師。

老規矩,先看效果圖,PC版:

小程式:

附上小程式二維碼,大家可以體驗一下。如果打開看不到效果可能審核沒通過,稍微晚點再開即可。

這篇文章會貼比較多的程式碼,並且公眾號閱讀起來不是很方便,所以文末我在文末會附上源碼的獲取方式。(公眾號回復關鍵字 垃圾分類 即可獲取整篇文章全部源碼)

那麼,接下來我們進入到具體的細節是如何做的。其實垃圾分類已經開始很長一段時間了,肯定會有一些服務商把垃圾分類的能力通過API的方式開放出來,供大家調用。我找了3家簡單對比下供大家參考:

  • 聚合數據(www.juhe.cn):提供文本、影像、語音分類。免費調用20次,定價不靈活只能批量購買
  • 天行數據(www.tianapi.com):提供文本、影像、語音分類。文本分類5000次,其他50次,定價按量計費
  • 京東AI開放平台:提供文本、影像、語音分類。免費,每日5000次

簡單對比了影像分類情況,聚合和天行數據明顯更好,再綜合定價因素最終我決定用天行數據
下面就來編寫程式碼,將API介面封裝成我們需要的服務,以文本(垃圾名稱)分類介面為例,請求的介面如下

//api.tianapi.com/txapi/lajifenlei/index?key=APIKEY&word=眼鏡

APIKEY需要到天行網站註冊來獲取,返回的結果如下:

{
  "code":200,
  "msg":"success",
  "newslist":[
    {
      "name":"隱形眼鏡",
      "type":3,
      "aipre":0,
      "explain":"干垃圾即其它垃圾,指除可回收物、有害垃圾、廚餘垃圾(濕垃圾)以外的其它生活廢棄物。",
      "contain":"常見包括磚瓦陶瓷、渣土、衛生間廢紙、貓砂、污損塑料、毛髮、硬殼、一次性製品、灰土、瓷器碎片等難以回收的廢棄物",
      "tip":"盡量瀝干水分;難以辨識類別的生活垃圾都可以投入干垃圾容器內"
    },
    {
      "name":"眼鏡",
      "type":3,
      "aipre":0,
      "explain":"干垃圾即其它垃圾,指除可回收物、有害垃圾、廚餘垃圾(濕垃圾)以外的其它生活廢棄物。",
      "contain":"常見包括磚瓦陶瓷、渣土、衛生間廢紙、貓砂、污損塑料、毛髮、硬殼、一次性製品、灰土、瓷器碎片等難以回收的廢棄物",
      "tip":"盡量瀝干水分;難以辨識類別的生活垃圾都可以投入干垃圾容器內"
    },
  ]
}

介面的欄位說明大家可以看官網文檔,這裡我就不再贅述了。下面來編寫請求文本分類介面的程式碼:

import base64
import requests


class TxApiService:
    def __init__(self):
        self.appkey = 'xxx'  # 需要換成自己的
        self.text_cls_url_root = '//api.tianapi.com/txapi/lajifenlei/index?key=%s&word=%s'
        self.img_cls_url_root = '//api.tianapi.com/txapi/imglajifenlei/index'

    def get_text_cls_res(self, garbage_name):
        url = self.text_cls_url_root % (self.appkey, garbage_name)
        response = requests.get(url)

        res = []
        if response.status_code == 200:
            res_json = response.json()
            if res_json.get('newslist'):
                new_list_json = res_json['newslist']
                for item in new_list_json:
                    name = item.get('name')
                    cat = self.garbage_id_to_name(item.get('type'))
                    tip = item.get('tip')
                    ai_pre = item.get('aipre')
                    pre_type = 'None'
                    if ai_pre == 0:
                        pre_type = '正常結果'
                    if ai_pre == 1:
                        pre_type = '預判結果'
                    item_dict = {'name': name, 'type': cat, 'tip': tip, 'pre_type': pre_type}
                    res.append(item_dict)
                return res
            else:
                return None
        return None

    def garbage_id_to_name(self, id):
        if id == 0:
            return '可回收物'
        if id == 1:
            return '有害垃圾'
        if id == 2:
            return '廚餘垃圾'
        if id == 3:
            return '其他垃圾'
        return None

程式碼比較簡單,用Python的requests庫請求垃圾分類介面,並對返回的數據格式化。
下面再來編寫請求影像分類的介面

def get_img_cls_res(self, img_base64):
    headers = {
        'Content-Type''application/x-www-form-urlencoded'
    }
    body = {
        'key': self.appkey,
        "img": img_base64,
    }
    response = requests.post(self.img_cls_url_root, headers=headers, data=body)

    res = []
    if response.status_code == 200:
        res_json = response.json()
        if res_json.get('newslist'):
            new_list_json = res_json['newslist']
            for item in new_list_json:
                name = item.get('name')
                cat = self.garbage_id_to_name(item.get('lajitype'))
                tip = item.get('lajitip')
                trust = item.get('trust')
                if trust <= 80:
                    continue
                item_dict = {'name': name, 'type': cat, 'tip': tip, 'pre_score': trust}
                res.append(item_dict)
            return res
        else:
            return None
    return None

函數的參數是影像的base64編碼,請求方式是POST請求,返回值欄位與文本分類略有不同,但思路是一樣的。這兩部分內容其實比簡單,這裡我就不再過多解釋了。

有了數據服務,下面我們就來開發GUI,這裡我用的是tkinter,用它編寫的APP可以運行在Linux、Windows和Mac系統,關於tkinter的使用這裡我不會做過多介紹,不了解的朋友自行百度,之前我也沒結果過基本上看網上的教程照貓畫虎。
首選,創建GarbageClassificationApp類,來定義用到的各種組件

import base64
import tkinter

from tkinter import *
import hashlib
import time
from tkinter import filedialog

from TxApiService import TxApiService

class GarbageClassificationApp:
    def __init__(self, tk):
        """
        初始化各個組件
        :param tk:
        """

        self.tk = tk

        # 第一行定義文本分類相關的組件
        self.text_cls_label = Label(self.tk, text="垃圾名:")
        self.garbage_name_text = Entry(self.tk)
        self.text_cls_button = \
            Button(self.tk, text="垃圾名分類", bg="lightblue", width=10, height=1, command=self.garbage_name_cls)

        # 第二行定義影像分類相關的組件
        self.img_cls_label = Label(self.tk, text="垃圾圖片:")
        self.select_file_button = Button(self.tk, text='選擇圖片', command=self.select_pic)
        self.img_cls_button = \
            Button(self.tk, text="圖片分類", bg="lightblue", width=10, height=1, command=self.garbage_img_cls)
        self.img_name_text = Text(self.tk, height=2)
        self.img_name_text.insert(1.0'未選擇圖片:')
        self.img_name_text['state'] = DISABLED

        # 第三行定義輸出結果相關的組件
        self.cls_result_label = Label(self.tk, text="分類結果:")
        self.output_cls_result_list_box = Listbox(self.tk, width=100, height=30)

        # 初始化 api 服務
        self.api_service = TxApiService()

        self.set_init_window()

再來創建set_init_window函數對各個組件進行布局

# 各組件布局
def set_init_window(self):
    self.tk.title("垃圾分類")
    self.tk.geometry('1068x681+350+200')  # 1068x681為窗口大小,+100 +100 定義窗口彈出時的默認展示位置

    # 第一行文本分類各組件的布局
    self.text_cls_label.grid(row=0, column=0, sticky=E)
    self.garbage_name_text.grid(row=0, column=1)
    self.text_cls_button.grid(row=0, column=2, padx=10)

    # 第二行影像分類各組件的布局
    self.img_cls_label.grid(row=1, column=0, sticky=E)
    self.select_file_button.grid(row=1, column=1)
    self.img_cls_button.grid(row=1, column=2, padx=10)
    self.img_name_text.grid(row=1, column=3, padx=10)

    # 第三行輸出結果各組件的布局
    self.cls_result_label.grid(row=2, column=0, rowspan=2, sticky=E)
    self.output_cls_result_list_box.grid(row=4, column=1, columnspan=10, pady=10, sticky=E)

這樣,介面就完成了。上面定義的一些組件中會有一些事件處理邏輯,比如一個按鈕Button被按下時,它就會調用commond屬性指定的函數。以文本分類Button為例(text_cls_button),用戶按下該按鈕後,程式就會執行garbage_name_cls函數,在該函數中我們就可以請求文本分類服務,並將返回的數據顯示到介面上。程式碼如下:

def garbage_name_cls(self):
    garbage_name = self.garbage_name_text.get()
    cat_arr = self.api_service.get_text_cls_res(garbage_name)
    self.output_cls_result_list_box.delete(0, END)

    if cat_arr:
        i = 0
        for item in cat_arr:
            name = '垃圾名稱: %s' % item.get('name''None')
            self.output_cls_result_list_box.insert(i, name)
            i += 1
            cat = '垃圾類別: %s' % item.get('type''None')
            self.output_cls_result_list_box.insert(i, cat)
            i += 1
            pre_type = '預判類型: %s' % item.get('pre_type''None')
            self.output_cls_result_list_box.insert(i, pre_type)
            i += 1
            tip = '投放提示: %s' % item.get('tip''None')
            self.output_cls_result_list_box.insert(i, tip)
            i += 1

            self.output_cls_result_list_box.insert(i, '')
            i += 1

其他事件處理邏輯類似,程式碼如下

def select_pic(self):
    """
    單選圖片
    :return:
    """

    file_name = filedialog.askopenfilename(
        filetypes=[('圖片', ('.png''.jpg''.jpeg'))])
    if file_name:
        self.img_name_text['state'] = NORMAL
        self.img_name_text.delete(1.0, END)
        self.img_name_text.insert(1.0'已選擇圖片:%s' % file_name)
        self.img_name_text['state'] = DISABLED

def garbage_img_cls(self):
    img_name_text = self.img_name_text.get(1.0, END)
    if img_name_text.startswith('已選擇圖片:'):
        file_path = img_name_text[6:].strip()
    else:
        return
    with open(file_path, 'rb'as f:
        base64_data = base64.b64encode(f.read())
        img_base64 = base64_data.decode()
    cat_arr = self.api_service.get_img_cls_res(img_base64)
    self.output_cls_result_list_box.delete(0, END)

    if cat_arr:
        i = 0
        for item in cat_arr:
            name = '垃圾名稱: %s' % item.get('name''None')
            self.output_cls_result_list_box.insert(i, name)
            i += 1
            cat = '垃圾類別: %s' % item.get('type''None')
            self.output_cls_result_list_box.insert(i, cat)
            i += 1
            pre_type = '預測得分: %s' % item.get('pre_score''None')
            self.output_cls_result_list_box.insert(i, pre_type)
            i += 1
            tip = '投放提示: %s' % item.get('tip''None')
            self.output_cls_result_list_box.insert(i, tip)
            i += 1

            self.output_cls_result_list_box.insert(i, '')
            i += 1

至此,PC端桌面APP就開發完成,這裡我們沒有實現語音分類服務,但思路是一樣的,大家可以嘗試一下。小程式的程式碼我就不貼了,我會一起放到源碼目錄中,在公眾號回復關鍵字 垃圾分類 即可獲取整篇文章全部源碼。今天開發這個小項目還是花了不少的時間,文章整理出來已經比較晚了,現在是凌晨1點左右,如果又不好理解的後者需要我深入講解的大家可以給我留言。另外,時間比較緊,所以APP做的比較挫,交互也比較差,有任何建議也歡迎大家留言。

歡迎公眾號「渡碼」,輸出別地兒看不到的乾貨。

Tags: