標籤: 新北清潔

  • 吃到飽變吃不飽?電動機車商用資費為何如此難算?

    吃到飽變吃不飽?電動機車商用資費為何如此難算?

    近日 Gogoro 電池月租吃到飽方案引發爭議,對於如何定義商用,以及如何舉證開罰,各界都有不同看法,但 Gogoro 先是強調不再寬待,昨夜又臨時發表聲明政策轉彎,然而品牌形象已經產生傷害。究竟 Gogoro 為何如此堅持,而電動機車的資費又應該怎麼設計會更合理呢?

    近日由於網友貼出了一張 Gogoro 寄送的「違規使用通知信」,而讓吃到飽方案成為爭論焦點。我們快速整理一下目前的重點。

    1. Gogoro 月租 899 吃到飽方案,禁止商業使用。
    2. 連續兩個月里程超過 1,600 公里,將被視為商業使用而開罰。
    3. 用戶收到通知後,可以回寄照片證明是用於出遊或長程通勤,即可免罰。
    4. 有路人開始檢舉外送員騎 gogoro 送餐。
    5. Gogoro 發公開信,5/10 起若被檢舉,無論里程長短,將直接變更為商用方案。
    6. Gogoro 修改標準,需連續兩個月里程超過 1,600 公里且被檢舉商業使用才開罰。

    且不談這次資費爭議,我們此時可以想的一件事情是,如果燃油車終將被淘汰,電動車需要怎樣的能源費用標準才合理?

    假設以每月 1,600 公里為使用里程來計算,目前各種能源方案以 Gogoro 商業型最貴,七期燃油車最便宜,充電式機車在光陽調降月租費用之後,如果採用兩顆電池方案,再加上全部在家充電,費用也相當便宜。

    每月騎 1,600 公里,機車能源費用比較。(圖片來源:科技新報製)

    不過 IONEX 方案並未說明是否可作為商業使用,而且月租費 398 方案限定綁約兩年,期滿後回到原價 598 元,這個方案還提供 2,000 公里里程,算是相當優惠,如果能夠在家充電的話,是一個不錯的選項。(充電時間約 4 小時)

    而燃油車在油價狂降的此刻,商用優勢更為明顯,即使九五汽油價格回升到 30 元,每月費用仍然不到一千元,當然前提是要騎乘七期燃油車,才有每公升 50 公里的低油耗表現。

    Gogoro 商業方案的天價,讓人望之卻步,為什麼會訂出這麼高的金額呢?雖然 Gogoro 官方並未明說,但顯然換電站建置與電池成本,如果在頻繁換電情況下,確實讓 Gogoro 電網不堪負荷,而原本換電的優勢也因為電池來不及充飽而打折,因此官方才祭出強硬手腕。

    Gogoro 第二次政策轉彎,重新定義吃到飽違約標準。(Source:)

    但 Gogoro 滿街跑對於官方來說又是最佳宣傳,所以之前才會容許模糊地帶存在,但是當其他車主開始檢舉之後,官方也不得不有所回應。經過兩次轉彎,最新的定調是,連續兩個月里程超過 1,600 公里且經檢舉才會視為商業使用。換句話說,如果偶爾兼差外送,並不會被追討違約金。

    按照 Gogoro 官方說法,為了 99% 的用戶著想,他們願意放寬認定標準,但也看得出來,換電站與電池流通量不足,才是這次爭議真正的核心。否則何必為了 0.3% 的極少數用戶,而鬧出滿城風雨。

    而充電式機車像是 e-moving 推出的商用版 ie PICKUP,則看準 Gogoro 在這個領域的不足,期望能夠搶佔商用電動機車市場,電池租賃方案分別為 399 元/月基礎型(家充不限里程)、599 元/月輕量型提供 100 分鐘超級充電時數、799 元/月進階型提供 400 分鐘,合約皆為 2 年一簽,車輛定價則為 83,800 元。

    光陽 IONEX 的電池租用方案費用較低,但需要用戶自行在家充電,或是找快充站付費充電。(圖片來源:)

    那麼充電式機車會是商用機車的新未來嗎?這仍要取決於未來充電式機車的性能是否有充足進步,以 IONEX 為例,定價 66,800 元新台幣,極速在 60 km/h 以下,在理想狀態下的滿電續航里程為 60 km,而快充到滿需要一個小時(額外付費),要作為商業使用,恐怕還有所不足。更何況當前資費方案,其實是因為用戶量極少,才推出的短期優惠,未來如果用戶增加,會否漲價,或是加入禁止商用條款也未可知。

    電動車要商用化的另一項挑戰,來自於維修保養體系,對於商業用戶來說,時間就是金錢,而據點少、難預約的電動機車服務站,在這一點就輸給發展許久的油車一大截了。

    以目前兩種電動機車的型態來看,換電系統對於使用者來說比較符合商用需求,但營運商成本較高;充電系統雖然有價格優勢,卻輸在車輛性能與時間彈性上。在可見的將來,全面禁用燃油車幾乎已是定局,若要讓商用機車能夠全面電動化,勢必需要更多的基礎建設(充電站、換電站、保修據點)才能拉低成本與里程焦慮,在那之前,恐怕難有比現在更好的作法。

    最終我們建議,Gogoro 不該繼續在模糊地帶打轉,而是仔細估算商用方案的定價,相信如果能夠將方案價格調降到 1,500 元以下,或是與外送平台、快遞業者合作推優惠方案,讓商用族群可以正正當當的「吃到飽」,而不是每個月精算里程才是正途。試想,如果滿街的外送員都騎電動車,不正是電動車的一大勝利嗎?

    (合作媒體:。首圖來源:)

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

    【其他文章推薦】

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

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

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    南投搬家前需注意的眉眉角角,別等搬了再說!

    新北清潔公司,居家、辦公、裝潢細清專業服務

  • SpringBoot2.x的依賴管理

    SpringBoot2.x的依賴管理

    前提

    這篇文章是《SpringBoot2.x入門》專輯的第1篇文章,使用的SpringBoot版本為2.3.1.RELEASEJDK版本為1.8

    主要梳理一下SpringBoot2.x的依賴關係和依賴的版本管理,依賴版本管理是開發和管理一個SpringBoot項目的前提。

    SpringBoot其實是通過starter的形式,對spring-framework進行裝箱,消除了(但是兼容和保留)原來的XML配置,目的是更加便捷地集成其他框架,打造一個完整高效的開發生態。

    SpringBoot依賴關係

    因為個人不太喜歡Gradle,所以下文都以Maven舉例。

    SpringCloud的版本(SpringCloud的正式版是用倫敦地鐵站或者說倫敦某地名的英文名稱作為版本號,例如比較常用的F版本Finchley就是位於倫敦北部芬奇利)管理不同,SpringBoot的依賴組件發布版本格式是:X.Y.Z.RELEASE。因為SpringBoot組件一般會裝箱為starter,所以組件的依賴GAV一般為:org.springframework.boot:spring-boot-starter-${組件名}:X.Y.Z.RELEASE,其中X是主版本,不同的主版本意味着可以放棄兼容性,也就是SpringBoot1.xSpringBoot2.x不保證兼容性,而組件名一般是代表一類中間件或者一類功能,如data-redisspring-boot-starter-data-redis,提供Redis訪問功能)、jdbcspring-boot-starter-jdbc,提供基於JDBC驅動訪問數據庫功能)等等。以SpringBoot當前最新的發布版本2.3.1.RELEASEorg.springframework.boot:spring-boot-starter:jar:2.3.1.RELEASE為例,用mvn dependency:tree分析它的依賴關係如下:

    這個依賴樹也印證了starter是基於Spring項目裝箱和擴展的。

    SpringBoot依賴管理

    如果使用Spring Initializr創建一個SpringBoot項目的話,那麼會發現項目的POM文件中會加入了一個parent元素:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    

    其實spring-boot-starter-parent相當於作為了當前項目的父模塊,在父模塊裏面管理了當前指定的SpringBoot版本2.3.1.RELEASE所有依賴的第三方庫的統一版本管理,通過spring-boot-starter-parent上溯到最頂層的項目,會找到一個properties元素,裏面統一管理Spring框架和所有依賴到的第三方組件的統一版本號,這樣就能確保對於一個確定的SpringBoot版本,它引入的其他starter不再需要指定版本,同時所有的第三方依賴的版本也是固定的。如項目的POM文件如下:

    <!-- 暫時省略其他的配置屬性 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    

    這樣只需要修改parent元素中的版本號,就能全局更變所有starter的版本號。這種做法其實本質上是把當前項目作為spring-boot-starter-parent的子項目,其實在一定程度上並不靈活。這裏推薦使用另一種方式:通過dependencyManagement元素全局管理SpringBoot版本,適用於單模塊或者多模塊的Maven項目。項目的(父)POM文件如下:

    <!-- spring-boot-guide 父POM -->
    <properties>
        <spring.boot.version>2.3.1.RELEASE</spring.boot.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>
    

    然後需要用到其他starter的時候,只需要在dependencies直接引入即可,不再需要指定版本號,版本號由dependencyManagement中定義的版本號統一管理。

    <!-- spring-boot-guide/ch0-dependency 子POM -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
    

    SpringBoot依賴覆蓋

    有些特殊的情況,可能項目中大部分的starter使用的是相對低的版本,但是由於部分新的功能需要使用到更高版本的個別starter,則需要強制引入該高版本的starter。這裏舉一個例子,項目用到的SpringBoot組件的版本是2.1.5.RELEASE,使用的中間件服務Elasticsearch的版本是7.x,而spring-boot-starter-data-elasticsearch支持的版本如下:

    理論上可以一下子升級SpringBoot2.3.1.RELEASE,其實也可以直接指定spring-boot-starter-data-elasticsearch的版本覆蓋掉全局的SpringBoot組件版本,這裏應用了Maven依賴調解原則

    <!-- 父POM或者全局POM -->
    <properties>
        <spring.boot.version>2.1.5.RELEASE</spring.boot.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
            <version>2.3.1.RELEASE</version>
        </dependency>
    </dependencies>
    

    這樣就能單獨提升spring-boot-starter-data-elasticsearch的版本為2.3.1.RELEASE,其他組件的版本依然保持為2.1.5.RELEASE

    小結

    目前有兩種常用的方式管理SpringBoot組件的版本(兩種方式二選一):

    1. 配置parent元素,通過項目繼承的方式指定SpringBoot組件的版本號,這是Spring Initializr生成的項目中默認的配置方式。
    2. 配置dependencyManagement元素(推薦此方式),通過(父)POM文件統一指定SpringBoot組件的版本號。

    另外,SpringBoot1.x2.x之間有兼容性問題(最明顯的一點是2.x中刪除了1.x中大量的內建類,如果用到了這些SpringBoot中的內建類,容易出現ClassNotFoundException),降級或者升級都有比較大的風險。一般情況下,建議使用同一個大版本進行項目開發,如果確定需要進行大版本切換,請務必做完畢的功能測試。

    (本文完 c-1-d e-a-20200628)

    技術公眾號(《Throwable文摘》,id:throwable-doge),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

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

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

    新北清潔公司,居家、辦公、裝潢細清專業服務

  • pythonic context manager知多少

    Context Managers 是我最喜歡的 python feature 之一,在恰當的時機使用 context manager 使代碼更加簡潔、清晰,更加安全,復用性更好,更加 pythonic。本文簡單介紹一下其使用方法以及常見使用場景。

    本文地址:https://www.cnblogs.com/xybaby/p/13202496.html

    with statement and context manager

    Python’s with statement supports the concept of a runtime context defined by a context manager

    new statement “with” to the Python language to make it possible to factor out standard uses of try/finally statements.

    在 pep0343 中,通過引入 context manager protocol 來支持 With statement , context manager 是用來管理 context(上下文)的,即保證程序要保持一種特定的狀態 — 無論是否發生異常。可以說,context manager 簡化了對 try-finally 的使用,而且更加安全,更加便於使用。

    Transforming Code into Beautiful, Idiomatic Python 中,指出了 context manager 的最顯著的優點:

    • Helps separate business logic from administrative logic
    • Clean, beautiful tools for factoring code and improving code reuse

    最廣為人知的例子,就是通過 with statement 來讀寫文件,代碼如下:

    with open('test.txt') as f:
        contect = f.read()
        handle_content(content)
    

    上面的代碼幾乎等價於

    f = open('test.txt') 
    try:
        contect = f.read()
        handle_content(content)
    finally:
        f.close()
    

    注意,上面的finally的作用就是保證file.close一定會被調用,也就是資源一定會釋放。不過,很多時候,都會忘了去寫這個finally,而 with statement 就徹底避免了這個問題。

    從上述兩段代碼也可以看出,with statement 更加簡潔,而且將核心的業務邏輯(從文件中讀取、處理數據)與其他邏輯(打開、關係文件)相分離,可讀性更強。

    實現context manager protocol

    一個類只要定義了__enter____exit__方法就實現了context manager 協議

    object.__enter__(self)
    Enter the runtime context related to this object. The with statement will bind this method’s return value to the target(s) specified in the as clause of the statement, if any.
    
    object.__exit__(self, exc_type, exc_value, traceback)
    Exit the runtime context related to this object. The parameters describe the exception that caused the context to be exited. If the context was exited without an exception, all three arguments will be None.
    
    If an exception is supplied, and the method wishes to suppress the exception (i.e., prevent it from being propagated), it should return a true value. Otherwise, the exception will be processed normally upon exit from this method.
    
    Note that __exit__() methods should not reraise the passed-in exception; this is the caller’s responsibility.
    

    __enter__方法在進入這個 context 的時候調用,返回值賦值給 with as X 中的 X

    __exit__方法在退出 context 的時候調用,如果沒有異常,后三個參數為 None。如果返回值為 True,則Suppress Exception,所以除非特殊情況都應返回 False。另外注意, __exit__方法本身不應該拋出異常。

    例子:BlockGuard

    在看c++代碼(如mongodb源碼)的時候,經常看見其用 RAII 實現BlockGuard, 用以保證在離開 Block 的時候執行某些動作,同時,也提供手段來取消執行。

    下面用python實現一下:

    class BlockGuard(object):
    	def __init__(self, fn, *args, **kwargs):
    		self._fn = fn
    		self._args = args
    		self._kwargs = kwargs
    		self._canceled = False
    
    	def __enter__(self):
    		return self
    
    	def __exit__(self, exc_type, exc_value, traceback):
    		if not self._canceled:
    			self._fn(*self._args, **self._kwargs)
    		self._fn = None
    		self._args = None
    		self._kwargs = None
    		return False
    
    	def cancel(self):
    		self._canceled = True
    
    
    def foo():
    	print 'sth should be called'
    
    
    def test_BlockGuard(cancel_guard):
    	print 'test_BlockGuard'
    	with BlockGuard(foo) as guard:
    		if cancel_guard:
    			guard.cancel()
    	print 'test_BlockGuard  finish'
    

    用yield實現context manager

    標準庫 contextlib 中提供了一些方法,能夠簡化我們使用 context manager,如 contextlib.contextmanager(func) 使我們
    無需再去實現一個包含__enter__ __exit__方法的類。

    The function being decorated must return a generator-iterator when called. This iterator must yield exactly one value, which will be bound to the targets in the with statement’s as clause, if any.

    例子如下:

    from contextlib import contextmanager
    
    @contextmanager
    def managed_resource(*args, **kwds):
        # Code to acquire resource, e.g.:
        resource = acquire_resource(*args, **kwds)
        try:
            yield resource
        finally:
            # Code to release resource, e.g.:
            release_resource(resource)
    
    >>> with managed_resource(timeout=3600) as resource:
    ...     # Resource is released at the end of this block,
    ...     # even if code in the block raises an exception
    

    需要注意的是:

    • 一定要寫 try finally,才能保證release_resource邏輯一定被調用
    • 除非特殊情況,不再 catch exception,這就跟 __exit__ 一般不返回True一樣

    例子: no_throw

    這是業務開發中的一個需求, 比如觀察者模式,不希望因為其中一個觀察者出了 trace 就影響後續的觀察者,就可以這樣做:

    from contextlib import contextmanager
    
    @contextmanager
    def no_throw(*exceptions):
    	try:
    		yield
    	except exceptions:
    		pass
    
    def notify_observers(seq):
    	for fn in [sum, len, max, min]:
    		with no_throw(Exception):
    			print "%s result %s" % (fn.__name__, fn(seq))
    
    if __name__ == '__main__':
    	notify_observers([])
    

    在python 3.x 的 contexlib 中,就提供了一個contextlib.suppress(*exceptions), 實現了同樣的效果。

    context manager 應用場景

    context manager 誕生的初衷就在於簡化 try-finally,因此就適合應用於在需要 finally 的地方,也就是需要清理的地方,比如

    • 保證資源的安全釋放,如 file、lock、semaphore、network connection 等
    • 臨時操作的復原,如果一段邏輯有 setup、prepare,那麼就會對應 cleanup、teardown。

    對於第一種情況,網絡連接釋放的例子,後面會結合 pymongo 的代碼展示。

    在這裏先來看看第二種用途:保證代碼在一個臨時的、特殊的上下文(context)中執行,且在執行結束之後恢復到之前的上下文環境。

    改變工作目錄

    from contextlib import contextmanager
    import os
    
    @contextmanager
    def working_directory(path):
        current_dir = os.getcwd()
        os.chdir(path)
        try:
            yield
        finally:
            os.chdir(current_dir)
    
    with working_directory("data/stuff"):
        pass
    

    臨時文件、文件夾

    很多時候會產生一堆臨時文件,比如build的中間狀態,這些臨時文件都需要在結束之後清除。

    from tempfile import mkdtemp
    from shutil import rmtree
    
    @contextmanager
    def temporary_dir(*args, **kwds):
        name = mkdtemp(*args, **kwds)
        try:
            yield name
        finally:
            shutil.rmtree(name)
    
    with temporary_dir() as dirname:
        pass
    

    重定向標準輸出、標準錯誤

    @contextmanager
    def redirect_stdout(fileobj):
        oldstdout = sys.stdout
        sys.stdout = fileobj
        try:
            yield fieldobj
        finally:
            sys.stdout = oldstdout
    

    在 python3.x 中,已經提供了 contextlib.redirect_stdout contextlib.redirect_stderr 實現上述功能

    調整logging level

    這個在查問題的適合非常有用,一般生產環境不會輸出 debug level 的日誌,但如果出了問題,可以臨時對某些制定的函數調用輸出debug 日誌

    from contextlib import contextmanager
    import logging
    
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    
    ch = logging.StreamHandler()
    ch.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
    logger.addHandler(ch)
    
    
    @contextmanager
    def change_log_level(level):
    	old_level = logger.getEffectiveLevel()
    	try:
    		logger.setLevel(level)
    		yield
    	finally:
    		logger.setLevel(old_level)
    
    
    def test_logging():
    	logger.debug("this is a debug message")
    	logger.info("this is a info message")
    	logger.warn("this is a warning message")
    
    with change_log_level(logging.DEBUG):
    	test_logging()
    

    pymongo中的context manager使用

    在 pymongo 中,封裝了好幾個 context manager,用以

    • 管理 semaphore
    • 管理 connection
    • 資源清理

    而且,在 pymongo 中,給出了嵌套使用 context manager 的好例子,用來保證 socket 在使用完之後一定返回連接池(pool)。

    # server.py
    @contextlib.contextmanager
    def get_socket(self, all_credentials, checkout=False):
        with self.pool.get_socket(all_credentials, checkout) as sock_info:
            yield sock_info
            
    # pool.py
    @contextlib.contextmanager
    def get_socket(self, all_credentials, checkout=False):
        sock_info = self._get_socket_no_auth()
        try:
            sock_info.check_auth(all_credentials)
            yield sock_info
        except:
            # Exception in caller. Decrement semaphore.
            self.return_socket(sock_info)
            raise
        else:
            if not checkout:
                self.return_socket(sock_info)
    

    可以看到,server.get_socket 調用了 pool.get_socket, 使用 server.get_socket 的代碼完全不了解、也完全不用關心 socket 的釋放細節,如果把 try-except-finally-else 的邏輯移到所有使用socket的地方,代碼就會很醜、很臃腫。

    比如,在mongo_client 中需要使用到 socket:

    with server.get_socket(all_credentials) as sock_info:
        sock_info.authenticate(credentials)
    

    references

    With statement

    Context Managers

    contextlib

    what-is-the-python-with-statement-designed-for

    Transforming Code into Beautiful, Idiomatic Python

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

    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

  • Python 圖像處理 OpenCV (12): Roberts 算子、 Prewitt 算子、 Sobel 算子和 Laplacian 算子邊緣檢測技術

    Python 圖像處理 OpenCV (12): Roberts 算子、 Prewitt 算子、 Sobel 算子和 Laplacian 算子邊緣檢測技術

    前文傳送門:

    「Python 圖像處理 OpenCV (1):入門」

    「Python 圖像處理 OpenCV (2):像素處理與 Numpy 操作以及 Matplotlib 显示圖像」

    「Python 圖像處理 OpenCV (3):圖像屬性、圖像感興趣 ROI 區域及通道處理」

    「Python 圖像處理 OpenCV (4):圖像算數運算以及修改顏色空間」

    「Python 圖像處理 OpenCV (5):圖像的幾何變換」

    「Python 圖像處理 OpenCV (6):圖像的閾值處理」

    「Python 圖像處理 OpenCV (7):圖像平滑(濾波)處理」

    「Python 圖像處理 OpenCV (8):圖像腐蝕與圖像膨脹」

    「Python 圖像處理 OpenCV (9):圖像處理形態學開運算、閉運算以及梯度運算」

    「Python 圖像處理 OpenCV (10):圖像處理形態學之頂帽運算與黑帽運算」

    「Python 圖像處理 OpenCV (11):Canny 算子邊緣檢測技術」

    引言

    前文介紹了 Canny 算子邊緣檢測,本篇繼續介紹 Roberts 算子、 Prewitt 算子、 Sobel 算子和 Laplacian 算子等常用邊緣檢測技術。

    Roberts 算子

    Roberts 算子,又稱羅伯茨算子,是一種最簡單的算子,是一種利用局部差分算子尋找邊緣的算子。他採用對角線方向相鄰兩象素之差近似梯度幅值檢測邊緣。檢測垂直邊緣的效果好於斜向邊緣,定位精度高,對噪聲敏感,無法抑制噪聲的影響。

    1963年, Roberts 提出了這種尋找邊緣的算子。 Roberts 邊緣算子是一個 2×2 的模版,採用的是對角方向相鄰的兩個像素之差。

    Roberts 算子的模板分為水平方向和垂直方向,如下所示,從其模板可以看出, Roberts 算子能較好的增強正負 45 度的圖像邊緣。

    \[dx = \left[ \begin{matrix} -1 & 0\\ 0 & 1 \\ \end{matrix} \right] \]

    \[dy = \left[ \begin{matrix} 0 & -1\\ 1 & 0 \\ \end{matrix} \right] \]

    Roberts 算子在水平方向和垂直方向的計算公式如下:

    \[d_x(i, j) = f(i + 1, j + 1) – f(i, j) \]

    \[d_y(i, j) = f(i, j + 1) – f(i + 1, j) \]

    Roberts 算子像素的最終計算公式如下:

    \[S = \sqrt{d_x(i, j)^2 + d_y(i, j)^2} \]

    今天的公式都是小學生水平,千萬別再說看不懂了。

    實現 Roberts 算子,我們主要通過 OpenCV 中的 filter2D() 這個函數,這個函數的主要功能是通過卷積核實現對圖像的卷積運算:

    def filter2D(src, ddepth, kernel, dst=None, anchor=None, delta=None, borderType=None)
    
    • src: 輸入圖像
    • ddepth: 目標圖像所需的深度
    • kernel: 卷積核

    接下來開始寫代碼,首先是圖像的讀取,並把這個圖像轉化成灰度圖像,這個沒啥好說的:

    # 讀取圖像
    img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
    rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)
    
    # 灰度化處理圖像
    grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    

    然後是使用 Numpy 構建卷積核,並對灰度圖像在 x 和 y 的方向上做一次卷積運算:

    # Roberts 算子
    kernelx = np.array([[-1, 0], [0, 1]], dtype=int)
    kernely = np.array([[0, -1], [1, 0]], dtype=int)
    
    x = cv.filter2D(grayImage, cv.CV_16S, kernelx)
    y = cv.filter2D(grayImage, cv.CV_16S, kernely)
    

    注意:在進行了 Roberts 算子處理之後,還需要調用convertScaleAbs()函數計算絕對值,並將圖像轉換為8位圖進行显示,然後才能進行圖像融合:

    # 轉 uint8 ,圖像融合
    absX = cv.convertScaleAbs(x)
    absY = cv.convertScaleAbs(y)
    Roberts = cv.addWeighted(absX, 0.5, absY, 0.5, 0)
    

    最後是通過 pyplot 將圖像显示出來:

    # 显示圖形
    titles = ['原始圖像', 'Roberts算子']
    images = [rgb_img, Roberts]
    
    for i in range(2):
        plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()
    

    最終結果如下:

    Prewitt 算子

    Prewitt 算子是一種一階微分算子的邊緣檢測,利用像素點上下、左右鄰點的灰度差,在邊緣處達到極值檢測邊緣,去掉部分偽邊緣,對噪聲具有平滑作用。

    由於 Prewitt 算子採用 3 * 3 模板對區域內的像素值進行計算,而 Robert 算子的模板為 2 * 2 ,故 Prewitt 算子的邊緣檢測結果在水平方向和垂直方向均比 Robert 算子更加明顯。Prewitt算子適合用來識別噪聲較多、灰度漸變的圖像。

    Prewitt 算子的模版如下:

    \[dx = \left[ \begin{matrix} 1 & 0 & -1\\ 1 & 0 & -1\\ 1 & 0 & -1\\ \end{matrix} \right] \]

    \[dy = \left[ \begin{matrix} -1 & -1 & -1\\ 0 & 0 & 0\\ 1 & 1 & 1\\ \end{matrix} \right] \]

    在代碼實現上, Prewitt 算子的實現過程與 Roberts 算子比較相似,我就不多介紹,直接貼代碼了:

    import cv2 as cv
    import numpy as np
    import matplotlib.pyplot as plt
    
    # 讀取圖像
    img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
    rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)
    
    # 灰度化處理圖像
    grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    
    # Prewitt 算子
    kernelx = np.array([[1,1,1],[0,0,0],[-1,-1,-1]],dtype=int)
    kernely = np.array([[-1,0,1],[-1,0,1],[-1,0,1]],dtype=int)
    
    x = cv.filter2D(grayImage, cv.CV_16S, kernelx)
    y = cv.filter2D(grayImage, cv.CV_16S, kernely)
    
    # 轉 uint8 ,圖像融合
    absX = cv.convertScaleAbs(x)
    absY = cv.convertScaleAbs(y)
    Prewitt = cv.addWeighted(absX, 0.5, absY, 0.5, 0)
    
    # 用來正常显示中文標籤
    plt.rcParams['font.sans-serif'] = ['SimHei']
    
    # 显示圖形
    titles = ['原始圖像', 'Prewitt 算子']
    images = [rgb_img, Prewitt]
    
    for i in range(2):
        plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()
    

    從結果上來看, Prewitt 算子圖像銳化提取的邊緣輪廓,其效果圖的邊緣檢測結果比 Robert 算子更加明顯。

    Sobel 算子

    Sobel 算子的中文名稱是索貝爾算子,是一種用於邊緣檢測的離散微分算子,它結合了高斯平滑和微分求導。

    Sobel 算子在 Prewitt 算子的基礎上增加了權重的概念,認為相鄰點的距離遠近對當前像素點的影響是不同的,距離越近的像素點對應當前像素的影響越大,從而實現圖像銳化並突出邊緣輪廓。

    算法模版如下:

    \[dx = \left[ \begin{matrix} 1 & 0 & -1\\ 2 & 0 & -2\\ 1 & 0 & -1\\ \end{matrix} \right] \]

    \[dy = \left[ \begin{matrix} -1 & -2 & -1\\ 0 & 0 & 0\\ 1 & 2 & 1\\ \end{matrix} \right] \]

    Sobel 算子根據像素點上下、左右鄰點灰度加權差,在邊緣處達到極值這一現象檢測邊緣。對噪聲具有平滑作用,提供較為精確的邊緣方向信息。因為 Sobel 算子結合了高斯平滑和微分求導(分化),因此結果會具有更多的抗噪性,當對精度要求不是很高時, Sobel 算子是一種較為常用的邊緣檢測方法。

    Sobel 算子近似梯度的大小的計算公式如下:

    \[G = \sqrt{d_X^2 + d_y^2} \]

    梯度方向的計算公式如下:

    \[\theta = \tan^{-1}(\frac {d_x}{d_y}) \]

    如果以上的角度 θ 等於零,即代表圖像該處擁有縱向邊緣,左方較右方暗。

    在 Python 中,為我們提供了 Sobel() 函數進行運算,整體處理過程和前面的類似,代碼如下:

    import cv2 as cv
    import matplotlib.pyplot as plt
    
    # 讀取圖像
    img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
    rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)
    
    # 灰度化處理圖像
    grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    
    # Sobel 算子
    x = cv.Sobel(grayImage, cv.CV_16S, 1, 0)
    y = cv.Sobel(grayImage, cv.CV_16S, 0, 1)
    
    # 轉 uint8 ,圖像融合
    absX = cv.convertScaleAbs(x)
    absY = cv.convertScaleAbs(y)
    Sobel = cv.addWeighted(absX, 0.5, absY, 0.5, 0)
    
    # 用來正常显示中文標籤
    plt.rcParams['font.sans-serif'] = ['SimHei']
    
    # 显示圖形
    titles = ['原始圖像', 'Sobel 算子']
    images = [rgb_img, Sobel]
    
    for i in range(2):
        plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()
    

    Laplacian 算子

    拉普拉斯( Laplacian )算子是 n 維歐幾里德空間中的一個二階微分算子,常用於圖像增強領域和邊緣提取。

    Laplacian 算子的核心思想:判斷圖像中心像素灰度值與它周圍其他像素的灰度值,如果中心像素的灰度更高,則提升中心像素的灰度;反之降低中心像素的灰度,從而實現圖像銳化操作。

    在實現過程中, Laplacian 算子通過對鄰域中心像素的四方向或八方向求梯度,再將梯度相加起來判斷中心像素灰度與鄰域內其他像素灰度的關係,最後通過梯度運算的結果對像素灰度進行調整。

    Laplacian 算子分為四鄰域和八鄰域,四鄰域是對鄰域中心像素的四方向求梯度,八鄰域是對八方向求梯度。

    四鄰域模板如下:

    \[H = \left[ \begin{matrix} 0 & -1 & 0\\ -1 & 4 & -1\\ 0 & -1 & 0\\ \end{matrix} \right] \]

    八鄰域模板如下:

    \[H = \left[ \begin{matrix} -1 & -1 & -1\\ -1 & 4 & -1\\ -1 & -1 & -1\\ \end{matrix} \right] \]

    通過模板可以發現,當鄰域內像素灰度相同時,模板的卷積運算結果為0;當中心像素灰度高於鄰域內其他像素的平均灰度時,模板的卷積運算結果為正數;當中心像素的灰度低於鄰域內其他像素的平均灰度時,模板的卷積為負數。對卷積運算的結果用適當的衰弱因子處理並加在原中心像素上,就可以實現圖像的銳化處理。

    在 OpenCV 中, Laplacian 算子被封裝在 Laplacian() 函數中,其主要是利用Sobel算子的運算,通過加上 Sobel 算子運算出的圖像 x 方向和 y 方向上的導數,得到輸入圖像的圖像銳化結果。

    import cv2 as cv
    import matplotlib.pyplot as plt
    
    # 讀取圖像
    img = cv.imread('maliao.jpg', cv.COLOR_BGR2GRAY)
    rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)
    
    # 灰度化處理圖像
    grayImage = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    
    # Laplacian
    dst = cv.Laplacian(grayImage, cv.CV_16S, ksize = 3)
    Laplacian = cv.convertScaleAbs(dst)
    
    # 用來正常显示中文標籤
    plt.rcParams['font.sans-serif'] = ['SimHei']
    
    # 显示圖形
    titles = ['原始圖像', 'Laplacian 算子']
    images = [rgb_img, Laplacian]
    
    for i in range(2):
        plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()
    

    最後

    邊緣檢測算法主要是基於圖像強度的一階和二階導數,但導數通常對噪聲很敏感,因此需要採用濾波器來過濾噪聲,並調用圖像增強或閾值化算法進行處理,最後再進行邊緣檢測。

    最後我先使用高斯濾波去噪之後,再進行邊緣檢測:

    import cv2 as cv
    import numpy as np
    import matplotlib.pyplot as plt
    
    # 讀取圖像
    img = cv.imread('maliao.jpg')
    rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)
    
    # 灰度化處理圖像
    gray_image = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    
    # 高斯濾波
    gaussian_blur = cv.GaussianBlur(gray_image, (3, 3), 0)
    
    # Roberts 算子
    kernelx = np.array([[-1, 0], [0, 1]], dtype = int)
    kernely = np.array([[0, -1], [1, 0]], dtype = int)
    x = cv.filter2D(gaussian_blur, cv.CV_16S, kernelx)
    y = cv.filter2D(gaussian_blur, cv.CV_16S, kernely)
    absX = cv.convertScaleAbs(x)
    absY = cv.convertScaleAbs(y)
    Roberts = cv.addWeighted(absX, 0.5, absY, 0.5, 0)
    
    # Prewitt 算子
    kernelx = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]], dtype=int)
    kernely = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=int)
    x = cv.filter2D(gaussian_blur, cv.CV_16S, kernelx)
    y = cv.filter2D(gaussian_blur, cv.CV_16S, kernely)
    absX = cv.convertScaleAbs(x)
    absY = cv.convertScaleAbs(y)
    Prewitt = cv.addWeighted(absX, 0.5, absY, 0.5, 0)
    
    # Sobel 算子
    x = cv.Sobel(gaussian_blur, cv.CV_16S, 1, 0)
    y = cv.Sobel(gaussian_blur, cv.CV_16S, 0, 1)
    absX = cv.convertScaleAbs(x)
    absY = cv.convertScaleAbs(y)
    Sobel = cv.addWeighted(absX, 0.5, absY, 0.5, 0)
    
    # 拉普拉斯算法
    dst = cv.Laplacian(gaussian_blur, cv.CV_16S, ksize = 3)
    Laplacian = cv.convertScaleAbs(dst)
    
    # 展示圖像
    titles = ['Source Image', 'Gaussian Image', 'Roberts Image',
              'Prewitt Image','Sobel Image', 'Laplacian Image']
    images = [rgb_img, gaussian_blur, Roberts, Prewitt, Sobel, Laplacian]
    for i in np.arange(6):
       plt.subplot(2, 3, i+1), plt.imshow(images[i], 'gray')
       plt.title(titles[i])
       plt.xticks([]), plt.yticks([])
    plt.show()
    

    示例代碼

    如果有需要獲取源碼的同學可以在公眾號回復「OpenCV」進行獲取。

    參考

    https://blog.csdn.net/Eastmount/article/details/89001702

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

    【其他文章推薦】

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

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

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    南投搬家前需注意的眉眉角角,別等搬了再說!

    新北清潔公司,居家、辦公、裝潢細清專業服務

  • YoyoGo基於ASP.NET Core設計的Golang實現

    YoyoGo基於ASP.NET Core設計的Golang實現

    YoyoGo

    YoyoGo 是一個用 Go 編寫的簡單,輕便,快速的 微服務框架,目前已實現了Web框架的能力,但是底層設計已支持多種服務架構。

    Github

    https://github.com/yoyofx/yoyogo

    特色

    • 漂亮又快速的路由器
    • 中間件支持 (handler func & custom middleware)
    • 對 REST API 友好
    • 支持 MVC 模式
    • 受到許多出色的 Go Web 框架的啟發

    框架安裝

    go get github.com/yoyofx/yoyogo
    

    安裝依賴 (由於某些原因國內下載不了依賴)

    go version < 1.13

    window 下在 cmd 中執行:
    set GO111MODULE=on
    set  GOPROXY=https://goproxy.cn
    
    linux  下執行:
    export GO111MODULE=on
    export GOPROXY=https://goproxy.cn
    

    go version >= 1.13

    go env -w GOPROXY=https://goproxy.cn,direct
    

    簡單的例子

    package main
    import ...
    
    func main() {
        YoyoGo.CreateDefaultBuilder(func(router Router.IRouterBuilder) {
            router.GET("/info",func (ctx *Context.HttpContext) {    // 支持Group方式
                ctx.JSON(200, Context.M{"info": "ok"})
            })
        }).Build().Run()       //默認端口號 :8080
    }
    

    實現進度

    標準功能

    • [X] 打印Logo和日誌(YoyoGo)
    • [X] 統一程序輸入參數和環境變量 (YoyoGo)
    • [X] 簡單路由器綁定句柄功能
    • [X] HttpContext 上下文封裝(請求,響應)
    • [X] 靜態文件端點(靜態文件服務器)
    • [X] JSON 序列化結構(Context.M)
    • [X] 獲取請求文件並保存
    • [X] 獲取請求數據(form-data,x-www-form-urlencoded,Json ,XML,Protobuf 等)
    • [X] Http 請求的綁定模型(Url, From,JSON,XML,Protobuf)

    響應渲染功能

    • [X] Render Interface
    • [X] JSON Render
    • [X] JSONP Render
    • [X] Indented Json Render
    • [X] Secure Json Render
    • [X] Ascii Json Render
    • [X] Pure Json Render
    • [X] Binary Data Render
    • [X] TEXT
    • [X] Protobuf
    • [X] MessagePack
    • [X] XML
    • [X] YAML
    • [X] File
    • [X] Image
    • [X] Template
    • [X] Auto formater Render

    中間件

    • [X] Logger
    • [X] StaticFile
    • [X] Router Middleware
    • [ ] Session
    • [ ] CORS
    • [ ] GZip
    • [X] Binding
    • [ ] Binding Valateion

    路由

    • [x] GET,POST,HEAD,PUT,DELETE 方法支持
    • [x] 路由解析樹與表達式支持
    • [x] RouteData路由數據 (/api/:version/) 與 Binding的集成
    • [x] 路由組功能
    • [ ] MVC默認模板功能
    • [ ] 路由過濾器 Filter

    MVC

    • [x] 路由請求觸發Controller&Action
    • [X] Action方法參數綁定
    • [ ] 內部對象的DI化
    • [ ] 關鍵對象的參數傳遞

    Dependency injection

    • [X] 抽象集成第三方DI框架
    • [X] MVC模式集成
    • [X] 框架級的DI支持功能

    擴展

    • [ ] 配置
    • [ ] WebSocket
    • [ ] JWT
    • [ ] swagger
    • [ ] GRpc
    • [ ] OAuth2
    • [ ] Prometheus
    • [ ] 安全

    進階範例

    package main
    import ...
    
    func main() {
    	webHost := CreateCustomWebHostBuilder().Build()
    	webHost.Run()
    }
    
    // 自定義HostBuilder並支持 MVC 和 自動參數綁定功能,簡單情況也可以直接使用CreateDefaultBuilder 。
    func CreateCustomBuilder() *Abstractions.HostBuilder {
    	return YoyoGo.NewWebHostBuilder().
    		SetEnvironment(Context.Prod).
    		UseFastHttp().
    		//UseServer(YoyoGo.DefaultHttps(":8080", "./Certificate/server.pem", "./Certificate/server.key")).
    		Configure(func(app *YoyoGo.WebApplicationBuilder) {
    			app.UseStatic("Static")
    			app.UseEndpoints(registerEndpointRouterConfig)
    			app.UseMvc(func(builder *Mvc.ControllerBuilder) {
    				builder.AddController(contollers.NewUserController)
    			})
    		}).
    		ConfigureServices(func(serviceCollection *DependencyInjection.ServiceCollection) {
    			serviceCollection.AddTransientByImplements(models.NewUserAction, new(models.IUserAction))
    		}).
    		OnApplicationLifeEvent(getApplicationLifeEvent)
    }
    
    //region endpoint 路由綁定函數
    func registerEndpoints(router Router.IRouterBuilder) {
    	router.GET("/error", func(ctx *Context.HttpContext) {
    		panic("http get error")
    	})
    
        //POST 請求: /info/:id ?q1=abc&username=123
    	router.POST("/info/:id", func (ctx *Context.HttpContext) {
            qs_q1 := ctx.Query("q1")
            pd_name := ctx.Param("username")
    
            userInfo := &UserInfo{}
            
            _ = ctx.Bind(userInfo)    // 手動綁定請求對象
    
            strResult := fmt.Sprintf("Name:%s , Q1:%s , bind: %s", pd_name, qs_q1, userInfo)
    
            ctx.JSON(200, Std.M{"info": "hello world", "result": strResult})
        })
    
        // 路由組功能實現綁定 GET 請求:  /v1/api/info
    	router.Group("/v1/api", func(router *Router.RouterGroup) {
    		router.GET("/info", func (ctx *Context.HttpContext) {
    	        ctx.JSON(200, Std.M{"info": "ok"})
            })
    	})
        
        // GET 請求: HttpContext.RequiredServices獲取IOC對象
    	router.GET("/ioc", func (ctx *Context.HttpContext) {
            var userAction models.IUserAction
            _ = ctx.RequiredServices.GetService(&userAction)
            ctx.JSON(200, Std.M{"info": "ok " + userAction.Login("zhang")})
        })
    }
    
    //endregion
    
    //region 請求對象
    type UserInfo struct {
    	UserName string `param:"username"`
    	Number   string `param:"q1"`
    	Id       string `param:"id"`
    }
    
    // ----------------------------------------- MVC 定義 ------------------------------------------------------
    
    // 定義Controller
    type UserController struct {
    	*Controller.ApiController
    	userAction models.IUserAction    // IOC 對象參數
    }
    
    // 構造器依賴注入
    func NewUserController(userAction models.IUserAction) *UserController {
    	return &UserController{userAction: userAction}
    }
    
    // 請求對象的參數化綁定
    type RegiserRequest struct {
    	Controller.RequestParam
    	UserName string `param:"username"`
    	Password string `param:"password"`
    }
    
    // Register函數自動綁定參數
    func (this *UserController) Register(ctx *Context.HttpContext, request *RegiserRequest) ActionResult.IActionResult {
    	result := Controller.ApiResult{Success: true, Message: "ok", Data: request}
    	return ActionResult.Json{Data: result}
    }
    
    // use userAction interface by ioc  
    func (this *UserController) GetInfo() Controller.ApiResult {
    	return this.OK(this.userAction.Login("zhang"))
    }
    
    
    // Web程序的開始與停止事件
    func fireApplicationLifeEvent(life *YoyoGo.ApplicationLife) {
    	printDataEvent := func(event YoyoGo.ApplicationEvent) {
    		fmt.Printf("[yoyogo] Topic: %s; Event: %v\n", event.Topic, event.Data)
    	}
    	for {
    		select {
    		case ev := <-life.ApplicationStarted:
    			go printDataEvent(ev)
    		case ev := <-life.ApplicationStopped:
    			go printDataEvent(ev)
    			break
    		}
    	}
    }
    
    

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

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

    新北清潔公司,居家、辦公、裝潢細清專業服務

  • 重學 Java 設計模式:實戰備忘錄模式「模擬互聯網系統上線過程中,配置文件回滾場景」

    重學 Java 設計模式:實戰備忘錄模式「模擬互聯網系統上線過程中,配置文件回滾場景」

    作者:小傅哥
    博客:https://bugstack.cn – 原創系列專題文章

    沉澱、分享、成長,讓自己和他人都能有所收穫!

    一、前言

    實現不了是研發的借口?

    實現不了,有時候是功能複雜度較高難以實現,有時候是工期較短實現不完。而編碼的行為又是一個不太好量化的過程,同樣一個功能每個人的實現方式不一樣,遇到開發問題解決問題的速度也不一樣。除此之外還很不好給產品解釋具體為什麼要這個工期時間,這就像蓋樓的圖紙最終要多少水泥砂漿一樣。那麼這時研發會盡可能的去通過一些經驗,制定流程規範、設計、開發、評審等,確定一個可以完成的時間範圍,又避免風險的時間點后。再被壓縮,往往會出一些矛盾點,能壓縮要解釋為什麼之前要那麼多時間,不能壓縮又有各方不斷施加的壓力。因此有時候不一定是借口,是要考慮如何讓整個團隊健康的發展。

    鼓勵有時比壓力要重要!

    在學習的過程中,很多時候我們聽到的都是,你要怎樣,怎樣,你瞧瞧誰誰誰,哪怕今天聽不到這樣的聲音了,但因為曾經反覆聽到過而導致內心抗拒。雖然也知道自己要去學,但是很難堅持,學着學着就沒有了方向,看到還有那麼多不會的就更慌了,以至於最後心態崩了,更不願意學。其實程序員的壓力並不小,想成長几乎是需要一直的學習,就像似乎再也不敢說精通java了一樣,知識量實在是隨着學習的深入,越來越深,越來越廣。所以需要,開心學習,快樂成長!

    臨陣的你好像一直很着急!

    經常的聽到;老師明天就要了你幫我弄弄吧你給我寫一下完事我就學這次着急現在這不是沒時間學嗎快給我看看。其實看到的類似的還有很多,很納悶你的着急怎麼來的,不太可能,人在家中坐,禍從天上落。老師怎麼就那個時間找你了,老闆怎麼就今天管你要了,還不是日積月累你沒有學習,臨時抱佛腳亂着急!即使後來真的有人幫你了,但最好不要放鬆,要儘快學會,躲得過初一還有初二呢!

    二、開發環境

    1. JDK 1.8
    2. Idea + Maven
    3. 涉及工程一個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
    工程 描述
    itstack-demo-design-17-00 開發配置文件備忘錄

    三、備忘錄模式介紹

    備忘錄模式是以可以恢復或者說回滾,配置、版本、悔棋為核心功能的設計模式,而這種設計模式屬於行為模式。在功能實現上是以不破壞原對象為基礎增加備忘錄操作類,記錄原對象的行為從而實現備忘錄模式。

    這個設計在我們平常的生活或者開發中也是比較常見的,比如:後悔葯、孟婆湯(一下回滾到0),IDEA編輯和撤銷、小霸王遊戲機存檔。當然還有我們非常常見的Photoshop,如下;

    四、案例場景模擬

    在本案例中我們模擬系統在發布上線的過程中記錄線上配置文件用於緊急回滾

    在大型互聯網公司系統的發布上線一定是易用、安全、可處理緊急狀況的,同時為了可以隔離線上和本地環境,一般會把配置文件抽取出來放到線上,避免有人誤操作導致本地的配置內容發布出去。同時線上的配置文件也會在每次變更的時候進行記錄,包括;版本號、時間、MD5、內容信息和操作人。

    在後續上線時如果發現緊急問題,系統就會需要回滾操作,如果執行回滾那麼也可以設置配置文件是否回滾。因為每一個版本的系統可能會隨着帶着一些配置文件的信息,這個時候就可以很方便的讓系統與配置文件一起回滾操作。

    我們接下來就使用備忘錄模式,模擬如何記錄配置文件信息。實際的使用過程中還會將信息存放到庫中進行保存,這裏暫時只是使用內存記錄。

    五、備忘錄模式記錄配置文件版本信息

    備忘錄的設計模式實現方式,重點在於不更改原有類的基礎上,增加備忘錄類存放記錄。可能平時雖然不一定非得按照這個設計模式的代碼結構來實現自己的需求,但是對於功能上可能也完成過類似的功能,記錄系統的信息。

    除了現在的這個案例外,還可以是運營人員在後台erp創建活動對信息的記錄,方便運營人員可以上下修改自己的版本,而不至於因為誤操作而丟失信息。

    1. 工程結構

    itstack-demo-design-17-00
    └── src
        ├── main
        │   └── java
        │       └── org.itstack.demo.design
        │           ├── Admin.java
        │           ├── ConfigFile.java
        │           ├── ConfigMemento.java
        │           └── ConfigOriginator.java
        └── test
            └── java
                └── org.itstack.demo.design.test
                    └── ApiTest.java
    

    備忘錄模式模型結構

    • 以上是工程結構的一個類圖,其實相對來說並不複雜,除了原有的配置類(ConfigFile)以外,只新增加了三個類。
    • ConfigMemento:備忘錄類,相當於是對原有配置類的擴展
    • ConfigOriginator:記錄者類,獲取和返回備忘錄類對象信息
    • Admin:管理員類,用於操作記錄備忘信息,比如你一些列的順序執行了什麼或者某個版本下的內容信息

    2. 代碼實現

    2.1 配置信息類

    public class ConfigFile {
    
        private String versionNo; // 版本號
        private String content;   // 內容
        private Date dateTime;    // 時間
        private String operator;  // 操作人
        
        // ...get/set
    }
    
    • 配置類可以是任何形式的,這裏只是簡單的描述了一個基本的配置內容信息。

    2.2 備忘錄類

    public class ConfigMemento {
    
        private ConfigFile configFile;
    
        public ConfigMemento(ConfigFile configFile) {
            this.configFile = configFile;
        }
    
        public ConfigFile getConfigFile() {
            return configFile;
        }
    
        public void setConfigFile(ConfigFile configFile) {
            this.configFile = configFile;
        }
        
    }
    
    • 備忘錄是對原有配置類的擴展,可以設置和獲取配置信息。

    2.3 記錄者類

    public class ConfigOriginator {
    
        private ConfigFile configFile;
    
        public ConfigFile getConfigFile() {
            return configFile;
        }
    
        public void setConfigFile(ConfigFile configFile) {
            this.configFile = configFile;
        }
    
        public ConfigMemento saveMemento(){
            return new ConfigMemento(configFile);
        }
    
        public void getMemento(ConfigMemento memento){
            this.configFile = memento.getConfigFile();
        }
    
    }
    
    • 記錄者類除了對ConfigFile配置類增加了獲取和設置方法外,還增加了保存saveMemento()、獲取getMemento(ConfigMemento memento)
    • saveMemento:保存備忘錄的時候會創建一個備忘錄信息,並返回回去,交給管理者處理。
    • getMemento:獲取的之後並不是直接返回,而是把備忘錄的信息交給現在的配置文件this.configFile,這部分需要注意。

    2.4 管理員類

    public class Admin {
    
        private int cursorIdx = 0;
        private List<ConfigMemento> mementoList = new ArrayList<ConfigMemento>();
        private Map<String, ConfigMemento> mementoMap = new ConcurrentHashMap<String, ConfigMemento>();
    
        public void append(ConfigMemento memento) {
            mementoList.add(memento);
            mementoMap.put(memento.getConfigFile().getVersionNo(), memento);
            cursorIdx++;
        }
    
        public ConfigMemento undo() {
            if (--cursorIdx <= 0) return mementoList.get(0);
            return mementoList.get(cursorIdx);
        }
    
        public ConfigMemento redo() {
            if (++cursorIdx > mementoList.size()) return mementoList.get(mementoList.size() - 1);
            return mementoList.get(cursorIdx);
        }
    
        public ConfigMemento get(String versionNo){
            return mementoMap.get(versionNo);
        }
    
    }
    
    • 在這個類中主要實現的核心功能就是記錄配置文件信息,也就是備忘錄的效果,之後提供可以回滾和獲取的方法,拿到備忘錄的具體內容。
    • 同時這裏設置了兩個數據結構來存放備忘錄,實際使用中可以按需設置。List<ConfigMemento>Map<String, ConfigMemento>
    • 最後是提供的備忘錄操作方法;存放(append)、回滾(undo)、返回(redo)、定向獲取(get),這樣四個操作方法。

    3. 測試驗證

    3.1 編寫測試類

    @Test
    public void test() {
        Admin admin = new Admin();
        ConfigOriginator configOriginator = new ConfigOriginator();
        configOriginator.setConfigFile(new ConfigFile("1000001", "配置內容A=哈哈", new Date(), "小傅哥"));
        admin.append(configOriginator.saveMemento()); // 保存配置
        configOriginator.setConfigFile(new ConfigFile("1000002", "配置內容A=嘻嘻", new Date(), "小傅哥"));
        admin.append(configOriginator.saveMemento()); // 保存配置
        configOriginator.setConfigFile(new ConfigFile("1000003", "配置內容A=么么", new Date(), "小傅哥"));
        admin.append(configOriginator.saveMemento()); // 保存配置
        configOriginator.setConfigFile(new ConfigFile("1000004", "配置內容A=嘿嘿", new Date(), "小傅哥"));
        admin.append(configOriginator.saveMemento()); // 保存配置  
    
        // 歷史配置(回滾)
        configOriginator.getMemento(admin.undo());
        logger.info("歷史配置(回滾)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));  
    
        // 歷史配置(回滾)
        configOriginator.getMemento(admin.undo());
        logger.info("歷史配置(回滾)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));  
    
        // 歷史配置(前進)
        configOriginator.getMemento(admin.redo());
        logger.info("歷史配置(前進)redo:{}", JSON.toJSONString(configOriginator.getConfigFile()));   
    
        // 歷史配置(獲取)
        configOriginator.getMemento(admin.get("1000002"));
        logger.info("歷史配置(獲取)get:{}", JSON.toJSONString(configOriginator.getConfigFile()));
    }
    
    • 這個設計模式的學習有一部分重點是體現在了單元測試類上,這裏包括了四次的信息存儲和備忘錄歷史配置操作。
    • 通過上面添加了四次配置后,下面分別進行操作是;回滾1次再回滾1次之後向前進1次最後是獲取指定的版本配置。具體的效果可以參考測試結果。

    3.2 測試結果

    23:12:09.512 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(回滾)undo:{"content":"配置內容A=嘿嘿","dateTime":159209829432,"operator":"小傅哥","versionNo":"1000004"}
    23:12:09.514 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(回滾)undo:{"content":"配置內容A=么么","dateTime":159209829432,"operator":"小傅哥","versionNo":"1000003"}
    23:12:09.514 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(前進)redo:{"content":"配置內容A=嘿嘿","dateTime":159209829432,"operator":"小傅哥","versionNo":"1000004"}
    23:12:09.514 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(獲取)get:{"content":"配置內容A=嘻嘻","dateTime":159320989432,"operator":"小傅哥","versionNo":"1000002"}
    
    Process finished with exit code 0
    
    • 從測試效果上可以看到,歷史配置按照我們的指令進行了回滾和前進,以及最終通過指定的版本進行獲取,符合預期結果。

    六、總結

    • 此種設計模式的方式可以滿足在不破壞原有屬性類的基礎上,擴充了備忘錄的功能。雖然和我們平時使用的思路是一樣的,但在具體實現上還可以細細品味,這樣的方式在一些源碼中也有所體現。
    • 在以上的實現中我們是將配置模擬存放到內存中,如果關機了會導致配置信息丟失,因為在一些真實的場景里還是需要存放到數據庫中。那麼此種存放到內存中進行回復的場景也不是沒有,比如;Photoshop、運營人員操作ERP配置活動,那麼也就是即時性的一般不需要存放到庫中進行恢復。另外如果是使用內存方式存放備忘錄,需要考慮存儲問題,避免造成內存大量消耗。
    • 設計模式的學習都是為了更好的寫出可擴展、可管理、易維護的代碼,而這個學習的過程需要自己不斷的嘗試實際操作,理論的知識與實際結合還有很長一段距離。切記多多上手!

    七、推薦閱讀

    • 1. 重學 Java 設計模式:實戰工廠方法模式「多種類型商品不同接口,統一發獎服務搭建場景」
    • 2. 重學 Java 設計模式:實戰原型模式「上機考試多套試,每人題目和答案亂序排列場景」
    • 3. 重學 Java 設計模式:實戰橋接模式「多支付渠道(微信、支付寶)與多支付模式(刷臉、指紋)場景」
    • 4. 重學 Java 設計模式:實戰組合模式「營銷差異化人群發券,決策樹引擎搭建場景」
    • 5. 重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」
    • 6. 重學 Java 設計模式:實戰享元模式「基於Redis秒殺,提供活動與庫存信息查詢場景」
    • 7. 重學 Java 設計模式:實戰備忘錄模式「模擬互聯網系統上線過程中,配置文件回滾場景」

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

    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

  • Shader專題:卡通着色(一)控制顏色的藝術

    Shader專題:卡通着色(一)控制顏色的藝術

    什麼是 Shader?

    關於什麼是 Shader ,各種百科各種教程都有說過,但是今天我們就從一個另一個角度去試着理解什麼是 Shader?

    我們先看下 Shade 的英文意思,如下:
    v.給…遮擋(光線);把…塗暗

    其中 把…塗暗 更貼近我們想要的意思。
    所以:Shader 這個單詞從字面上理解,就是把什麼東西塗暗。

    再強調一次:Shader 從單詞字面上理解,就是把什麼東西塗暗。
    再強調一次:把什麼東西塗暗的就是 Shader,就是着色器。

    Shader 把什麼塗暗了?

    當然是遊戲世界的各個物體,總所周知:有光明就有黑暗,有光照物體就有明暗對比,同時也會有陰影,而 Shader 之所以叫 Shader 是因為起初的時候,Shader 就是用來給物體增加明暗對比的,有了明暗對比,物體在遊戲世界中就會更加立體,從而畫面會更加真實。

    所以 Shader 的作用就是給物體添加明暗對比。

    Shader 為什麼叫 Shader

    當然以上純屬個人推測。現在 Shader 不止可以給物體添加明暗對比,而且還可以做很多濾鏡效果,也可以做很多性能優化(比如減少包大小、減少圖片內存等)的事情。

    也許,一開始給 Shader 起名叫 Shader 的時候,Shader 功能非常有限,僅僅只是給物體添加明暗對比(也就是光照計算),後來由於硬件和軟件的發展, 很多離線渲染(電影 CG)的算法都逐步應用在實時渲染(主要是 遊戲 和3D 仿真等),Shader 能做的事情就越來越多,發展到今天,Shader 主要的功能並不只有光照計算。這樣導致,在概念理解上給很多初學者增加了很多阻礙。

    教練有一次聽過一位搞圖形學的朋友說:“我們搞實時渲染的都是那些搞視頻(離線渲染)玩剩的”。

    Shader 是着色器

    什麼是 Shader,中文叫做着色器,也就是給物體上色的意思,也就是說寫 Shader 就是給物體上色的藝術。而這個上色不只是簡單的色彩填充,而是涵蓋了非常多的技巧(幾何計算、顏色計算、貼圖等)

    所以中文的着色器,是一個非常精準的翻譯。

    群內的笑笑說了一個比較不錯的說法:Shader 主要是光線數據作用在不同數據的物體上產生不同效果。

    Shader 學習的順序

    不管是 Shader 還是其它某個科目,都有一些最常用、最簡單的知識點。

    而這些知識點很容易學以致用,也就是說,這種知識點,我們學習完了就能馬上落地。

    所以,教練要做的就是,把 Shader 中的知識點按照是否常用和是否簡單這兩個維度進行排列篩選,然後把它們一個個整理成案例,這樣童鞋們的學習體驗就會大幅上升。

    主題式研究第三個階段

    • 第一個階段:確定主題(關鍵字)
    • 第二個階段:搜索資料、搜索信息(搜集情報)
    • 第三個階段:構建知識體系(畫腦圖、寫大綱)

    到此,Shader 這個主題,我們目前已經到了第三個階段,也就是構建知識體系的階段。

    當然,這一整篇,都再講,我們要怎麼怎麼做,接下來幹嗎,並沒有學習 Shader 的任何一個知識點。

    那麼今天就學習一點 Shader 知識意思一下。

    顏色的控制

    現有一張貼圖,如下:

    用來控制顏色的 shader 代碼如下:

    float4 frag (v2f i) : SV_Target
    {
        // 圖片上每個像素的顏色值
        float4 color = tex2D(_MainTex, i.uv);
                    
        // 返回顏色,表示將改像素的顏色值輸出到屏幕上
        return color;
    }
    

    我們只看方法中的代碼,先不要在意一些細節。

    雖然,我們沒有 Shader 的語法學習經驗,但是憑我們的 C# 經驗,可以將上述代碼推測個大概來。

    首先 float4 是一個類型,可以存儲 4 個 float 數值。而顏色一般都是由 r(red 紅色)、g(green,綠色)、b(blue,藍色)、a(alpha,透明度) 四個值控制。所以 float4 可以存儲一個顏色。

    現在,我們把圖片中每個像素顏色重的紅色值設置為 0,圖片結果則如下所示:

    代碼如下所示:

    float4 frag (v2f i) : SV_Target
    {
        // 圖片上每個像素的顏色值
        float4 color = tex2D(_MainTex, i.uv);
                    
        color.r = 0;
    
        // 返回顏色,表示將改像素的顏色值輸出到屏幕上
        return color;
    }
    

    我們看到,圖片變成了藍綠色。

    小結

    Shader 是一門控制顏色的藝術,Shader 的核心也是如此。
    在此篇,我們學習了 Shader 的兩個重要知識點:

    1. float4 結構體
    2. 顏色的 rgb 控制

    這兩個知識點非常簡單,也非常基礎,但是是非常常用的兩個知識點。

    這片文章的內容就這些。

    知識地圖

    相關下載:

    轉載請註明地址:liangxiegame.com

    更多內容
    QFramework 地址:https://github.com/liangxiegame/QFramework
    QQ 交流群:623597263
    涼鞋的主頁:https://liangxiegame.com/zhuanlan
    關注公眾號:liangxiegame 獲取第一時間更新通知及更多的免費內容。

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

    【其他文章推薦】

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

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

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    南投搬家前需注意的眉眉角角,別等搬了再說!

    新北清潔公司,居家、辦公、裝潢細清專業服務

  • 北極熊身上遭噴黑字「T-34」 專家震驚:失去保護色恐難覓食

    摘錄自2019年12月4日上報報導

    隨著全球暖化加劇,近幾個月以來常見北極熊出現在俄羅斯北方。但是近期在社群媒流傳的影片可見,有隻北極熊身體側邊被噴上黑色字母和數字「T-34」,目前專家正在調查這部影片的確切拍攝地點是否位於俄羅斯,並且警告這樣的行為可能會影響北極熊的生活技能,使牠們失去原有的保護色,難以在白雪覆蓋的地區捕獵食物。

    《英國廣播公司》(BBC)報導,這部影片被世界自然基金會(World Wildlife Fund,WWF)成員卡夫里(Sergey Kavry)發布在Facebook,之後又被當地媒體廣泛分享。卡夫里說,這部影片被分享在通訊軟體WhatsApp的一個俄羅斯東部楚科奇自治區(Chukotka)當地居民的群組裡,而負責監控野生動物的科學家也不會以噴漆的方式將北極熊進行編號。

    由於「T-34」戰車是蘇聯在第二次世界大戰(World War Two)戰勝德國納粹的重要關鍵,卡夫里說:「我不知道這段影片究竟是在哪個區域、地區,或哪裡的附近拍攝。如果這是個傳遞軍事訊息的舉動……某種程度來說是對歷史不尊重。」

    俄羅斯北方生態問題研究所(Institute Of The Biological Problems Of The North)科學家科赫涅夫(Anatoly Kochnev)指出,北極熊不太可能在未被麻醉的情況下,任人在牠身上噴字。

    科赫涅夫提到,這隻北極熊被噴漆的當下可能無法活動,或是可能靜止不動,因為牠被噴上的字樣均勻,而且大小相同。此外,他認為這可能發生在俄羅斯北部地區新地島(Novaya Zemlya),因為先前有專家團隊將北極熊進行麻醉,阻止牠們在人類居住地遊蕩。

    科赫涅夫說,如今要將這隻北極熊的噴漆洗掉可能要花上好幾週的時間,而且這會對牠的白色保護色造成影響,使牠難以在冰雪覆蓋的地方順利捕食獵物。

    俄羅斯媒體推測,在北極熊身上噴漆的舉動,可能是基於附近居民對於許多北極熊屢次闖入人類居住地的恐懼與不滿。隨著全球暖化日漸加劇,新地島2月宣布進入緊急狀態,因為有超過50隻北極熊出現在該地區,牠們在人口密集居住的建築物周遭漫步、尋找食物。

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

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

    新北清潔公司,居家、辦公、裝潢細清專業服務

  • 聯合國:反疫苗運動 助長薩摩亞麻疹疫情惡化

    摘錄自2019年12月6日中央通訊社綜合報導

    聯合國兒童基金會(UNICEF)太平洋島嶼負責人今天(5日)表示,社群媒體巨頭必須嚴厲取締反疫苗接種貼文,這些貼文助長薩摩亞(Samoa)致命麻疹疫情惡化。

    UNICEF地區代表耶特(Sheldon Yett)表示,推特(Twitter)、臉書(Facebook)和Instagram(IG)等網路平台上「極不負責任」的反疫苗接種訊息,加劇了薩摩亞爆發的麻疹疫情,自10月中旬以來已造成62人死亡。耶特告訴法新社:「很明顯地它們必需負起企業責任並展開行動,確保那些人民,特別是弱勢族群能獲得正確資訊,讓孩童得以存活。」

    在麻疹疫情爆發前,薩摩亞的疫苗接種率降至只剩略超過30%,遠低於公認最佳接種率90%,這也使得該海島國家極易受到感染。世界衛生組織(WHO)把矛頭指向反疫苗宣傳運動。耶特表示,這項運動主要是由海外倡議人士在網路上展開。

    「很不幸的是,這項運動在薩摩亞找到願意相信的民眾,那裡有一部分人懷疑醫療保健服務品質,且可能不信任當地(疫苗)供應者。」他說,來自諸如美國和澳洲等富裕已開發國家的運動人士,在網路上張貼反疫苗訊息,他們必須意識到自己的所作所為會對開發中國家帶來衝擊。

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

    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

  • 澳洲野火持續燃燒 摧毀約四成台灣面積

    整理:劉妙慈(環境資訊中心實習編輯)

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

    【其他文章推薦】

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

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

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    南投搬家前需注意的眉眉角角,別等搬了再說!

    新北清潔公司,居家、辦公、裝潢細清專業服務