分類
發燒車訊

萬向李澤楷競買菲斯科 今日將出結果

據《華爾街日報》報導,美國電動汽車品牌菲斯科(Fisker)最終將於美國當地時間2月12日在紐約進行拍賣,美國法院表示會在一個工作日後宣判結果,李澤楷控股的混合動力技術控股有限公司與萬向集團仍是最有可能的競買成功者。

據路透社的消息,混合動力為加強此次競買工作以及公司的管理,特別聘請了曾主要負責福特歐洲業務的前高管Martin Leach。Leach表示,混合動力目前最大的困境是,萬向集團在一年前收購了A123系統公司,而A123是Fisker的主要電池供應商,不過混合動力可以得到另一家電池公司波士頓動力的支持。

美國汽車經銷協會首席經濟學家史蒂文表示,競買的最終贏家將獲得的不僅是Fisker的汽車產品或設計,還包括其知識產權,其中涉及的36項專利(大約一半為待定),包括電氣傳動系統、太陽能等專利技術。

據悉,萬向錢潮作為萬向集團控股的汽車零部件製造和銷售公司,其股價近日來也因受各種消息刺激連續上漲。

萬向錢潮並於昨(12)日發佈公告表示,從2000年開展電動汽車研發以來,控股股東萬向集團一直致力於發展清潔能源產業,但與國際先進技術相比尚有一定差距。為此,萬向集團希望通過併購聯合方式提升技術能力等。此前,萬向集團掌門人魯冠球曾表示,對同特斯拉電動車結盟合作抱開放態度。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

分類
發燒車訊

LeetCode 79,這道走迷宮問題為什麼不能用寬搜呢?

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是LeetCode專題第48篇文章,我們一起來看看LeetCode當中的第79題,搜索單詞(Word Search)。

這一題官方給的難度是Medium,通過率是34.5%,點贊3488,反對170。單從這份數據上來看,這題的質量很高,並且難度比之前的題目稍稍大一些。我個人覺得通過率是比官方給的題目難得更有參考意義的指標,10%到20%可以認為是較難的題,30%左右是偏難的題。50%是偏易題,所以如果看到某題標着Hard,但是通過率有50%,要麼說明題目很水,要麼說明數據很水,總有一點很水。

題意

廢話不多說,我們來看題意:

這題的題面挺有意思,給定一個二維的字符型數組,以及一個字符串,要求我們來判斷能否在二維數組當中找到一條路徑,使得這條路徑上的字符連成的字符串和給定的字符串相等?

樣例

board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

Given word = "ABCCED", return true.
Given word = "SEE", return true.
Given word = "ABCB", return false.

比如第一個字符串ABCCED,我們可以在數組當中找到這樣一條路徑:

題解

不知道大家看到題面和這個樣例有什麼樣的感覺,如果你刷過許多題,經常思考的話,我想應該不難發現,這道題的本質其實和走迷宮問題是一樣的。

我們拿到的這個二維的字符型數組就是一個迷宮, 我們是要在這個迷宮當中找一條“出路”。不過我們的目的不是找到終點,而是找到一條符合題意的路徑。在走迷宮問題當中,迷宮中不是每一個點都可以走的,同樣在當前問題當中,也不是每一個點都符合字符串的要求的。這兩個問題雖然題面看起來大相徑庭,但是核心的本質是一樣的。

我們來回憶一下,走迷宮問題應該怎麼解決?

這個答案應該已經非常確定了,當然是搜索算法。我們需要搜索解可能存在的空間去尋找存在的解,也就是說我們面臨的是一個解是否存在的問題,要麼找到解,要麼遍歷完所有的可能性發現解不存在。確定了是搜索算法之後,剩下的就簡單了,我們只有兩個選項,深度優先或者是廣度優先。

理論上來說,一般判斷解的存在性問題,我們使用廣度優先搜索更多,因為一般來說它可以更快地找到解。但是本題當中有一個小問題是,廣度優先搜索需要在隊列當中存儲中間狀態,需要記錄地圖上行走過的信息,每有一個狀態就需要存儲一份地圖信息,這會帶來比較大的內存開銷,同樣存儲的過程也會帶來計算開銷,在這道題當中,這是不可以接受的。拷貝狀態帶來的空間消耗還是小事,關鍵是拷貝帶來的時間開銷,就足夠讓這題超時了。所以我們別無選擇,只能深度優先。

明確了算法之後,只剩下了最後一個問題,在這個走迷宮問題當中,我們怎麼找到迷宮的入口呢?因為題目當中並沒有規定我們起始點的位置,這也不難解決,我們遍歷二維的字符數組,和字符串開頭相匹配的位置都可以作為迷宮的入口。

最後,我們來看代碼,並沒有什麼技術含量,只是簡單的回溯法而已。

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        fx = [[0, 1], [0, -1], [1, 0], [-1, 0]]
        def dfs(x, y, l):
            if l == len(word):
                return True
            for i in range(4):
                nx = x + fx[i][0]
                ny = y + fx[i][1]
                # 出界或者是走過的時候,跳過
                if nx < 0 or nx == n or ny < 0 or ny == m or visited[nx][ny]:
                    continue
                if board[nx][ny] == word[l]:
                    visited[nx][ny] = 1
                    if dfs(nx, ny, l+1):
                        return True
                    visited[nx][ny] = 0
            return False
                
        n = len(board)
        if n == 0:
            return False
        m = len(board[0])
        if m == 0:
            return False
        
        visited = [[0 for i in range(m)] for j in range(n)]
        
        for i in range(n):
            for j in range(m):
                # 找到合法的起點
                if board[i][j] == word[0]:
                    visited = [[0 for _ in range(m)] for _ in range(n)]
                    visited[i][j] = 1
                    if dfs(i, j, 1):
                        return True
                    
        return False

總結

如果能夠想通回溯法,並且對於回溯法的實現足夠熟悉,那麼這題的難度是不大的。實際上至今為止,我們一路刷來,已經做了好幾道回溯法的問題了,我想對你們來說,回溯法的問題應該已經小菜一碟了。

相比於回溯法來說,我覺得更重要的是我們能夠通過分析想清楚,為什麼廣度優先搜索不行,底層核心的本質原因是什麼。這個思考的過程往往比最後的結論來得重要。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

分類
發燒車訊

豐田、本田或於明年量產並銷售燃料電池車

行駛時不會排放二氧化碳的燃料電池車(FCV)在日本一直受到企業與政府的推崇與支持。FCV目前以租賃販售為主,但自2015年起,FCV將開始針對一般消費者、企業進行販售,可望進一步加快普及。

據日經新聞26日報導,本田汽車(Honda)將在2015年11月透過狹山工廠開始生產FCV,並將在2015年內於日美歐進行販售,年產量預估為1,000台、售價預估將壓在1,000萬日圓以下。

本田所將生產的FCV為5人座車款,且充飽一次燃料所能行駛的距離可達約500km、為現行電動車(EV)的2倍水準。

除了本田之外,豐田(Toyota)也將透過本社工廠生產FCV,年產量將同樣為1,000台、也同樣將在2015年內於日美歐開賣,且之後並計劃於2020年將年產量擴增至數萬台的規模。

豐田預計在2015年開賣的FCV售價將壓在1,000萬日圓以下,且之後並計劃於2020年代將售價壓低至300-500萬日圓的水準。

燃料電池車研發「三國鼎立」格局

豐田汽車於2013年1月宣布將攜手德國車廠BMW研發燃料電池車。

雷諾-日產聯盟(The Renault-Nissan Alliance)也於2013年1月宣布將攜手德國戴姆勒(Daimler)、美國福特汽車(Ford)研發燃料電池(FC)系統,以藉此大幅刪減投資成本,目標為在2017年開賣全球首款經濟實惠的量產款FCV。

另外,本田也於2013年7月宣布,將與美國汽車大廠通用汽車(General Motors;GM)攜手研發燃料電池車(FCV),而本田預計在2015年開賣的FCV就可能使用GM的技術。

日本政府補助建造燃料站

據華爾街日報去年12月26日的報導,日本政府宣布,2014年4月起的會計年度,將撥款72億日圓,補助建造氫燃料站;同時也將挹注64億日圓研發如何降低燃料電池的製造成本。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

分類
發燒車訊

中國正考慮降低電動汽車進口關稅

據中新網報道,中國政府正考慮進行電動汽車稅收改革。中國政府官員日前會見特斯拉(Tesla)首席執行官馬斯克(Musk Elon),均表示中國將支持電動汽車產業。

科技部部長萬鋼在北京會見馬斯克時表示,中國政府正在考慮電動汽車在稅收方面的改革,比如在進口關稅方面會有別於傳統汽車的進口,但具體細則現在還在制定之中。

工業和信息化部部長苗圩會見馬斯克時則表示,中國政府高度重視新能源汽車產業的發展,希望特斯拉公司發揮自身優勢,不斷創新,加強與中國企業的合作。

他同時指出,中國政府正在制定政策,幫助像特斯拉一樣的企業進入中國,促進電動汽車產業在中國的發展。

馬斯克亦表示,特斯拉公司非常重視中國市場,願加強與中國企業的合作。特斯拉已經在北京與上海建設充電站,電力來源為光伏與電網的結合,可實現24小時不間斷充電。」特斯拉未來還計畫建立超級充電網路。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

分類
發燒車訊

Linux Systemd 詳細介紹: Unit、Unit File、Systemctl、Target

Systemd

簡介

CentOS 7 使用 Systemd 替換了SysV

Ubuntu 從 15.04 開始使用 Systemd

Systemd 是 Linux 系統工具,用來啟動守護進程,已成為大多數發行版的標準配置

特點

優點:

  1. 按需啟動進程,減少系統資源消耗

  2. 并行啟動進程,提高系統啟動速度

    在 SysV-init 時代,將每個服務項目編號,依次執行啟動腳本。Ubuntu 的 Upstart 解決了沒有直接依賴的啟動之間的并行啟動。而 Systemd 通過 Socket 緩存、DBus 緩存和建立臨時掛載點等方法進一步解決了啟動進程之間的依賴,做到了所有系統服務併發啟動。對於用戶自定義的服務,Systemd 允許配置其啟動依賴項目,從而確保服務按必要的順序運行。

    SystemV Upstart 參考上一篇博文:Linux 初始化系統 SystemV Upstart

  3. 使用 CGroup 監視和管理進程的生命周期

    CGroup 提供了類似文件系統的接口,當進程創建子進程時,子進程會繼承父進程的 CGroup。因此無論服務如何啟動新的子進程,所有的這些相關進程都會屬於同一個 CGroup

    在 Systemd 之前的主流應用管理服務都是使用 進程樹 來跟蹤應用的繼承關係的,而進程的父子關係很容易通過 兩次 fork 的方法脫離。

    而 Systemd 則提供通過 CGroup 跟蹤進程關係,引補了這個缺漏。通過 CGroup 不僅能夠實現服務之間訪問隔離,限制特定應用程序對系統資源的訪問配額,還能更精確地管理服務的生命周期

  4. 統一管理服務日誌

  5. 支持快照和系統恢復

缺點:

  1. 過於複雜,與操作系統的其他部分強耦合,違反”keep simple, keep stupid”的Unix 哲學

架構圖

Unit(單元|服務)

Systemd 可以管理所有系統資源:

  1. 將系統資源劃分為12類
  2. 將每個系統資源稱為一個 Unit。Unit 是 Systemd 管理系統資源的基本單位
  3. 使用一個 Unit File 作為 Unit 的單元文件,Systemd 通過單元文件控制 Unit 的啟動

例如,MySQL服務被 Systemd 視為一個 Unit,使用一個 mysql.service 作為啟動配置文件

Unit File(單元文件|配置文件)

單元文件中包含該單元的描述、屬性、啟動命令等

類型

Systemd 將系統資源劃分為12類,對應12種類型的單元文件

系統資源類型 單元文件擴展名 單元文件描述
Service .service 封裝守護進程的啟動、停止、重啟和重載操作,是最常見的一種 Unit 文件
Target .target 定義 target 信息及依賴關係,一般僅包含 Unit 段
Device .device 對於 /dev 目錄下的硬件設備,主要用於定義設備之間的依賴關係
Mount .mount 定義文件系統的掛載點,可以替代過去的 /etc/fstab 配置文件
Automount .automount 用於控制自動掛載文件系統,相當於 SysV-init 的 autofs 服務
Path .path 用於監控指定目錄或文件的變化,並觸發其它 Unit 運行
Scope .scope 這種 Unit 文件不是用戶創建的,而是 Systemd 運行時產生的,描述一些系統服務的分組信息
Slice .slice 用於表示一個 CGroup 的樹
Snapshot .snapshot 用於表示一個由 systemctl snapshot 命令創建的 Systemd Units 運行狀態快照,可以切回某個快照
Socket .socket 監控來自於系統或網絡的數據消息
Swap .swap 定義一個用戶做虛擬內存的交換分區
Timer .timer 用於配置在特定時間觸發的任務,替代了 Crontab 的功能

對於操作單元文件的命令,如果缺省擴展名,則默認.service擴展名

而操作 target 的命令,例如 isolate,則默認.target擴展名

語法

單元文件的語法來源於 XDG桌面入口配置文件.desktop文件

Unit 文件可以分為三個配置區段:

  • Unit 段:所有 Unit 文件通用,用來定義 Unit 的元數據,以及配置與其他 Unit 的關係
  • Install 段:所有 Unit 文件通用,用來定義如何啟動,以及是否開機啟動
  • Service 段:服務(Service)類型的 Unit 文件(後綴為 .service)特有的,用於定義服務的具體管理和執行動作

單元文件中的區段名和字段名大小寫敏感

每個區段內都是一些等號連接的鍵值對(鍵值對的等號兩側不能有空格)

Unit 段

主要字段如下:

  • Description:當前服務的簡單描述

  • Documentation:文檔地址,可以是一個或多個文檔的 URL 路徑

    【依賴關係】

  • Requires:與其它 Unit 的強依賴關係,如果其中任意一個 Unit 啟動失敗或異常退出,當前 Unit 也會被退出

  • Wants:與其它 Unit 的弱依賴關係,如果其中任意一個 Unit 啟動失敗或異常退出,不影響當前 Unit 繼續執行

    只涉及依賴關係,默認情況下 兩個 Unit 同時啟動

    【啟動順序】

  • After:該字段指定的 Unit 全部啟動完成以後,才會啟動當前 Unit

  • Before:該字段指定的 Unit 必須在當前 Unit 啟動完成之後再啟動

    只涉及啟動順序,不影響啟動結果和運行情況

  • Binds To:與 Requires 相似,該字段指定的 Unit 如果退出,會導致當前 Unit 停止運行

  • Part Of:一個 Bind To 作用的子集,僅在列出的 Unit 失敗或重啟時,終止或重啟當前 Unit,而不會隨列出Unit 的啟動而啟動

http://manpages.ubuntu.com/manpages/bionic/en/man5/systemd.unit.5.html

Install 段

主要字段如下:

  • WantedBy:它的值是一個或多個 target,執行enable命令時,符號鏈接會放入/etc/systemd/system目錄下以 target 名 + .wants後綴構成的子目錄中
  • RequiredBy:它的值是一個或多個 target,執行enable命令時,符號鏈接會放入/etc/systemd/system目錄下以 target 名 + .required後綴構成的子目錄中
  • Alias:當前 Unit 可用於啟動的別名
  • Also:當前 Unit 被 enable/disable 時,會被同時操作的其他 Unit

http://manpages.ubuntu.com/manpages/bionic/en/man5/systemd.unit.5.html

Service 段

主要字段如下:

【啟動類型】

  • Type:定義啟動時的進程行為。它有以下幾種值。
    • Type=simple:默認值,ExecStart字段啟動的進程為主進程
      • 服務進程不會 fork,如果該服務要啟動其他服務,不要使用此類型啟動,除非該服務是 socket 激活型
    • Type=forkingExecStart字段將以fork()方式從父進程創建子進程啟動,創建後父進程會立即退出,子進程成為主進程。
      • 通常需要指定PIDFile字段,以便 Systemd 能夠跟蹤服務的主進程
      • 對於常規的守護進程(daemon),除非你確定此啟動方式無法滿足需求,使用此類型啟動即可
    • Type=oneshot:只執行一次,Systemd 會等當前服務退出,再繼續往下執行
      • 適用於只執行一項任務、隨後立即退出的服務
      • 通常需要指定RemainAfterExit=yes字段,使得 Systemd 在服務進程退出之後仍然認為服務處於激活狀態
    • Type=dbus:當前服務通過 D-Bus 信號啟動。當指定的 BusName 出現在 DBus 系統總線上時,Systemd認為服務就緒
    • Type=notify:當前服務啟動完畢會發出通知信號,通知 Systemd,然後 Systemd 再啟動其他服務
    • Type=idle:Systemd 會等到其他任務都執行完,才會啟動該服務。
      • 一種使用場合是:讓該服務的輸出,不與其他服務的輸出相混合

【啟動行為】

  • ExecStart:啟動當前服務的命令

    ExecStart=/bin/echo execstart1
    ExecStart=
    ExecStart=/bin/echo execstart2
    

    順序執行設定的命令,把字段置空,表示清除之前的值

  • ExecStartPre:啟動當前服務之前執行的命令

  • ExecStartPost:啟動當前服務之後執行的命令

  • ExecReload:重啟當前服務時執行的命令

  • ExecStop:停止當前服務時執行的命令

  • ExecStopPost:停止當前服務之後執行的命令

  • RemainAfterExit:當前服務的所有進程都退出的時候,Systemd 仍認為該服務是激活狀態

    • 這個配置主要是提供給一些並非常駐內存,而是啟動註冊后立即退出,然後等待消息按需啟動的特殊類型服務使用的
  • TimeoutSec:定義 Systemd 停止當前服務之前等待的秒數

    注:所有的啟動設置之前,都可以加上一個連詞號(-),表示”抑制錯誤”,即發生錯誤的時候,不影響其他命令的執行。比如,EnvironmentFile=-/etc/sysconfig/sshd(注意等號後面的那個連詞號),就表示即使/etc/sysconfig/sshd文件不存在,也不會拋出錯誤。

【重啟行為】

  • RestartSec:Systemd 重啟當前服務間隔的秒數
  • KillMode:定義 Systemd 如何停止服務,可能的值包括:
    • control-group(默認值):當前控制組裡面的所有子進程,都會被殺掉
    • process:只殺主進程(sshd 服務,推薦值)
    • mixed:主進程將收到 SIGTERM 信號,子進程收到 SIGKILL 信號
    • none:沒有進程會被殺掉,只是執行服務的 stop 命令。
  • Restart:定義何種情況 Systemd 會自動重啟當前服務,可能的值包括:
    • no(默認值):退出后不會重啟
    • on-success:只有正常退出時(退出狀態碼為0),才會重啟
    • on-failure:非正常退出時(退出狀態碼非0),包括被信號終止和超時,才會重啟(守護進程,推薦值)
    • on-abnormal:只有被信號終止和超時,才會重啟(對於允許發生錯誤退出的服務,推薦值)
    • on-abort:只有在收到沒有捕捉到的信號終止時,才會重啟
    • on-watchdog:超時退出,才會重啟
    • always:不管是什麼退出原因,總是重啟

【上下文】

  • PIDFile:指向當前服務 PID file 的絕對路徑。

  • User:指定運行服務的用戶

  • Group:指定運行服務的用戶組

  • EnvironmentFile:指定當前服務的環境參數文件。該文件內部的key=value鍵值對,可以用$key的形式,在當前配置文件中獲取

    啟動sshd,執行的命令是/usr/sbin/sshd -D $OPTIONS,其中的變量$OPTIONS就來自EnvironmentFile字段指定的環境參數文件。

http://manpages.ubuntu.com/manpages/bionic/en/man5/systemd.service.5.html

佔位符

在 Unit 文件中,有時會需要使用到一些與運行環境有關的信息,例如節點 ID、運行服務的用戶等。這些信息可以使用佔位符來表示,然後在實際運行中動態地替換為實際的值。

詳細了解見 https://cloud.tencent.com/developer/article/1516125

模板

在現實中,往往有一些應用需要被複制多份運行,就會用到模板文件

模板文件的寫法與普通單元文件基本相同,只是模板文件名是以 @ 符號結尾。例如:apache@.service

通過模板文件啟動服務實例時,需要在其文件名的 @ 字符後面附加一個用於區分服務實例的參数字符串,通常這個參數是用於監控的端口號或控制台 TTY 編譯號

systemctl start apache@8080.service

Systemd 在運行服務時,首先尋找跟單元名完全匹配的單元文件,如果沒有找到,才會嘗試選擇匹配模板

例如上面的命令,System 首先會在約定的目錄下尋找名為 apache@8080.service 的單元文件,如果沒有找到,而文件名中包含 @ 字符,它就會嘗試去掉後綴參數匹配模板文件。對於 apache@8080.service,Systemd 會找到 apache@.service 模板文件,並通過這個模板文件將服務實例化。

詳細了解見 https://cloud.tencent.com/developer/article/1516125

狀態

systemctl list-unit-files 將會列出文件的 state,包括 static, enabled, disabled, masked, indirect

  • masked

    service軟鏈接到/dev/null

    該單元文件被禁止建立啟動鏈接

  • static

    該單元文件沒有[Install]部分(無法執行),只能作為其他配置文件的依賴

  • enabled

    已建立啟動鏈接

  • disabled

    沒建立啟動鏈接

https://askubuntu.com/a/731674

示例

  1. 關掉觸摸板配置文件

    Unit]
    Description=Switch-off Touchpad
    
    [Service]
    Type=oneshot
    ExecStart=/usr/bin/touchpad-off start
    ExecStop=/usr/bin/touchpad-off stop
    RemainAfterExit=yes
    
    [Install]
    WantedBy=multi-user.target
    
    • oneshot 表明這個服務只要運行一次就夠了,不需要長期運行
    • RemainAfterExit字段設為yes,表示進程退出以後,服務仍然保持執行。這樣的話,一旦使用systemctl stop命令停止服務,ExecStop指定的命令就會執行,從而重新開啟觸摸板

Systemd 內建命令

systemd-analyze

Analyze and debug system manager, If no command is passed, Systemd-analyze time is implied

https://www.freedesktop.org/software/systemd/man/systemd-analyze.html

systemd-analyze time

查看初始化耗時

systemd-analyze blame

打印所有運行單元,按它們初始化的時間排序。此信息可用於優化啟動時間。注意,輸出可能具有誤導性,因為一個服務的初始化可能非常緩慢,因為它等待另一個服務的初始化完成

systemd-run

將一個指定的服務變成後台服務

未測試

參考 https://www.freedesktop.org/software/systemd/man/systemd-run.html

systemctl 系統服務管理命令

systemctl是 Systemd 的主命令,用於管理系統

與 service 命令的區別

  1. systemctl 融合了 service 和 chkconfig 的功能
  2. 在 Ubuntu18.04 中沒有自帶 chkconfig 命令;service 命令實際上重定向到 systemctl 命令
動作 SysV Init 指令 Systemd 指令
啟動某服務 service httpd start systemctl start httpd
停止某服務 service httpd stop systemctl stop httpd
重啟某服務 service httpd restart systemctl restart httpd
檢查服務狀態 service httpd status systemctl status httpd
刪除某服務 chkconfig –del httpd 停掉應用,刪除其配置文件
使服務開機自啟動 chkconfig –level 5 httpd on systemctl enable httpd
使服務開機不自啟動 chkconfig –level 5 httpd off systemctl disable httpd
查詢服務是否開機自啟 chkconfig –list | grep httpd systemctl is-enabled httpd
加入自定義服務 chkconfig –add test systemctl load test
显示所有已啟動的服務 chkconfig –list systemctl list-unit-files | grep enabled

參數

--all

显示加載到內存的所有單元

--type

-t --type=

显示指定類型(12種類型)的單元

--state

--state=

显示指定狀態的單元或單元文件

  • 單元狀態

    輸入 systemctl list-units --stateTab鍵,显示所有可用的值

  • 單元文件狀態

    另外還可以用 enabled static disabled 等systemctl list-unit-files 显示的狀態

--failed

--state=failed

显示加載失敗的單元

 systemctl --failed
--version

打印 Systemd 版本

lfp@legion:/lib/systemd/system$ systemctl --version
Systemd 237
+PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD -IDN2 +IDN -PCRE2 default-hierarchy=hybrid

單元命令

我的理解

  • systemd 對單元的管理,不涉及單元文件自身屬性和內容
list-units

相當於systemctl

列出當前已加載的單元(內存)

默認情況下僅显示處於激活狀態(正在運行)的單元

UNIT 單元名

LOAD 加載狀態

ACTIVE SUB 執行狀態(大狀態 子狀態)

DESCRIPTION 描述

start

啟動單元

systemctl start mysql.service
stop

停止單元

systemctl stop mysql.service
kill

殺掉單元進程

systemctl kill mysql.service
reload

不終止單元,重新加載 針對該單元的 運行配置文件,而不是 針對 systemd的 該單元的啟動配置文件

例如啟動 MySQL 服務,reload 可以在不停止服務的情況下重載 MySQL 的配置文件 my.cnf

restart

重啟單元

該單元在重啟之前擁有的資源不會被完全清空,比如文件描述符存儲設施

systemctl reload mysql.service
status

status [unit | PID]

显示單元或進程所屬單元的運行信息

systemctl status mysql.service

Loaded行:配置文件的位置,是否設為開機啟動

Active行:表示正在運行

Main PID行:主進程ID

CGroup塊:應用的所有子進程

日誌塊:應用的日誌

is-active

判斷指定的單元是否處於激活狀態

# 默認會打印當前單元的狀態,可以通過 --quiet 參數取消打印
lfp@legion:~$ systemctl is-active mysql
active
is-failed

判斷指定的單元是否處於啟動失敗狀態

lfp@legion:~$ systemctl is-failed mysql
active
list-dependencies

查看單元之間的依賴關係

systemctl list-dependencies graphical.target 
systemctl list-dependencies mysql.service
show

show --property= Unit <==> show -p Unit

显示單元所有底層參數

lfp@legion:~$ systemctl show -p MainPID mysql
MainPID=1061
set-property

在單元啟動的時候設置運行時的某個屬性,立即生效,並保存在磁盤中作為啟動配置

如果添加了--runtime則重啟后失效

並非所有的屬性都可以設置,只是 systemd.resource-control 包含的屬性

isolate

切換到某個 target(系統狀態),立即停止該 target 未包含的單元進程。也可以理解為切換 runlevel

如果沒有指定擴展名,則默認.target

只有當.target單元文件中的AllowIsolate=yes時,才能使用 isolate 切換;也可以用IgnoreOnIsolate=yes字段來拒絕使用 isolate 切換

systemctl isolate multi-user.target
cat

显示單元配置文件的備份文件,包括插入式配置drop-ins,可以完整的看到單元服務的配置。注意這裏打印的依據是磁盤的上內容,如果用戶修改了配置文件(磁盤已修改)但是未執行daemon-reload命令(內存中未更新),那麼該命令显示的配置和實際執行情況有出入

lfp@legion:~$ systemctl cat mysql.service 
# /lib/systemd/system/mysql.service
# MySQL Systemd service file

[Unit]
Description=MySQL Community Server
...

[Install]
WantedBy=multi-user.target

[Service]
Type=forking
...
# 這段就显示的是 插入式配置 drop-in 的內容
# /etc/systemd/system/mysql.service.d/mysql.conf
# MySQL Systemd service file

[Unit]
# Description=MySQL Community Server conf

[Service]
# ExecStartPost=/home/lfp/bin/espeak.sh

單元文件命令

我的理解

  • systemd 對單元文件自身屬性和內容的管理
list-unit-files

列出所有已安裝的單元文件和它們的啟用狀態

list-units的區別是

  • list-units 僅显示當前已加載到內存中的單元
  • list-unit-files 會讀取單元文件內容,列出所有單元,包括存在於硬盤未加載進內存的單元

實際測試結果:

systemctl list-unit-files 显示“348 Unit files listed”

systemctl list-units –all 显示“405 loaded units listed”

systemctl list-units 显示 “232 loaded units listed”

enable

使某個單元開機自啟動

這會根據單元文件內容中的[Install]指定的 target 組,創建一個軟鏈接

lfp@legion:/etc/systemd/system$ vim v2rayL.service
# 文件內容 [Install] 段
......
[Install]
WantedBy=multi-user.target
......

lfp@legion:/etc/systemd/system$ systemctl is-enabled v2rayL.service 
disabled
lfp@legion:/etc/systemd/system$ systemctl enable v2rayL.service 
# 根據 [Install] 段指定的組,添加軟鏈接
Created symlink /etc/systemd/system/multi-user.target.wants/v2rayL.service → /etc/systemd/system/v2rayL.service.
lfp@legion:/etc/systemd/system$ systemctl is-enabled v2rayL.service 
enabled
disable

取消某個單元開機自啟動設置,刪除軟鏈接

這會刪除所有指向該單元文件的軟鏈接,不僅僅是 enable 操作創建的

reenable

disable 和 enable 的結合,根據單元文件內容中的 [Install] 段,重置軟鏈接

is-enabled

檢查某個單元是否是開機自啟動的(建立的啟動鏈接)

lfp@legion:~$ systemctl is-enabled mysql
enabled
get-default

獲取默認啟動 target,default-target 是指向該 target 的軟鏈接

set-default

設置默認啟動 target,同時修改 default-target 指向設定的 target

systemctl set-default multi-user.target

生命周期管理命令

daemon-reload

重新加載所有的單元文件和依賴關係

對單元文件有修改的時候,需要執行該命令重新加載文件內容

系統管理命令

reboot
systemctl reboot

重啟系統(異步操作)

it will return after the reboot operation is enqueued, without waiting for it to complete

poweroff

關閉系統,切斷電源(異步操作)

halt

僅CPU停止工作,其他硬件仍處於開機狀態(異步操作)

suspend

暫停系統(異步操作)

將觸發執行suspend.target

hibernate

讓系統進入冬眠狀態(異步操作)

將觸發執行hibernate.target

目錄、文件

/run/systemd/system/

單元(服務)運行時生成的配置文件所在目錄
/etc/systemd/system/

系統或用戶自定義的配置文件,初始化過程中Systemd只執行/etc/systemd/system目錄裏面的配置文件

/lib/systemd/system/

軟件安裝時添加的配置文件,類似於 /etc/init.d/

對於支持 Systemd 的程序,安裝的時候,會自動的在 /lib/systemd/system 目錄添加一個配置文件

其他目錄都是軟鏈接

/etc/systemd/system/default.target

Systemd 執行的第一個單元文件,符號鏈接到默認啟動 target 對應的 .target 單元文件

優先級

SysV 的啟動腳本放在/etc/init.d目錄下

Systemd 的單元文件放在/etc/systemd/system/lib/systemd/system目錄下

當一個程序在3個目錄下都存在啟動方式時,優先級是/etc/systemd/system --> /lib/systemd/system --> /etc/init.d

lfp@legion:/etc/init.d$ ll
-rwxr-xr-x   1 root root  5650 5月  19 22:09 mysql*

lfp@legion:/etc/systemd/system$ ll
-rw-r--r--  1 root root  511 5月  20 01:42  mysql.service

lfp@legion:/lib/systemd/system$ ll
-rw-r--r--  1 root root   499 5月  20 01:20  mysql.service
  • /etc/systemd/system 裏面的同名service會覆蓋/lib/systemd/system 裏面的

    注意查看文件信息,該同名文件不能是指向 /lib/systemd/system 的軟鏈接

    軟鏈接不會覆蓋而會同步

  • 如果某個程序不存在Systemd 單元文件,那麼會執行/etc/init.d裏面的啟動腳本

根據啟動過程, /etc/systemd/system/multi-user.target.wants/ 目錄下是很多指向 /lib/systemd/system/目錄的軟鏈接,所以兩個目錄下的單元文件會互相同步。

如果/etc/systemd/system//etc/systemd/system/multi-user.target.wants/ 同時存在單元文件,測試發現,不管是手動啟動還是開機自啟動,使用的都是 /etc/systemd/system/ 目錄下的service單元文件

測試
執行/etc/init.d目錄下的腳本

mysql 修改 mysql.servicemysql.service.bak 然後通過service mysql restart啟動/etc/init.d/mysql腳本

下面是啟動后的一些信息

注:在恢復mysql.service之前,需要先通過service mysql stop 利用/etc/init.d/mysql腳本中的stop結束上面的進程,否則一旦恢復,service mysql stop 執行的操作就不是 /etc/init.d/mysql腳本中的stop,無法結束上面的進程,出現命令無法正常執行的情況

結束上面的進程,恢復mysql.service,重新啟動

/etc/systemd/system 覆蓋測試

未修改前,查看MySQL的狀態

lfp@legion:~$ service mysql status
● mysql.service - MySQL Community Server
	# 可以發現這裏的 mysql.service 是在 /lib/systemd/system 下面
   Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: en
   Active: active (running) since Sat 2020-04-25 18:34:30 CST; 5h 33min ago
 Main PID: 988 (mysqld)
    Tasks: 28 (limit: 4915)
   CGroup: /system.slice/mysql.service
           └─988 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid

4月 25 18:34:30 legion Systemd[1]: Starting MySQL Community Server...
4月 25 18:34:30 legion Systemd[1]: Started MySQL Community Server.

將 /lib/systemd/system 下面的文件複製到 /etc/systemd/system/ 下面

sudo cp /lib/systemd/system/mysql.service /etc/systemd/system/

修改 mysql.service

sudo vim /etc/systemd/system/mysql.service

重啟 mysql.service ,系統提示需要重新加載

lfp@legion:~$ systemctl restart mysql.service 
Warning: The Unit file, source configuration file or drop-ins of mysql.service changed on disk. 
Run 'systemctl daemon-reload' to reload units.
lfp@legion:~$ systemctl daemon-reload	#  重新加載
lfp@legion:~$ systemctl restart mysql.service # 重啟
lfp@legion:~$ systemctl status mysql.service 
● mysql.service - MySQL Community Server hahahaha	# 發現這裡是修改之後的,覆蓋了 /lib/systemd/lib 中的
#                                 這裏也可以看到加載路徑
   Loaded: loaded (/etc/systemd/system/mysql.service; enabled; vendor preset: en
   Active: active (running) since Sun 【2020-04-26】 00:47:02 CST; 5s ago
  Process: 21590 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/m
  Process: 21581 ExecStartPre=/usr/share/mysql/mysql-Systemd-start pre (code=exi
 Main PID: 21592 (mysqld)
    Tasks: 27 (limit: 4915)
   CGroup: /system.slice/mysql.service
           └─21592 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pi

4月 26 00:47:02 legion Systemd[1]: Starting MySQL Community Server hahahaha...
4月 26 00:47:02 legion Systemd[1

Target

兩個含義

  1. 系統的某個狀態稱為一個 target(類似於”狀態點”)

  2. 達到某個系統狀態,所需的一個或多個資源(Unit)稱為一個 target(一個 Unit 組)

    1. target是一個抽象的系統資源,不像MySQL有實體

    2. 如果一個target只包含一個Unit,那麼該 target,沒有對應的目錄,指的就是這個 Unit

      例如 hibernate.target 只包含 systemd-hibernate.service一個Unit

      如果一個target包含多個Unit,那麼該target,有對應的 xxx.target.wants 目錄,指的是目錄裏面所有的Unit

      例如 multi-user.target 包含位於/etc/systemd/system/multi-user.target.wants目錄下的多個 Unit

target也是一個 Target 類型的系統資源,有對應的單元文件 xxx.target

Systemd 使用 target 來劃分和管理資源(Unit),啟動(激活)某個 xxx.target 單元文件,通過執行該 target 包含的 Unit,使系統達到某種狀態

對於狀態點的理解:

例如,執行systemd suspend命令讓系統暫停,會觸發啟動suspend.target,然後執行裏面的systemd-suspend.service Unit,使系統達到一個暫停的狀態

傳統的init啟動模式裏面,有 RunLevel 的概念,跟 Target 的作用很類似。不同的是,RunLevel 是互斥的,不可能多個 RunLevel 同時啟動,但是多個 Target 可以同時啟動

啟動 target

runlevel是 SysV init 初始化系統中的概念,在Systemd初始化系統中使用的是 Target,他們之間的映射關係是

Runlevel Target 說明
0 poweroff.target 關閉系統
1 rescue.target 維護模式
2,3,4 multi-user.target 多用戶,無圖形系統(命令行界面)
5 graphical.target 多用戶,圖形化系統(圖形用戶界面)
6 reboot.target 重啟系統

啟動過程

  1. 讀入 /boot 目錄下的內核文件

  2. 內核文件加載完之後,開始執行第一個程序/sbin/init 初始化進程,由 Systemd 初始化系統引導,完成相關的初始化工作

  3. Systemd 執行default.target ,獲知設定的啟動 target

    實際上 default.target 是指向設定的啟動 target 的軟鏈接

  4. Systemd 執行啟動 target 對應的單元文件。根據單元文件中定義的依賴關係,傳遞控制權,依次執行其他 target 單元文件,同時啟動每個 target 包含的單元

    對於圖形化界面,默認 target 是 graphical,Systemd 執行位於/lib/systemd/system/ 目錄下的 graphical.target 單元文件,根據 target 單元文件中定義的依賴關係,依次啟動其他 target 單元文件以及各個 target 包含的位於/etc/systemd/system/目錄下的單元

    例如: graphical.target 的依賴關係是

    [Unit]
    Description=Graphical Interface
    Documentation=man:systemd.special(7)
    Requires=multi-user.target #
    Wants=display-manager.service #
    Conflicts=rescue.service rescue.target
    After=multi-user.target rescue.service rescue.target display-manager.service #
    AllowIsolate=yes
    

    因此,依次啟動 multi-user.target –> basic.target –> sysinit.target –> local-fs.target –>local-fs-pre.target –> …

    同時啟動每個 target 包含的位於/etc/systemd/system/目錄下的Unit

    SysV對應的 rc5.d –> /etc/init.d 目錄下的指定的腳本就不會在開機的時候執行了

查看默認 target

systemctl get-default

lfp@legion:~$ runlevel
N 5
lfp@legion:~$ systemctl get-default
graphical.target

修改默認 target

systemctl set-default [xxx.target]

# Ubuntu18.04
# 圖形用戶界面 切換 命令行界面
sudo systemctl set-default multi-user.target
# 命令行界面 切換 圖形用戶界面
 systemctl set-default graphical.target
 reboot
 # 命令行界面 想進入 圖形用戶界面(僅進入一次,重啟系統后仍然會進入命令行模式)
 sudo systemctl start lightdm

https://ask.csdn.net/questions/695344

https://askubuntu.com/a/788465

其他操作

修改配置文件

  1. 直接修改/lib/systemd/system目錄下的單元文件

    如果軟件包更新,修改會被丟棄

  2. /lib/systemd/system中的單元文件複製到/etc/systemd/system/

    如果軟件包更新,不會同步更新

  3. /etc/systemd/system/ 中添加配置(推薦)

添加配置

步驟:

  1. /etc/systemd/system/ 目錄下新建<單元名>.d目錄
  2. <單元名>.d目錄下,新建<單元名>.conf文件
  3. <單元名>.conf文件中修改配置

測試:

  1. 創建目錄及文件

    # /mysql.service.d
    lfp@legion:/etc/systemd/system/mysql.service.d$ ls
    mysql.conf
    
  2. 修改配置

    # MySQL Systemd service config file
    # 不需要所有的組,僅添加需要修改的組及選項
    
    [Unit]
    Description=MySQL Community Server config test
    
    [Service]
    ExecStartPost=/home/lfp/bin/espeak.sh
    
  3. 重啟測試

    lfp@legion:/etc/systemd/system/mysql.service.d$ systemctl daemon-reload
    lfp@legion:/etc/systemd/system/mysql.service.d$ systemctl restart mysql.service
    lfp@legion:/etc/systemd/system/mysql.service.d$ systemctl status mysql.service
    lfp@legion:/etc/systemd/system/mysql.service.d$ systemctl status mysql.service
    #                                                                            描述已經被修改
    ● mysql.service - MySQL Community Server config test
       Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled)
    # 加入了配置文件
      Drop-In: /etc/systemd/system/mysql.service.d
               └─mysql.conf
       Active: active (running) since Thu 2020-05-21 20:18:02 CST; 12min ago
    # 新增的動作執行成功
      Process: 4703 ExecStartPost=/home/lfp/bin/espeak.sh (code=exited, status=0/SUCCESS)
      Process: 4672 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid (code=exited, status=0/SUCCESS)
      Process: 4663 ExecStartPre=/usr/share/mysql/mysql-Systemd-start pre (code=exited, status=0/SUCCESS)
     Main PID: 4674 (mysqld)
        Tasks: 27 (limit: 4915)
       CGroup: /system.slice/mysql.service
               └─4674 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid
    
    5月 21 20:18:02 legion espeak.sh[4703]: ALSA lib pcm_route.c:867:(find_matching_chmap) Found no matching channel map
    

添加開機啟動服務

  1. 添加啟動配置文件
  2. 通過 rc.local文件

/lib/systemd/rc.local.service

# 如果存在,就自動添加到 multi-user.target
# This Unit gets pulled automatically into multi-user.target by
# Systemd-rc-local-generator if /etc/rc.local is executable.
[Unit]
Description=/etc/rc.local Compatibility
Documentation=man:systemd-rc-local-generator(8)
ConditionFileIsExecutable=/etc/rc.local
After=network.target

[Service]
Type=forking
ExecStart=/etc/rc.local start
TimeoutSec=0
RemainAfterExit=yes
GuessMainPID=no

創建 rc.local 文件,賦予可執行權限,即可添加啟動命令

sudo touch /etc/rc.local
chmod 755 /etc/rc.local

lfp@legion:/etc$ vim rc.local
#!/bin/sh -e
echo "test rc.local" > /usr/local/rclocal.log
echo "rc.local `date +%Y-%m-%d-%H:%M:%S`" >/home/lfp/log/rclocal.log
exit 0

system 工具集

hostnamectl 主機名管理命令

hostnamectl
hostnamectl status

Show current system hostname and related information

lfp@legion:/lib/systemd/system$ hostnamectl status
   Static hostname: legion
         Icon name: computer-laptop
           Chassis: laptop
        Machine ID: b28xxxxxxxx2ecafa29e
           Boot ID: 21xxxxxxxxxxxx1d3a47504d
  Operating System: Ubuntu 18.04.4 LTS
            Kernel: Linux 5.3.0-51-generic
      Architecture: x86-64

journalctl 日誌管理命令

Systemd 統一管理所有 Unit 的啟動日誌。帶來的好處就是,可以只用journalctl一個命令,查看所有日誌(內核日誌和應用日誌)。

配置文件

/etc/systemd/journald.conf

日誌保存目錄

/var/log/journal/

默認日誌最大限製為所在文件系統容量的 10%,可通過 /etc/systemd/journald.conf 中的 SystemMaxUse 字段來指定

該目錄是 systemd 軟件包的一部分。若被刪除,systemd 不會自動創建它,直到下次升級軟件包時重建該目錄。如果該目錄缺失,systemd 會將日誌記錄寫入 /run/systemd/journal。這意味着,系統重啟後日志將丟失。

journalctl -u [服務名]

查看指定單元的日誌

journalctl -b
  • journalctl -b -0 显示本次啟動的信息
  • journalctl -b -1 显示上次啟動的信息
  • journalctl -b -2 显示上上次啟動的信息

參考

http://manpages.ubuntu.com/manpages/bionic/en/man1/systemctl.1.html

http://manpages.ubuntu.com/manpages/bionic/en/man5/systemd.unit.5.html

https://www.cnblogs.com/yingsong/p/6012180.html

https://www.cnblogs.com/sparkdev/p/8472711.html

http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html

http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html

Systemd Boot Process a Close Look in Linux

https://cloud.tencent.com/developer/article/1516125

https://www.cnblogs.com/sparkdev/p/8472711.html

https://www.ibm.com/developerworks/cn/linux/1407_liuming_init3/index.html?ca=drs-

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

分類
發燒車訊

沒有廁所的大廈──日本核廢燃料棒處理概況

文:宋瑞文

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

分類
發燒車訊

小師妹學JVM之:JIT中的PrintCompilation

目錄

  • 簡介
  • PrintCompilation
  • 分析PrintCompilation的結果
  • 總結

簡介

上篇文章我們講到了JIT中的LogCompilation,將編譯的日誌都收集起來,存到日誌文件裏面,並且詳細的解釋了LogCompilation日誌文件中的內容定義。今天我們再和小師妹一起學習LogCompilation的姊妹篇PrintCompilation,看看都有什麼妙用吧。

PrintCompilation

小師妹:F師兄,上次你給講的LogCompilation實在是太複雜了,生成的日誌文件又多,完全看不了,我其實只是想知道有哪些方法被編譯成了機器碼,有沒有什麼更加簡單的辦法呢?

真理的大海,讓未發現的一切事物躺卧在我的眼前,任我去探尋- 牛頓(英國)

當然有的,那就給你介紹一下LogCompilation的妹妹PrintCompilation,為什麼是妹妹呢?因為PrintCompilation輸出的日誌要比LogCompilation少太多了。

老規矩,上上我們的JMH運行代碼,文章中使用的代碼鏈接都會在文末註明,這裏使用圖片的原因只是為了方便讀者閱讀代碼:

這裏和上次的LogCompilation不同的是,我們使用:-XX:+PrintCompilation參數。

其實我們還可以添加更多的參數,例如:

-Xbatch -XX:-TieredCompilation -XX:+PrintCompilation

先講一下-Xbatch。

一般來說JIT編譯器使用的是和主線程完全不同的新的線程。這樣做的好處就是JIT可以和主線程并行執行,編譯器的運行基本上不會影響到主線程的的運行。

但是有陰就有陽,有利就有弊。多線程在提高的處理速度的同時,帶給我們的就是輸出日誌的混亂。因為是并行執行的,我們主線程的日誌中,穿插了JIT編譯器的線程日誌。

如果使用-Xbatch就可以強迫JIT編譯器使用主線程。這樣我們的輸出日誌就是井然有序的。真棒。

再講一下TieredCompilation。

為了更好的提升編譯效率,JVM在JDK7中引入了分層編譯Tiered compilation的概念。

大概來說分層編譯可以分為三層:

第一層就是禁用C1和C2編譯器,這個時候沒有JIT進行。
第二層就是只開啟C1編譯器,因為C1編譯器只會進行一些簡單的JIT優化,所以這個可以應對常規情況。
第三層就是同時開啟C1和C2編譯器。

在JDK8中,分層編譯是默認開啟的。因為不同的編譯級別處理編譯的時間是不一樣的,後面層級的編譯器啟動的要比前面層級的編譯器要慢,但是優化的程度更高。

這樣我們其實會產生很多中間的優化代碼,這裏我們只是想分析最終的優化代碼,所以我們需要停止分層編譯的功能。

最後是今天的主角:PrintCompilation。

PrintCompilation將會輸出被編譯方法的統計信息,因此使用PrintCompilation可以很方便的看出哪些是熱點代碼。熱點代碼也就意味着存在着被優化的可能性。

分析PrintCompilation的結果

小師妹:F師兄,我照着你的例子運行了一下,結果果然清爽了很多。可是我還是看不懂。

沒有一個人能全面把握真理。小師妹,我們始終在未知的路上前行。不懂就問,不會就學。

我們再截個圖看一下生成的日誌吧。

因為日誌太長了,為了節約大家的流量,我只截取了開頭的部分和結尾的部分。

大家可以看到開頭部分基本上都是java自帶的類的優化。只有最後才是我們自己寫的類。

第一列是方法開始編譯的時間。

第二列是簡單的index。

第三列是一系列的flag的組合,有下面幾個flag:

b    Blocking compiler (always set for client)
*    Generating a native wrapper
%    On stack replacement (where the compiled code is running)
!    Method has exception handlers
s    Method declared as synchronized
n    Method declared as native
made non entrant    compilation was wrong/incomplete, no future callers will use this version
made zombie         code is not in use and ready for GC

如果我們沒有關閉分層編譯的話,在方法名前面還會有数字,表示是使用的那個編譯器。

分層編譯詳細的來說可以分為5個級別。

0表示是使用解釋器,不使用JIT編譯。
1,2,3是使用C1編譯器(client)。
4是使用C2編譯器(server)。

現在讓我們來看一下最後一列。

最後一列包含了方法名和方法的長度。注意這裏的長度指的是字節碼的長度。

如果字節碼被編譯成為機器碼,長度會增加很多倍。

總結

本文介紹了JIT中PrintCompilation的使用,並再次複習了JIT中的分層編譯架構。希望大家能夠喜歡。

本文的例子https://github.com/ddean2009/learn-java-base-9-to-20

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/jvm-jit-printcompilation/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

分類
發燒車訊

耐用、無害、可降解 科學家發明可預防野火的黏液

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

分類
發燒車訊

C# 人臉識別庫

.NET 人臉識別庫 ViewFaceCore

這是基於 SeetaFace6 人臉識別開發的 .NET 平台下的人臉識別庫
這是一個使用超簡單的人臉識別庫
這是一個基於 .NET Standard 2.0 開發的庫
這個庫已經發布到 NuGet ,你可以一鍵集成到你的項目
此項目可以免費商業使用

⭐、開源

開源協議:Apache-2.0
GitHub地址: ViewFaceCore
十分感謝您的小星星

一、示例

示例項目地址:WinForm 攝像頭人臉檢測
示例項目效果:

 

二、使用

一分鐘在你的項目里集成人臉識別

1. 創建你的 .NET 應用

.NET Standard >= 2.0
.NET Core >= 2.0
.NET Framework >= 4.6.1^2

2. 使用 Nuget 安裝 ViewFaceCore

  • Author : View
  • Version >= 0.1.1

此 Nuget 包會自動添加依賴的 C++ 庫,以及最精簡的識別模型。
如果需要其它場景的識別模型,請下載 SeetaFace6 模型文件。

3. 在項目中編寫你的代碼

  • 按照 說明 自己編寫
  • 或者參考以下代碼

簡單的調用示例

 1 static void Main()
 2         {
 3             ViewFace viewFace = new ViewFace((str) => { Debug.WriteLine(str); }); // 初始化人臉識別類,並設置 日誌回調函數
 4             viewFace.DetectorSetting = new DetectorSetting() { FaceSize = 20, MaxWidth = 2000, MaxHeight = 2000, Threshold = 0.5 };
 5 
 6             // 系統默認使用的輕量級識別模型。如果對精度有要求,請切換到 Normal 模式;並下載需要模型文件 放入生成目錄的 model 文件夾中
 7             viewFace.FaceType = FaceType.Normal;
 8             // 系統默認使用5個人臉關鍵點。//不建議改動,除非是使用口罩模型。
 9             viewFace.MarkType = MarkType.Light;
10 
11             #region 識別老照片
12             float[] oldEigenValues;
13             Bitmap oldImg = (Bitmap)Image.FromFile(@"C:\Users\yangw\OneDrive\圖片\Camera Roll\IMG_20181103_142707.jpg"/*老圖片路徑*/); // 從文件中加載照片 // 或者視頻幀等
14             var oldFaces = viewFace.FaceDetector(oldImg); // 檢測圖片中包含的人臉信息。(置信度、位置、大小)
15             if (oldFaces.Length > 0) //識別到人臉
16             {
17                 { // 打印人臉信息
18                     Console.WriteLine($"識別到的人臉數量:{oldFaces.Length} 。人臉信息:\n");
19                     Console.WriteLine($"序號\t人臉置信度\t位置X\t位置Y\t寬度\t高度");
20                     for (int i = 0; i < oldFaces.Length; i++)
21                     {
22                         Console.WriteLine($"{i + 1}\t{oldFaces[i].Score}\t{oldFaces[i].Location.X}\t{oldFaces[i].Location.Y}\t{oldFaces[i].Location.Width}\t{oldFaces[i].Location.Height}");
23                     }
24                     Console.WriteLine();
25                 }
26                 var oldPoints = viewFace.FaceMark(oldImg, oldFaces[0]); // 獲取 第一個人臉 的識別關鍵點。(人臉識別的關鍵點數據)
27                 oldEigenValues = viewFace.Extract(oldImg, oldPoints); // 獲取 指定的關鍵點 的特徵值。
28             }
29             else { oldEigenValues = new float[0]; /*未識別到人臉*/ }
30             #endregion
31 
32             #region 識別新照片
33             float[] newEigenValues;
34             Bitmap newImg = (Bitmap)Image.FromFile(@"C:\Users\yangw\OneDrive\圖片\Camera Roll\IMG_20181129_224339.jpg"/*新圖片路徑*/); // 從文件中加載照片 // 或者視頻幀等
35             var newFaces = viewFace.FaceDetector(newImg); // 檢測圖片中包含的人臉信息。(置信度、位置、大小)
36             if (newFaces.Length > 0) //識別到人臉
37             {
38                 { // 打印人臉信息
39                     Console.WriteLine($"識別到的人臉數量:{newFaces.Length} 。人臉信息:\n");
40                     Console.WriteLine($"序號\t人臉置信度\t位置X\t位置Y\t寬度\t高度");
41                     for (int i = 0; i < newFaces.Length; i++)
42                     {
43                         Console.WriteLine($"{i + 1}\t{newFaces[i].Score}\t{newFaces[i].Location.X}\t{newFaces[i].Location.Y}\t{newFaces[i].Location.Width}\t{newFaces[i].Location.Height}");
44                     }
45                     Console.WriteLine();
46                 }
47                 var newPoints = viewFace.FaceMark(newImg, newFaces[0]); // 獲取 第一個人臉 的識別關鍵點。(人臉識別的關鍵點數據)
48                 newEigenValues = viewFace.Extract(newImg, newPoints); // 獲取 指定的關鍵點 的特徵值。
49             }
50             else { newEigenValues = new float[0]; /*未識別到人臉*/ }
51             #endregion
52 
53             try
54             {
55                 float similarity = viewFace.Similarity(oldEigenValues, newEigenValues); // 對比兩張照片上的數據,確認是否是同一個人。
56                 Console.WriteLine($"閾值 = {Face.Threshold[viewFace.FaceType]}\t相似度 = {similarity}");
57                 Console.WriteLine($"是否是同一個人:{viewFace.IsSelf(similarity)}");
58             }
59             catch (Exception e)
60             { Console.WriteLine(e); }
61 
62             Console.ReadKey();
63         }

ViewFaceCore 使用示例

 

三、說明

命名空間:ViewFaceCore.Sharp : 人臉識別類所在的命名空間

  • 屬性說明:
 

屬性名稱 類型 說明 默認值
ModelPath string 獲取或設置模型路徑 [ 如非必要,請勿修改 ] ./model/
FaceType FaceType 獲取或設置人臉類型 FaceType.Light
MarkType MarkType 獲取或設置人臉關鍵點類型 MarkType.Light
DetectorSetting DetectorSetting 獲取或設置人臉檢測器設置 new DetectorSetting()

 

  • 方法說明:

 

 1 using System.Drawing;
 2 using ViewFaceCore.Sharp;
 3 using ViewFaceCore.Sharp.Model;
 4 
 5 // 識別 bitmap 中的人臉,並返回人臉的信息。
 6 FaceInfo[] FaceDetector(Bitmap);
 7 
 8 // 識別 bitmap 中指定的人臉信息 info 的關鍵點坐標。
 9 FaceMarkPoint[] FaceMark(Bitmap, FaceInfo);
10 
11 // 提取人臉特徵值。
12 float[] Extract(Bitmap, FaceMarkPoint[]);
13 
14 // 計算特徵值相似度。
15 float Similarity(float[], float[]);
16 
17 // 判斷相似度是否為同一個人。
18 bool IsSelf(float);

 

四、實現

此項目受到了 SeetaFaceEngine.NET 項目的啟發

這個項目本質上來說還是調用了 SeetaFace 的 C++ 類庫來實現的人臉識別功能。針對本人遇到過的相關的類庫的使用都不太方便,而且使用的 SeetaFace 的版本較老,故萌生了自己重新開發的想法。

本項目在開發完成之後為了方便調用,採用了 Nuget 包的形式,將所有需要的依賴以及最小識別模型一起打包。在使用時非常簡單,只需要 nuget 安裝,編寫代碼,運行即可,不需要多餘的操作。

首先查看 SeetaFace ,已經更新到了v3(v6即v3)(上面前輩的項目是基於v1開發的),最新版本暫時沒有開源,但是可以免費商用。然後是根據以前的經驗和 SeetaFace6 文檔的指導,以及前輩的項目,做了以下操作。

1.對SeetaFace6 的接口進行了 C++ 形式的封裝。

目前主要實現了 人臉檢測,關鍵點提取,特徵值提取,特徵值對比幾個人臉識別中的基礎接口。有了這幾個接口,可以完整的實現一套人臉識別和驗證的流程。

  • c++封裝的接口代碼如下:
  1 #include "seeta/FaceDetector.h"
  2 #include "seeta/FaceLandmarker.h"
  3 #include "seeta/FaceRecognizer.h"
  4 
  5 #include <time.h>
  6 
  7 #define View_Api extern "C" __declspec(dllexport)
  8 
  9 using namespace std;
 10 
 11 typedef void(_stdcall* LogCallBack)(const char* logText);
 12 
 13 string modelPath = "./model/"; // 模型所在路徑
 14 LogCallBack logger = NULL; // 日誌回調函數
 15 
 16 // 打印日誌
 17 void WriteLog(string str) { if (logger != NULL) { logger(str.c_str()); } }
 18 
 19 void WriteMessage(string fanctionName, string message) { WriteLog(fanctionName + "\t Message:" + message); }
 20 void WriteModelName(string fanctionName, string modelName) { WriteLog(fanctionName + "\t Model.Name:" + modelName); }
 21 void WriteRunTime(string fanctionName, int start) { WriteLog(fanctionName + "\t Run.Time:" + to_string(clock() - start) + " ms"); }
 22 void WriteError(string fanctionName, const std::exception& e) { WriteLog(fanctionName + "\t Error:" + e.what()); }
 23 
 24 // 註冊日誌回調函數
 25 View_Api void V_SetLogFunction(LogCallBack writeLog)
 26 {
 27     logger = writeLog;
 28     WriteMessage(__FUNCDNAME__, "Successed.");
 29 }
 30 
 31 // 設置人臉模型目錄
 32 View_Api void V_SetModelPath(const char* path)
 33 {
 34     modelPath = path;
 35     WriteMessage(__FUNCDNAME__, "Model.Path:" + modelPath);
 36 }
 37 // 獲取人臉模型目錄
 38 View_Api bool V_GetModelPath(char** path)
 39 {
 40     try
 41     {
 42 #pragma warning(disable:4996)
 43         strcpy(*path, modelPath.c_str());
 44 
 45         return true;
 46     }
 47     catch (const std::exception& e)
 48     {
 49         WriteError(__FUNCDNAME__, e);
 50         return false;
 51     }
 52 }
 53 
 54 seeta::FaceDetector* v_faceDetector = NULL;
 55 
 56 // 人臉檢測結果
 57 static SeetaFaceInfoArray detectorInfos;
 58 // 人臉數量檢測器
 59 View_Api int V_DetectorSize(unsigned char* imgData, int width, int height, int channels, double faceSize = 20, double threshold = 0.9, double maxWidth = 2000, double maxHeight = 2000, int type = 0)
 60 {
 61     try {
 62         clock_t start = clock();
 63 
 64         SeetaImageData img = { width, height, channels, imgData };
 65         if (v_faceDetector == NULL) {
 66             seeta::ModelSetting setting;
 67             setting.set_device(SEETA_DEVICE_CPU);
 68             string modelName = "face_detector.csta";
 69             switch (type)
 70             {
 71             case 1: modelName = "mask_detector.csta"; break;
 72             }
 73             setting.append(modelPath + modelName);
 74             WriteModelName(__FUNCDNAME__, modelName);
 75             v_faceDetector = new seeta::FaceDetector(setting);
 76         }
 77 
 78         if (faceSize != 20) { v_faceDetector->set(seeta::FaceDetector::Property::PROPERTY_MIN_FACE_SIZE, faceSize); }
 79         if (threshold != 0.9) { v_faceDetector->set(seeta::FaceDetector::Property::PROPERTY_THRESHOLD, threshold); }
 80         if (maxWidth != 2000) { v_faceDetector->set(seeta::FaceDetector::Property::PROPERTY_MAX_IMAGE_WIDTH, maxWidth); }
 81         if (maxHeight != 2000) { v_faceDetector->set(seeta::FaceDetector::Property::PROPERTY_MAX_IMAGE_HEIGHT, maxHeight); }
 82 
 83         auto infos = v_faceDetector->detect(img);
 84         detectorInfos = infos;
 85 
 86         WriteRunTime("V_Detector", start); // 此方法已經是人臉檢測的全過程,故計時器显示為 人臉識別方法
 87         return infos.size;
 88     }
 89     catch (const std::exception& e)
 90     {
 91         WriteError(__FUNCDNAME__, e);
 92         return -1;
 93     }
 94 }
 95 // 人臉檢測器
 96 View_Api bool V_Detector(float* score, int* x, int* y, int* width, int* height)
 97 {
 98     try
 99     {
100         //clock_t start = clock();
101 
102         for (int i = 0; i < detectorInfos.size; i++, detectorInfos.data++)
103         {
104             *score = detectorInfos.data->score;
105             *x = detectorInfos.data->pos.x;
106             *y = detectorInfos.data->pos.y;
107             *width = detectorInfos.data->pos.width;
108             *height = detectorInfos.data->pos.height;
109             score++, x++, y++, width++, height++;
110         }
111         detectorInfos.data = NULL;
112         detectorInfos.size = NULL;
113 
114         //WriteRunTime(__FUNCDNAME__, start); // 此方法只是將 人臉數量檢測器 獲取到的數據賦值傳遞,並不耗時。故不显示此方法的調用時間
115         return true;
116     }
117     catch (const std::exception& e)
118     {
119         WriteError(__FUNCDNAME__, e);
120         return false;
121     }
122 }
123 
124 
125 seeta::FaceLandmarker* v_faceLandmarker = NULL;
126 // 人臉關鍵點數量
127 View_Api int V_FaceMarkSize(int type = 0)
128 {
129     try
130     {
131         clock_t start = clock();
132 
133         if (v_faceLandmarker == NULL) {
134             seeta::ModelSetting setting;
135             setting.set_device(SEETA_DEVICE_CPU);
136             string modelName = "face_landmarker_pts68.csta";
137             switch (type)
138             {
139             case 1: modelName = "face_landmarker_mask_pts5.csta"; break;
140             case 2: modelName = "face_landmarker_pts5.csta"; break;
141             }
142             setting.append(modelPath + modelName);
143             WriteModelName(__FUNCDNAME__, modelName);
144             v_faceLandmarker = new seeta::FaceLandmarker(setting);
145         }
146         int size = v_faceLandmarker->number();
147 
148         WriteRunTime(__FUNCDNAME__, start);
149         return size;
150     }
151     catch (const std::exception& e)
152     {
153         WriteError(__FUNCDNAME__, e);
154         return -1;
155     }
156 }
157 // 人臉關鍵點
158 View_Api bool V_FaceMark(unsigned char* imgData, int width, int height, int channels, int x, int y, int fWidth, int fHeight, double* pointX, double* pointY, int type = 0)
159 {
160     try
161     {
162         clock_t start = clock();
163 
164         SeetaImageData img = { width, height, channels, imgData };
165         SeetaRect face = { x, y, fWidth, fHeight };
166         if (v_faceLandmarker == NULL) {
167             seeta::ModelSetting setting;
168             setting.set_device(SEETA_DEVICE_CPU);
169             string modelName = "face_landmarker_pts68.csta";
170             switch (type)
171             {
172             case 1: modelName = "face_landmarker_mask_pts5.csta"; break;
173             case 2: modelName = "face_landmarker_pts5.csta"; break;
174             }
175             setting.append(modelPath + modelName);
176             WriteModelName(__FUNCDNAME__, modelName);
177             v_faceLandmarker = new seeta::FaceLandmarker(setting);
178         }
179         std::vector<SeetaPointF> _points = v_faceLandmarker->mark(img, face);
180 
181         if (!_points.empty()) {
182             for (auto iter = _points.begin(); iter != _points.end(); iter++)
183             {
184                 *pointX = (*iter).x;
185                 *pointY = (*iter).y;
186                 pointX++;
187                 pointY++;
188             }
189 
190             WriteRunTime(__FUNCDNAME__, start);
191             return true;
192         }
193         else { return false; }
194     }
195     catch (const std::exception& e)
196     {
197         WriteError(__FUNCDNAME__, e);
198         return false;
199     }
200 }
201 
202 seeta::FaceRecognizer* v_faceRecognizer = NULL;
203 // 獲取人臉特徵值長度
204 View_Api int V_ExtractSize(int type = 0)
205 {
206     try
207     {
208         clock_t start = clock();
209 
210         if (v_faceRecognizer == NULL) {
211             seeta::ModelSetting setting;
212             setting.set_id(0);
213             setting.set_device(SEETA_DEVICE_CPU);
214             string modelName = "face_recognizer.csta";
215             switch (type)
216             {
217             case 1: modelName = "face_recognizer_mask.csta"; break;
218             case 2: modelName = "face_recognizer_light.csta"; break;
219             }
220             setting.append(modelPath + modelName);
221             WriteModelName(__FUNCDNAME__, modelName);
222             v_faceRecognizer = new seeta::FaceRecognizer(setting);
223         }
224         int length = v_faceRecognizer->GetExtractFeatureSize();
225 
226         WriteRunTime(__FUNCDNAME__, start);
227         return length;
228     }
229     catch (const std::exception& e)
230     {
231         WriteError(__FUNCDNAME__, e);
232         return -1;
233     }
234 }
235 // 提取人臉特徵值
236 View_Api bool V_Extract(unsigned char* imgData, int width, int height, int channels, SeetaPointF* points, float* features, int type = 0)
237 {
238     try
239     {
240         clock_t start = clock();
241 
242         SeetaImageData img = { width, height, channels, imgData };
243         if (v_faceRecognizer == NULL) {
244             seeta::ModelSetting setting;
245             setting.set_id(0);
246             setting.set_device(SEETA_DEVICE_CPU);
247             string modelName = "face_recognizer.csta";
248             switch (type)
249             {
250             case 1: modelName = "face_recognizer_mask.csta"; break;
251             case 2: modelName = "face_recognizer_light.csta"; break;
252             }
253             setting.append(modelPath + modelName);
254             WriteModelName(__FUNCDNAME__, modelName);
255             v_faceRecognizer = new seeta::FaceRecognizer(setting);
256         }
257         int length = v_faceRecognizer->GetExtractFeatureSize();
258         std::shared_ptr<float> _features(new float[v_faceRecognizer->GetExtractFeatureSize()], std::default_delete<float[]>());
259         v_faceRecognizer->Extract(img, points, _features.get());
260 
261         for (int i = 0; i < length; i++)
262         {
263             *features = _features.get()[i];
264             features++;
265         }
266 
267         WriteRunTime(__FUNCDNAME__, start);
268         return true;
269 
270     }
271     catch (const std::exception& e)
272     {
273         WriteError(__FUNCDNAME__, e);
274         return false;
275     }
276 }
277 // 人臉特徵值相似度計算
278 View_Api float V_CalculateSimilarity(float* leftFeatures, float* rightFeatures, int type = 0)
279 {
280     try
281     {
282         clock_t start = clock();
283 
284         if (v_faceRecognizer == NULL) {
285             seeta::ModelSetting setting;
286             setting.set_id(0);
287             setting.set_device(SEETA_DEVICE_CPU);
288             string modelName = "face_recognizer.csta";
289             switch (type)
290             {
291             case 1: modelName = "face_recognizer_mask.csta"; break;
292             case 2: modelName = "face_recognizer_light.csta"; break;
293             }
294             setting.append(modelPath + modelName);
295             WriteModelName(__FUNCDNAME__, modelName);
296             v_faceRecognizer = new seeta::FaceRecognizer(setting);
297         }
298 
299         auto similarity = v_faceRecognizer->CalculateSimilarity(leftFeatures, rightFeatures);
300         WriteMessage(__FUNCDNAME__, "Similarity = " + to_string(similarity));
301         WriteRunTime(__FUNCDNAME__, start);
302         return similarity;
303     }
304     catch (const std::exception& e)
305     {
306         WriteError(__FUNCDNAME__, e);
307         return -1;
308     }
309 }
310 
311 // 釋放資源
312 View_Api void V_Dispose()
313 {
314     if (v_faceDetector != NULL) delete v_faceDetector;
315     if (v_faceLandmarker != NULL) delete v_faceLandmarker;
316     if (v_faceRecognizer != NULL) delete v_faceRecognizer;
317 }

C++ 封裝層

2.採用 C# 對上訴接口進行了導入。

因為C++的項目測CPU架構區分x86和x64,所以C# 層也需要區分架構封裝

using System.Runtime.InteropServices;
using System.Text;
using ViewFaceCore.Sharp.Model;

namespace ViewFaceCore.Plus
{
    /// <summary>
    /// 日誌回調函數
    /// </summary>
    /// <param name="logText"></param>
    public delegate void LogCallBack(string logText);

    class ViewFacePlus64
    {
        const string LibraryPath = @"FaceLibraries\x64\ViewFace.dll";
        /// <summary>
        /// 設置日誌回調函數(用於日誌打印)
        /// </summary>
        /// <param name="writeLog"></param>
        [DllImport(LibraryPath, EntryPoint = "V_SetLogFunction", CallingConvention = CallingConvention.Cdecl)]
        public static extern void SetLogFunction(LogCallBack writeLog);

        /// <summary>
        /// 設置人臉模型的目錄
        /// </summary>
        /// <param name="path"></param>
        [DllImport(LibraryPath, EntryPoint = "V_SetModelPath", CallingConvention = CallingConvention.Cdecl)]
        private extern static void SetModelPath(byte[] path);
        /// <summary>
        /// 設置人臉模型的目錄
        /// </summary>
        /// <param name="path"></param>
        public static void SetModelPath(string path) => SetModelPath(Encoding.UTF8.GetBytes(path));

        /// <summary>
        /// 釋放使用的資源
        /// </summary>
        [DllImport(LibraryPath, EntryPoint = "V_Dispose", CallingConvention = CallingConvention.Cdecl)]
        public extern static void ViewDispose();

        /// <summary>
        /// 獲取人臉模型的目錄
        /// </summary>
        /// <param name="path"></param>
        [DllImport(LibraryPath, EntryPoint = "V_GetModelPath", CallingConvention = CallingConvention.Cdecl)]
        private extern static bool GetModelPathEx(ref string path);
        /// <summary>
        /// 獲取人臉模型的目錄
        /// </summary>
        public static string GetModelPath() { string path = string.Empty; GetModelPathEx(ref path); return path; }

        /// <summary>
        /// 人臉檢測器檢測到的人臉數量
        /// </summary>
        /// <param name="imgData"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="channels"></param>
        /// <param name="faceSize">最小人臉是人臉檢測器常用的一個概念,默認值為20,單位像素。
        /// <para>最小人臉和檢測器性能息息相關。主要方面是速度,使用建議上,我們建議在應用範圍內,這個值設定的越大越好。SeetaFace採用的是BindingBox Regresion的方式訓練的檢測器。如果最小人臉參數設置為80的話,從檢測能力上,可以將原圖縮小的原來的1/4,這樣從計算複雜度上,能夠比最小人臉設置為20時,提速到16倍。</para>
        /// </param>
        /// <param name="threshold">檢測器閾值默認值是0.9,合理範圍為[0, 1]。這個值一般不進行調整,除了用來處理一些極端情況。這個值設置的越小,漏檢的概率越小,同時誤檢的概率會提高</param>
        /// <param name="maxWidth">可檢測的圖像最大寬度。默認值2000。</param>
        /// <param name="maxHeight">可檢測的圖像最大高度。默認值2000。</param>
        /// <param name="type"></param>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_DetectorSize", CallingConvention = CallingConvention.Cdecl)]
        public extern static int DetectorSize(byte[] imgData, int width, int height, int channels, double faceSize = 20, double threshold = 0.9, double maxWidth = 2000, double maxHeight = 2000, int type = 0);
        /// <summary>
        /// 人臉檢測器
        /// <para>調用此方法前必須先調用 <see cref="DetectorSize(byte[], int, int, int, double, double, double, double, int)"/></para>
        /// </summary>
        /// <param name="score">人臉置信度集合</param>
        /// <param name="x">人臉位置集合</param>
        /// <param name="y">人臉位置集合</param>
        /// <param name="width">人臉大小集合</param>
        /// <param name="height">人臉大小集合</param>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_Detector", CallingConvention = CallingConvention.Cdecl)]
        public extern static bool Detector(float[] score, int[] x, int[] y, int[] width, int[] height);

        /// <summary>
        /// 人臉關鍵點數量
        /// </summary>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_FaceMarkSize", CallingConvention = CallingConvention.Cdecl)]
        public extern static int FaceMarkSize(int type = 0);
        /// <summary>
        /// 人臉關鍵點
        /// </summary>
        /// <param name="imgData"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="channels"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="fWidth"></param>
        /// <param name="fHeight"></param>
        /// <param name="pointX"></param>
        /// <param name="pointY"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_FaceMark", CallingConvention = CallingConvention.Cdecl)]
        public extern static bool FaceMark(byte[] imgData, int width, int height, int channels, int x, int y, int fWidth, int fHeight, double[] pointX, double[] pointY, int type = 0);

        /// <summary>
        /// 提取特徵值
        /// </summary>
        /// <param name="imgData"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="channels"></param>
        /// <param name="points"></param>
        /// <param name="features"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_Extract", CallingConvention = CallingConvention.Cdecl)]
        public extern static bool Extract(byte[] imgData, int width, int height, int channels, FaceMarkPoint[] points, float[] features, int type = 0);
        /// <summary>
        /// 特徵值大小
        /// </summary>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_ExtractSize", CallingConvention = CallingConvention.Cdecl)]
        public extern static int ExtractSize(int type = 0);

        /// <summary>
        /// 計算相似度
        /// </summary>
        /// <param name="leftFeatures"></param>
        /// <param name="rightFeatures"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_CalculateSimilarity", CallingConvention = CallingConvention.Cdecl)]
        public extern static float Similarity(float[] leftFeatures, float[] rightFeatures, int type = 0);
    }
    class ViewFacePlus32
    {
        const string LibraryPath = @"FaceLibraries\x86\ViewFace.dll";
        /// <summary>
        /// 設置日誌回調函數(用於日誌打印)
        /// </summary>
        /// <param name="writeLog"></param>
        [DllImport(LibraryPath, EntryPoint = "V_SetLogFunction", CallingConvention = CallingConvention.Cdecl)]
        public static extern void SetLogFunction(LogCallBack writeLog);

        /// <summary>
        /// 設置人臉模型的目錄
        /// </summary>
        /// <param name="path"></param>
        [DllImport(LibraryPath, EntryPoint = "V_SetModelPath", CallingConvention = CallingConvention.Cdecl)]
        private extern static void SetModelPath(byte[] path);
        /// <summary>
        /// 設置人臉模型的目錄
        /// </summary>
        /// <param name="path"></param>
        public static void SetModelPath(string path) => SetModelPath(Encoding.UTF8.GetBytes(path));

        /// <summary>
        /// 釋放使用的資源
        /// </summary>
        [DllImport(LibraryPath, EntryPoint = "V_Dispose", CallingConvention = CallingConvention.Cdecl)]
        public extern static void ViewDispose();

        /// <summary>
        /// 獲取人臉模型的目錄
        /// </summary>
        /// <param name="path"></param>
        [DllImport(LibraryPath, EntryPoint = "V_GetModelPath", CallingConvention = CallingConvention.Cdecl)]
        private extern static bool GetModelPathEx(ref string path);
        /// <summary>
        /// 獲取人臉模型的目錄
        /// </summary>
        public static string GetModelPath() { string path = string.Empty; GetModelPathEx(ref path); return path; }

        /// <summary>
        /// 人臉檢測器檢測到的人臉數量
        /// </summary>
        /// <param name="imgData"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="channels"></param>
        /// <param name="faceSize">最小人臉是人臉檢測器常用的一個概念,默認值為20,單位像素。
        /// <para>最小人臉和檢測器性能息息相關。主要方面是速度,使用建議上,我們建議在應用範圍內,這個值設定的越大越好。SeetaFace採用的是BindingBox Regresion的方式訓練的檢測器。如果最小人臉參數設置為80的話,從檢測能力上,可以將原圖縮小的原來的1/4,這樣從計算複雜度上,能夠比最小人臉設置為20時,提速到16倍。</para>
        /// </param>
        /// <param name="threshold">檢測器閾值默認值是0.9,合理範圍為[0, 1]。這個值一般不進行調整,除了用來處理一些極端情況。這個值設置的越小,漏檢的概率越小,同時誤檢的概率會提高</param>
        /// <param name="maxWidth">可檢測的圖像最大寬度。默認值2000。</param>
        /// <param name="maxHeight">可檢測的圖像最大高度。默認值2000。</param>
        /// <param name="type"></param>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_DetectorSize", CallingConvention = CallingConvention.Cdecl)]
        public extern static int DetectorSize(byte[] imgData, int width, int height, int channels, double faceSize = 20, double threshold = 0.9, double maxWidth = 2000, double maxHeight = 2000, int type = 0);
        /// <summary>
        /// 人臉檢測器
        /// <para>調用此方法前必須先調用 <see cref="DetectorSize(byte[], int, int, int, double, double, double, double, int)"/></para>
        /// </summary>
        /// <param name="score">人臉置信度集合</param>
        /// <param name="x">人臉位置集合</param>
        /// <param name="y">人臉位置集合</param>
        /// <param name="width">人臉大小集合</param>
        /// <param name="height">人臉大小集合</param>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_Detector", CallingConvention = CallingConvention.Cdecl)]
        public extern static bool Detector(float[] score, int[] x, int[] y, int[] width, int[] height);

        /// <summary>
        /// 人臉關鍵點數量
        /// </summary>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_FaceMarkSize", CallingConvention = CallingConvention.Cdecl)]
        public extern static int FaceMarkSize(int type = 0);
        /// <summary>
        /// 人臉關鍵點
        /// </summary>
        /// <param name="imgData"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="channels"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="fWidth"></param>
        /// <param name="fHeight"></param>
        /// <param name="pointX"></param>
        /// <param name="pointY"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_FaceMark", CallingConvention = CallingConvention.Cdecl)]
        public extern static bool FaceMark(byte[] imgData, int width, int height, int channels, int x, int y, int fWidth, int fHeight, double[] pointX, double[] pointY, int type = 0);

        /// <summary>
        /// 提取特徵值
        /// </summary>
        /// <param name="imgData"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="channels"></param>
        /// <param name="points"></param>
        /// <param name="features"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_Extract", CallingConvention = CallingConvention.Cdecl)]
        public extern static bool Extract(byte[] imgData, int width, int height, int channels, FaceMarkPoint[] points, float[] features, int type = 0);
        /// <summary>
        /// 特徵值大小
        /// </summary>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_ExtractSize", CallingConvention = CallingConvention.Cdecl)]
        public extern static int ExtractSize(int type = 0);

        /// <summary>
        /// 計算相似度
        /// </summary>
        /// <param name="leftFeatures"></param>
        /// <param name="rightFeatures"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        [DllImport(LibraryPath, EntryPoint = "V_CalculateSimilarity", CallingConvention = CallingConvention.Cdecl)]
        public extern static float Similarity(float[] leftFeatures, float[] rightFeatures, int type = 0);
    }
}

C# 導入層

3.採用 C# 的面向對象的封裝

因為C#的項目默認都是 AnyCPU,所以為了簡化調用,在這一層封裝的時候增加了架構判斷,當在你的項目中引用的時候,不用做任何修改。

且因為C++的C#導入方法在和原生的C#寫法略有差異,且數據的轉換和傳遞比較麻煩,所以類庫中對外隱藏了 C# 導入層。並使用大家都更熟悉的C#的面向對象的方式進行進一步的封裝和簡化。

  1     /// <summary>
  2     /// 人臉識別類
  3     /// </summary>
  4     public class ViewFace
  5     {
  6         bool Platform64 { get; set; } = false;
  7         // <para>需要模型:<see langword=""/></para>
  8 
  9         // ctor
 10         /// <summary>
 11         /// 使用默認的模型目錄初始化人臉識別類
 12         /// </summary>
 13         public ViewFace() : this("./model/") { }
 14         /// <summary>
 15         /// 使用指定的模型目錄初始化人臉識別類
 16         /// </summary>
 17         /// <param name="modelPath">模型目錄</param>
 18         public ViewFace(string modelPath)
 19         {
 20             Platform64 = IntPtr.Size == 8;
 21             if (Platform64)
 22             { ViewFacePlus64.SetModelPath(modelPath); }
 23             else
 24             { ViewFacePlus32.SetModelPath(modelPath); }
 25         }
 26         /// <summary>
 27         /// 使用指定的日誌回調函數初始化人臉識別類
 28         /// </summary>
 29         /// <param name="action">日誌回調函數</param>
 30         public ViewFace(LogCallBack action) : this("./model/", action) { }
 31         /// <summary>
 32         /// 使用指定的模型目錄、日誌回調函數初始化人臉識別類
 33         /// </summary>
 34         /// <param name="modelPath">模型目錄</param>
 35         /// <param name="action">日誌回調函數</param>
 36         public ViewFace(string modelPath, LogCallBack action) : this(modelPath)
 37         {
 38             if (Platform64)
 39             { ViewFacePlus64.SetLogFunction(action); }
 40             else
 41             { ViewFacePlus32.SetLogFunction(action); }
 42         }
 43 
 44         // public property
 45         /// <summary>
 46         /// 獲取或設置模型路徑
 47         /// </summary>
 48         public string ModelPath
 49         {
 50             get
 51             {
 52                 if (Platform64)
 53                 { return ViewFacePlus64.GetModelPath(); }
 54                 else
 55                 { return ViewFacePlus32.GetModelPath(); }
 56             }
 57             set
 58             {
 59                 if (Platform64)
 60                 { ViewFacePlus64.SetModelPath(value); }
 61                 else
 62                 { ViewFacePlus32.SetModelPath(value); }
 63             }
 64         }
 65         /// <summary>
 66         /// 獲取或設置人臉類型
 67         /// <para>
 68         /// <listheader>此屬性可影響到以下方法:</listheader><br />
 69         ///<c><see cref="FaceDetector(Bitmap)"/></c><br />
 70         ///<c><see cref="Extract(Bitmap, FaceMarkPoint[])"/></c><br />
 71         ///<c><see cref="Similarity(float[], float[])"/></c><br />
 72         /// </para>
 73         /// </summary>
 74         public FaceType FaceType { get; set; } = FaceType.Light;
 75         /// <summary>
 76         /// 獲取或設置人臉關鍵點類型
 77         /// <para>
 78         /// <listheader>此屬性可影響到以下方法:</listheader><br />
 79         ///<c><see cref="FaceMark(Bitmap, FaceInfo)"/></c><br />
 80         /// </para>
 81         /// </summary>
 82         public MarkType MarkType { get; set; } = MarkType.Light;
 83         /// <summary>
 84         /// 獲取或設置人臉檢測器設置
 85         /// </summary>
 86         public DetectorSetting DetectorSetting { get; set; } = new DetectorSetting();
 87 
 88 
 89         // public method
 90         /// <summary>
 91         /// 識別 <paramref name="bitmap"/> 中的人臉,並返回人臉的信息。
 92         /// <para>
 93         ///<c><see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Normal"/> <see langword="||"/> <see cref="FaceType.Light"/></c> 時, 需要模型:<see langword="face_detector.csta"/><br/>
 94         ///<c><see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Mask"/></c> 時, 需要模型:<see langword="mask_detector.csta"/><br/>
 95         /// </para>
 96         /// </summary>
 97         /// <param name="bitmap">包含人臉的圖片</param>
 98         /// <returns></returns>
 99         public FaceInfo[] FaceDetector(Bitmap bitmap)
100         {
101             byte[] bgr = ImageSet.Get24BGRFromBitmap(bitmap, out int width, out int height, out int channels);
102             int size;
103             if (Platform64)
104             { size = ViewFacePlus64.DetectorSize(bgr, width, height, channels, DetectorSetting.FaceSize, DetectorSetting.Threshold, DetectorSetting.MaxWidth, DetectorSetting.MaxHeight, (int)FaceType); }
105             else
106             { size = ViewFacePlus32.DetectorSize(bgr, width, height, channels, DetectorSetting.FaceSize, DetectorSetting.Threshold, DetectorSetting.MaxWidth, DetectorSetting.MaxHeight, (int)FaceType); }
107             float[] _socre = new float[size];
108             int[] _x = new int[size];
109             int[] _y = new int[size];
110             int[] _width = new int[size];
111             int[] _height = new int[size];
112             if (Platform64)
113             { _ = ViewFacePlus64.Detector(_socre, _x, _y, _width, _height); }
114             else
115             { _ = ViewFacePlus32.Detector(_socre, _x, _y, _width, _height); }
116             List<FaceInfo> infos = new List<FaceInfo>();
117             for (int i = 0; i < size; i++)
118             {
119                 infos.Add(new FaceInfo() { Score = _socre[i], Location = new FaceRect() { X = _x[i], Y = _y[i], Width = _width[i], Height = _height[i] } });
120             }
121             return infos.ToArray();
122         }
123 
124         /// <summary>
125         /// 識別 <paramref name="bitmap"/> 中指定的人臉信息 <paramref name="info"/> 的關鍵點坐標。
126         /// <para>
127         ///<see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Normal"/> 時, 需要模型:<see langword="face_landmarker_pts68.csta"/><br/>
128         ///<see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Mask"/> 時, 需要模型:<see langword="face_landmarker_mask_pts5.csta"/><br/>
129         ///<see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Light"/> 時, 需要模型:<see langword="face_landmarker_pts5.csta"/><br/>
130         /// </para>
131         /// </summary>
132         /// <param name="bitmap">包含人臉的圖片</param>
133         /// <param name="info">指定的人臉信息</param>
134         /// <returns></returns>
135         public FaceMarkPoint[] FaceMark(Bitmap bitmap, FaceInfo info)
136         {
137             byte[] bgr = ImageSet.Get24BGRFromBitmap(bitmap, out int width, out int height, out int channels);
138             int size;
139             if (Platform64)
140             { size = ViewFacePlus64.FaceMarkSize((int)MarkType); }
141             else
142             { size = ViewFacePlus32.FaceMarkSize((int)MarkType); }
143             double[] _pointX = new double[size];
144             double[] _pointY = new double[size];
145             bool val;
146             if (Platform64)
147             { val = ViewFacePlus64.FaceMark(bgr, width, height, channels, info.Location.X, info.Location.Y, info.Location.Width, info.Location.Height, _pointX, _pointY, (int)MarkType); }
148             else
149             { val = ViewFacePlus32.FaceMark(bgr, width, height, channels, info.Location.X, info.Location.Y, info.Location.Width, info.Location.Height, _pointX, _pointY, (int)MarkType); }
150             if (val)
151             {
152                 List<FaceMarkPoint> points = new List<FaceMarkPoint>();
153                 for (int i = 0; i < size; i++)
154                 { points.Add(new FaceMarkPoint() { X = _pointX[i], Y = _pointY[i] }); }
155                 return points.ToArray();
156             }
157             else
158             { throw new Exception("人臉關鍵點獲取失敗"); }
159         }
160 
161         /// <summary>
162         /// 提取人臉特徵值。
163         /// <para>
164         ///<see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Normal"/> 時, 需要模型:<see langword="face_recognizer.csta"/><br/>
165         ///<see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Mask"/> 時, 需要模型:<see langword="face_recognizer_mask.csta"/><br/>
166         ///<see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Light"/> 時, 需要模型:<see langword="face_recognizer_light.csta"/><br/>
167         /// </para>
168         /// </summary>
169         /// <param name="bitmap"></param>
170         /// <param name="points"></param>
171         /// <returns></returns>
172         public float[] Extract(Bitmap bitmap, FaceMarkPoint[] points)
173         {
174             byte[] bgr = ImageSet.Get24BGRFromBitmap(bitmap, out int width, out int height, out int channels);
175             float[] features;
176             if (Platform64)
177             { features = new float[ViewFacePlus64.ExtractSize((int)FaceType)]; }
178             else
179             { features = new float[ViewFacePlus32.ExtractSize((int)FaceType)]; }
180 
181             if (Platform64)
182             { ViewFacePlus64.Extract(bgr, width, height, channels, points, features, (int)FaceType); }
183             else
184             { ViewFacePlus32.Extract(bgr, width, height, channels, points, features, (int)FaceType); }
185             return features;
186         }
187 
188         /// <summary>
189         /// 計算特徵值相似度。
190         /// <para>只能計算相同 <see cref="FaceType"/> 計算出的特徵值</para>
191         /// <para>
192         ///<see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Normal"/> 時, 需要模型:<see langword="face_recognizer.csta"/><br/>
193         ///<see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Mask"/> 時, 需要模型:<see langword="face_recognizer_mask.csta"/><br/>
194         ///<see cref="FaceType"/> <see langword="="/> <see cref="FaceType.Light"/> 時, 需要模型:<see langword="face_recognizer_light.csta"/><br/>
195         /// </para>
196         /// </summary>
197         /// <exception cref="ArgumentException"/>
198         /// <exception cref="ArgumentNullException"/>
199         /// <param name="leftFeatures"></param>
200         /// <param name="rightFeatures"></param>
201         /// <returns></returns>
202         public float Similarity(float[] leftFeatures, float[] rightFeatures)
203         {
204             if (leftFeatures.Length == 0 || rightFeatures.Length == 0)
205                 throw new ArgumentNullException("參數不能為空", nameof(leftFeatures));
206             if (leftFeatures.Length != rightFeatures.Length)
207                 throw new ArgumentException("兩個參數長度不一致");
208 
209 
210             if (Platform64)
211             { return ViewFacePlus64.Similarity(leftFeatures, rightFeatures, (int)FaceType); }
212             else
213             { return ViewFacePlus32.Similarity(leftFeatures, rightFeatures, (int)FaceType); }
214         }
215 
216         /// <summary>
217         /// 判斷相似度是否為同一個人。
218         /// </summary>
219         /// <param name="similarity">相似度</param>
220         /// <returns></returns>
221         public bool IsSelf(float similarity) => similarity > Face.Threshold[FaceType];
222 
223         /// <summary>
224         /// 釋放資源
225         /// </summary>
226         ~ViewFace()
227         {
228             if (Platform64)
229             { ViewFacePlus64.ViewDispose(); }
230             else
231             { ViewFacePlus32.ViewDispose(); }
232         }
233     }

C# 面向對象層

 

五、也許…

  • 此項目還未實現 SeetaFace6 中的許多特性,也許:

    想起 GitHub 密碼,持續更新…
    刪除代碼倉庫跑路…

  • 如果在使用過程中遇到問題,你也許可以:

    在 GitHub 報告Bug…
    向我 發送郵件

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

分類
發燒車訊

Tesla Model S 遭爆有瑕疵,完美形象破功

    特斯拉當家電動車 Model S 不再完美無缺,去年給予 Model S 最高評級的消費者評鑒雜誌《Consumer Reports》點出 Model S 仍許多有小缺陷待改進。   《Consumer Reports》指出,Model S 多在跑超過一萬哩後出現問題,例如里程數超過 1.2 萬哩後,中央控制螢幕在會有反白的狀況,使多項功能無法操作。此外,還有車頂會發出異常噪音,前置行李箱蓋會自動開啟等問題。   Model S 去年 5 月在《Consumer Reports》評鑑中拿下 99 分(滿分 100 分),創史上最高分,之後並在 11 月獲選為年度 10 大好車第一名。   對此,特斯拉執行長 Elon Musk 7 月 31 日曾坦承,較早一批出廠的 Model S 的確有些生產上的瑕疵,但目前出廠的新車多已作修正。     (圖片來源:)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案