Cập nhật: sau khi bài viết chia sẻ trên Forum Machine Learning, bạn Vũ Hữu Tiệp gợi ý cách sử dụng Numpy đúng đắn nhất là nhân ma trận với Numpy để loại bỏ hoàn toàn 2 vòng lặp lồng nhau, tốc độ khi chạy nhân ma trận với Numpy cho tốc độ cao nhất

Python là ngôn ngữ lập trình phổ biến nhất hiện nay. Dễ học, ứng dụng đa dạng, thư viện nhiều, áp dụng cho xử lý, phân tích dữ liệu tuyệt vời, làm AI, Machine Learning, Deep Learning mà không dùng Python nhưng đi đáng bóng mà không mang bóng.

Tuy nhiên nhược điểm của Python là chạy chậm, rất chậm. Những đoạn lệnh nào chạy nhanh, thực ra là Python gọi vào thư viện bao lấy code C hoặc C++ thuần, ví dụ như numpy, pandas, opencv, tensorflow, yolo...

Để thí nghiệm, làm dự án prototype chúng ta có thể dùng Python thuần, nhưng khi lên production cần tối ưu tốc độ, đôi khi là bảo mật code chúng ta không thể máy móc dùng Python mà cần sử dụng thêm kỹ thuật khác nữa. Bài viết này giới thiệu Cython và tận dụng tối đa các thư viện gốc C/C++ để tăng tốc độ.

Ví dụ minh hoạ sử dụng OpenCV để mở một ảnh con báo gấm sau đó dùng 2 vòng lặp for lồng nhau để chỉnh độ xám từng điểm ảnh. Ví dụ ảnh có kích thước 769 x 960 thì chúng ta phải thực hiện 738,240 động tác chỉnh độ xám cho từng điểm ảnh.
Mã nguồn chi tiết ở đây https://github.com/TechMaster/CythonOpenCV
 

Tóm tắt File Thời gian Hơn
Python thuần + np.arange contrast.py 0.659  
Python thuần + range contrast2.py 0.366 1.8 lần
Cython, pyx dùng Python thuần contrast3.py
contrast.pyx
0.2728 2.4 lần
Cython, pyx hàm kiểu C contrast4.py
contrast_c.pyx
0.0794 8.3 lần
Cython, pyx hàm kiểu C, dùng ceil của C contrast5.py
contrast_ceil.pyx
0.0003 2196 lần
Không cần dùng Cython, dùng numpy contrast6.py 0.000176 3744 lần

 

Bước 1: Hãy clode demo code về

Bạn cần phải có Python 3.7.x cài sẵn trên máy tính

$ git clone --depth=1 https://github.com/TechMaster/CythonOpenCV.git
$ cd CythonOpenCV
$ source venv/bin/activate
$ pip install -r requirements.txt
$ python contrast.py

Nếu bạn thấy hiện ra hình ảnh con báo gấm là ok ! Code nguyên gốc chưa tối ưu đây. Code sử dụng thư viện numpy, cv2, math, time.
Trước và sau 2 vòng lặp sẽ có đoạn code để đo thời gian

import numpy as np
import cv2
import math
import time

img = cv2.imread('tiger_low_constrast.jpg', cv2.IMREAD_GRAYSCALE)

height = img.shape[0]
width = img.shape[1]

contrast = 3
start_time = time.time()
for i in np.arange(height):
    for j in np.arange(width):
        a = img.item(i, j)
        b = math.ceil(a * contrast)
        if b > 255:
            b = 255
        img.itemset((i, j), b)

elapsed_time = time.time() - start_time
print(elapsed_time)
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Kết quả chạy là 0.659 giây.

Bước 2: Thay np.arange và range, gói đoạn lệnh thay đổi contrast vào hàm

Ở đoạn code trên, chúng ta có thể thay thế np.arange bằng cú pháp thuần Python là range. Nhờ động tác này chúng ta giảm phụ thuộc vào numpy.
Tiếp đó gói đoạn logic thay đổi contrast vào hàm def adjust_contrast(img, contrast): việc này chỉ để code dễ đọc thôi chứ không tăng tốc.
Chạy lại kết quả là 0.366 giây, nhanh hơn 1.8 lần. Chúng ta vẫn được dạy là Numpy là thư viện xử lý rất là nhanh. Tuy nhiên nó chỉ nhanh đối với những phép toán ma trận nhiều chiều thôi, chứ lệnh đơn giản như np.arange thì nó sẽ không bằng hàm range chuẩn của Python đâu.

import cv2
import math
import time

img = cv2.imread('tiger_low_constrast.jpg', cv2.IMREAD_GRAYSCALE)

def adjust_contrast(img, contrast):
    height = img.shape[0]
    width = img.shape[1]

    for i in range(height):
        for j in range(width):
            a = img.item(i, j)
            b = math.ceil(a * contrast)
            if b > 255:
                b = 255
            img.itemset((i, j), b)


start_time = time.time()
adjust_contrast(img, 3)
elapsed_time = time.time() - start_time

print(elapsed_time)

cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Bước 3: Sử dụng Cython

Để sử dụng Cython, chúng ta cần tách hàm adjust_contrast ra một file contrast.pyx, file này hoàn toàn không nhúng code C, hay sử dụng biến kiểu strongly type

# cython: language_level=3, boundscheck=False
# Chưa tối ưu triệt để, chỉ biên dịch ra C
import cv2
import math

def adjust_contrast(img, contrast):
    height = img.shape[0]
    width = img.shape[1]

    for i in range(height):
        for j in range(width):
            a = img.item(i, j)
            b = math.ceil(a * contrast)
            if b > 255:
                b = 255
            img.itemset((i, j), b)

Ngoài ra thêm một file setup.py nội dung như sau.

from distutils.core import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("contrast.pyx")
)

Để biên dịch file contrast.pyx chỉ cần gõ lệnh. Kết quả sẽ biên dịch ra file contrast.cpython-37m-darwin.so

$ python setup.py build_ext --inplace

Chạy file contrast3.py có import contrast biên dịch bằng Cython ở bước trên

import cv2
import time
import contrast

img = cv2.imread('tiger_low_constrast.jpg', cv2.IMREAD_GRAYSCALE)

start_time = time.time()

contrast.adjust_contrast(img, 3)

elapsed_time = time.time() - start_time
print(elapsed_time)

cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Kết quả chạy sau khi dùng Cython là 0.2728 second. Nhanh hơn cách ở cách bước 1 là 2.4 lần, cách bước 2 là 1.3 lần

Bước 4: Thay hàm Python bằng hàm kiểu C

Chúng ta cải tiến contrast.pyx thành contrast_c.pyx và setup_c.pyx

  • Thay def bằng cpdef
  • Định nghĩa các biến với kiểu tường minh cdef int x, y, width, height
# cython: language_level=3, boundscheck=False

import cv2
import math

cpdef unsigned char[:, :] adjust_contrast(unsigned char[:, :] img, float contrast):
    cdef int x, y, width, height
    height = img.shape[0]
    width = img.shape[1]
    for y in range(height):
        for x in range(width):
            b = math.ceil(img[y, x] * contrast)
            if b > 255:
                b = 255
            img[y, x] = b

    return img

Thời gian chạy rút xuống còn 0.0794 giây, nhanh hơn cách 1 là 8.3 lần. Rõ ràng tốc độ cải thiện khá nhiều

Bước 5: Dùng hàm ceil C thay vì hàm ceil của Python

Nếu ngó vào file contrast_c.pyx bước 3, chúng ta thấy nó dùng một hàm math.ceil của Python. Như vậy code biên dịch ra C gọi vào module math của Python. Đây là cách rất thiếu tối ưu, hay tối ưu không dứt điểm. Chúng ta cần bỏ càng nhiều phụ thuộc vào các module Python trong file pyx, ngược lại phải tận dụng tối đa các thư viện C, C++ thuần.
Đây là contrast_ceil.pyx

Thay vì dùng hàm math.ceil của python chúng ta dùng hàm ceil của C.

# cython: language_level=3, boundscheck=False

import cv2
cdef extern from "math.h":
    double ceil(double x)

cpdef unsigned char[:, :] adjust_contrast(unsigned char[:, :] img, float contrast):
    cdef int x, y, width, height, b
    height = img.shape[0]
    width = img.shape[1]

    for y in range(height):
        for x in range(width):
            b = int(ceil(img[y, x] * contrast))
            if b > 255:
                b = 255
            img[y, x] = b
    return img

Code sau khi cải tiến, chạy chỉ còn 0.0003 giây. Tốc độ tăng lên 2196 lần so với cách đầu tiên.

Tại sao tốc độ lại tăng dữ dội đến vậy 2 vòng lặp lồng nhau sẽ khiến một lệnh chạy chậm chạy gấp lên M x N lần. Nếu ảnh cỡ 4 Mega Pixels thì hàm kém tối ưu sẽ là thảm hoạ.

Bước 6: Sử dụng Numpy nhân ma trận

Đây là cách đúng đắn khôn ngoan nhất bởi Numpy có sẵn hàm nhân ma trận cực kỳ tối ưu. Code lại hết sức đơn giản

import cv2
import math
import time

img = cv2.imread('tiger_low_constrast.jpg', cv2.IMREAD_GRAYSCALE)
contrast = 2

start_time = time.time()
img = img * contrast
img[img > 255] = 255
elapsed_time = time.time() - start_time

print(elapsed_time)

cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Tổng kết

  • Hạn chế tối đa việc dùng hai vòng lặp lồng nhau khi xử lý dữ liệu. Cố gắng thay bằng hàm có sẵn trong Numpy, luôn là tối ưu nhất
  • Nếu buộc phải dùng vòng lặp, mà không dùng được Numpy hãy sử dụng Cython để tối ưu tốc độ. Tốc độ sẽ cải thiện ít nhất là 1.5 lần.

Cảm ơn bạn Vũ Hữu Tiệp đã chỉ ra cách xử lý nhân ma trận bằng Numpy !