[Python] 物件導向程式設計- 繼承和多型

從事影像分析系統研究與開發,隨著接觸的案子越來越多,自然接觸的演算法也越來越多元、豐富,但即便如此,還是會遇到相似的案例,這時候就會出現『代碼重用』的需求。如果有做好文件工程的話,很容易就找到自己過往執行案件的參考資料;如果再厲害一點,還可以透過 Git/ GitLab 等工具方便檢索;再進一步的就是有著自己撰寫的影像分析函式庫,你不需要去找哪段程式碼,只需要向引用第三方函式庫的方法一樣,一條指令就能實做許多複雜的演算法。

以上的需求情境正是這篇文章撰寫的最大動機。

以影像分析型的演算法開發為例,幾乎九成以上的演算法不外乎:讀入影像、旋轉影像(Optional)、影像灰階化、核心演算法,接著就是分析結果可式化。其中 讀入影像、旋轉影像(Optional)、影像灰階化 這三個幾乎都是標準公式,有沒有方法不用每個演算法都做重複的動作?甚至是可以彈性擴充多個演算法?

這時就需要來瞭解物件導向程式設計中的繼承(Inheritance)多型(Polymorphism)是兩個核心概念。

到這邊文字已經夠多,我們來用程式碼說明:

class FrameProcessor:
    """基礎類別,用於處理影像框架。
    
    Attributes:
        ALLOWED_TYPES (FrameType): 列出所有允許的框架類型。
    """
     
    ALLOWED_TYPES = FrameType

    @staticmethod
    def CreateProcessor(_type):
        """根據給定的類型創建 FrameProcessor 實例。

        Args:
            _type (str): 處理器的類型。

        Returns:
            FrameProcessor: 創建的 FrameProcessor 實例。

        Raises:
            ValueError: 如果提供了不支持的處理器類型。
        """
        if _type == "NORMAL":
            return NormalProcessor()
        elif _type == "GRAY":
            return GrayProcessor()
        else:
            raise ValueError(f"Unsupported processor type: {_type}")

    def Process(self, frame, flip=None, returnByteImage=True):
        """處理影像並將結果作為位元組返回。

        Args:
            frame: 原始影像。
            flip (int, optional): 是否翻轉影像,翻轉類型由此參數決定。
            returnByteImage (bool, optional): 是否將處理過的影像作為位元組返回。

        Returns:
            bytes or np.array: 處理過的框架,格式取決於 returnByteImage 參數。
        """
        if flip is not None:
            frame = cv2.flip(frame, flip)
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        return self._process_frame(frame, gray, returnByteImage)

    def _process_frame(self, frame, gray, returnByteImage=True):
        """由子類別實現的處理邏輯。

        Args:
            frame (np.array): 原始的影像。
            gray (np.array): 灰階影像。

        Returns:
            bytes or np.array or None: 處理過的框架,具體返回類型取決於子類別的實現。
        """
        pass

我想直接使用一個實際的影像分析案例來做說明會更好理解。我先創建一個類別- FrameProcessor 裡頭可以有多個處理單元,我以為處理影像的處理單元- NormalProcessor 與 GrayProcessor 為例。

首先映射的公開方法是為了讓用戶方便選擇要使用的處理單元,我們先暫時跳過。接著是兩個核心的函式:Process() 與 _process_frame()這兩個函式命名很接近,但細看你會發現兩者是有關連的。前面提到的影像分析流程記錄在Process 函式中,透過參數讓用戶選擇是否要將輸入影像進行翻轉,甚至輸出的結果是否轉換成位元組(提供網頁前端使用),這樣還沒有完,有沒有留意到:

return self._process_frame(frame, gray, returnByteImage)

這裡非常關鍵,目的是要讓子類別來去處理進一步的影像分析算法實做。那具體要怎麼做呢?我們來上範例代碼:

class NormalProcessor(FrameProcessor):
    """用於處理普通影像框的處理器類別。
    
    Methods:
        _process_frame: 實作父類別 FrameProcessor 的方法以處理影像。
    """
    def _process_frame(self, frame, gray, returnByteImage=True):
        """實作影像處理。

        Args:
            frame (np.array): 原始影像框。
            gray (np.array): 灰度影像框。

        Returns:
            bytes or np.array: 處理後的影像框,格式取決於 returnByteImage 參數。
        """
        if returnByteImage:
            _, jpeg = cv2.imencode('.jpg', frame)
            return jpeg.tobytes()
        else:
            return frame

子類別就這麼簡單,透過塞入 FrameProcessor 宣告這個是子類別且繼承FrameProcessor 。這時又跑出第一個疑惑,既然是繼承,為什麼跟父類別一樣宣告_process_frame()?這是因為我們要複寫父類別的方法,展開細部的演算法細節。

如果用上面這個範例,可能會覺得何苦『多此一舉』,因此我們來試著加入更複雜一點的影像分析前處理。

class Landmark5Processor(FrameProcessor):
    """用於在影像中標示5點臉部特徵點的處理器類別。

    Attributes:
        predictor (object): 用於預測5點臉部特徵點的預測器。
        detector (object): 用於臉部偵測的偵測器。

    Methods:
        _process_frame: 實作父類別 FrameProcessor 的方法以處理影像。
    """
    def __init__(self):
        # Initialize dlib's face detector model
        self.predictor = dlib.shape_predictor('./shape_predictor_5_face_landmarks.dat')
        self.detector = dlib.get_frontal_face_detector()

    def _process_frame(self, frame, gray, returnByteImage=True):
        """實作影像處理。

        Args:
            frame (np.array): 原始影像
            gray (np.array): 灰度影像

        Returns:
            bytes: 處理後,含有5點臉部特徵點的影像框(位元組)。
        """
        rects = self.detector(gray, 0)
        for rect in rects:
            shape = self.predictor(gray, rect)
            for i in range(0, 5):
                cv2.circle(frame, (shape.part(i).x, shape.part(i).y), 3, (0, 0, 255), -1)
        if returnByteImage:
            _, jpeg = cv2.imencode('.jpg', frame)
            return jpeg.tobytes()
        else:
            return frame

遵照這個格式你可以擴充很多複雜的算法分析進來,就像這個 Dlib 提供的五點臉部特徵點分析。

那既然都包成這樣,後續應該怎麼使用?

from frame_processor import FrameProcessor
# 使用 create_processor 方法創建實例
normalProcessor = FrameProcessor.CreateProcessor(FrameProcessor.ALLOWED_TYPES.NORMAL)
grayProcessor = FrameProcessor.CreateProcessor(FrameProcessor.ALLOWED_TYPES.GRAY)
fivePointsProcessor = FrameProcessor.CreateProcessor(FrameProcessor.ALLOWED_TYPES.FIVE_POINT_LANDMARK)

... 略



normalProcessor.Process(frame)
grayProcessor.Process(frame)
fivePointsProcessor .Process(frame)

就是這麼簡單。
代碼的維護也瞬間就變得很單純。如果說想要擴充更多的前置影像處理,就像詢問是否需要翻轉影像或是決定輸出的影像格式與種類,只要在 FrameProcess 這個基類(Base Class)中的 Process()定義跟宣告即可,記得是情況要反映到 _process_frame()。

發佈留言