Xử lý khi đường dẫn có ký tự tiếng Nhật trong OpenCV (Python)

Xử lý khi đường dẫn có ký tự tiếng Nhật trong OpenCV (Python)
Photo by Gerd Altmann from Pixabay

Với những người làm việc với Nhật như mình thì thỉnh thoảng gặp những vấn đề liên quan đến encoding là chuyện bình thường. Mới đây mình có gặp vấn đề về encode ngay trong đường dẫn, tức là còn chưa kịp đọc file 😂 khi dùng thư viện OpenCV để đọc và ghi file ảnh.

Khái quát vấn đề

Chuyện Shift_JIS ở Nhật

Người Nhật vẫn dùng Shift_JIS chứ không chuyển sang UTF-8 và với Windows 10 phiên bản tiếng Nhật thì mặc định vẫn là dùng Shift_JIS. Lý do người Nhật vẫn duy trì việc sử dụng Shift_JIS chắc là do truyền thống. Shift_JIS là chuẩn encoding được sử dụng rất sớm, trước khi mà UTF-8 trở lên phổ biến như ngày hôm nay. Ngoài ra còn một lý do nữa mình đoán là Shift_JIS chỉ cần dùng 2 byte mã hoá trong khi UTF-8 cần tới 3-4 byte cho việc encoding tiếng Nhật nói riêng và CJK nói chung 😅

Và nếu làm việc với họ thì việc để Shift_JIS cho đồng bộ là việc nên làm. Lý do là để tránh những vấn đề xảy ra do lệch encode (nhất là khi gửi và nhận file zip, tên file và thư mục bên trong sẽ lỗi hết). Cũng may là lần này để Shift_JIS cho đồng bộ nên nhờ đó mà phát hiện ra vấn đề luôn 🤣

Vấn đề với OpenCV

Quay trở lại với vấn đề mình muốn nói hôm nay. Riêng với ngôn ngữ Python, hiện tại việc xử lý dữ liệu Unicode đã rất dễ dàng và hầu như mình không gặp nhiều vấn đề với nó. Ngay cả những thư viện chuẩn để làm việc với file và thư mục cũng làm việc ngon lành khi mà OS sử dụng Shift_JIS còn mã nguồn thì dùng UTF-8 như bình thường mà không gặp vấn đề gì cả. Tức là việc chuyển đổi encoding đã được hỗ trợ mặc định rồi 😍 và nhiều thư viện cài thêm cũng như vậy.

Thế nhưng một thư viện rất quan trọng khi làm việc với hình ảnh là OpenCV thì lại chưa hỗ trợ việc này 🤐 (mình đoán là do thư viện này dùng nhiều code C++ nên việc hỗ trợ này tương đối khó). Khi sử dụng OpenCV với Python thì mình gặp 2 vấn đề như sau (có lẽ là do cùng nguyên nhân):

  • cv2.imread không thể đọc được file nếu đường dẫn có chứa ký tự tiếng Nhật
  • cv2.imwrite ghi file sai nếu đường dẫn có chứa ký tự tiếng Nhật

Lúc đầu mình gặp vấn đề với cv2.imwrite, khi mà muốn ghi file với đường dẫn như sau:

output\テスト\foo.png

Thì kết quả là nhận được file ở đường dẫn thế này

output\繝・せ繝・foo.png

Nhìn vào kết quả này thì mình khá chắc vấn đề liên quan đến encoding. Thế nhưng điểm kỳ lạ là ở chỗ, chỗ ký tự không đọc được kia, một phần thì giống chữ テスト encode bằng UTF-8 rồi decode sai bằng Shift_JIS, một phần thì lại không giống. Với lại, nếu encode sai thì đúng ra từ chỗ \foo.png trở đi vẫn giữ nguyên, thì kết quả và mình phải nhận được foo.png trong một thư mục lạ hoắc chứ không phải một file viết liền thế kia.

Sau đó thì mình kiểm tra với cv2.imread thì cũng gặp vấn đề tương tự. Do đường dẫn bị lệch encode nên không thể đọc được file mình mong muốn.

Giải quyết

Đổi encode

Mình thử đổi OS sang sử dụng encode UTF-8 thì kết quả là vấn đề cũng tự nhiên biến mất 🤗. Như vậy là OpenCV có hỗ trợ UTF-8 và chỉ hỗ trợ UTF-8 mà thôi. Với những OS sử dụng encode khác thì sẽ bị lỗi decode sai như ở trên. Nếu thế thì khả năng lỗi này chỉ gặp khi dùng Windows, vì mình biết MacOS hay GNU/Linux đều sử dụng UTF-8 hết rồi.

Thế nhưng đến lúc này vấn đề chưa được giải quyết, vì đổi encode ở máy mình thì dễ rồi, nhưng code mình viết cần phải chạy trên máy khách hàng, mà khi đó không thể bắt họ đổi encode giống như mình được 😪. Đúng là mệt mỏi, tại sao dự án này khách lại dùng Windows cơ chứ 😅

Sau khi tìm hiểu một hồi thì mình thấy một issue của OpenCV liên quan đến chuyện này, và có vẻ còn nhiều issue khác liên quan. Tất cả đều đã closed nhưng không được giải quyết. Họ có để lại comment như sau:

There are 2 alternatives to use wchar_t strings with OpenCV:

  1. Convert wchar_t strings to UTF-8 and pass UTF-8 string as cv::imread and cv::imwrite parameter. UTF-8 string is handled by system fopen call and it’s behavior depends on OS support and locale. See mbstowcs in C++ standard for more details.
  2. OpenCV provides cv::imdecode and cv::imencode functions that allow to decode and encode image using memory buffer as input and output. The solution decouples file IO and image decoding and allows to manage path strings, locales, etc in user code. See code snippet for cv::imencode bellow. fopen can be replaced with _wfopen for wide strings support. See Microsoft reference manual for details: https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/fopen-wfopen?view=vs-2019

Với phương án đầu tiên, đổi encode của OS thì OK rồi nhưng còn vấn đề với khách hàng. Mình lại thử code mã nguồn của Python thành Shift_JIS mà vẫn không thành công. Thử truyền tên file vào hàm bằng cách encode Shift_JIS như sau nhưng cũng không thành công.

"output\テスト\foo.png".decode("Shift_JIS").encode("unicode-escape")

Vậy là chỉ còn phương án 2. Mình thì không thành thạo mấy hàm imencode với imdecode lắm nên hơi mất thời gian tìm hiểu một chút. Sau khi mày mò đủ nguồn trên mạng thì cuối cùng, mình cũng có thể viết lại hai hàm dựng sẵn của OpenCV là imreadimwrite (thực ra không phải tự viết mà đi “tham khảo” nhiều nơi 🤣)

Viết lại cv2.imread

Thay vì dùng trực tiếp cv2.imread có sẵn, thì mình chuyển sang dùng np.fromfile (may sao thư viện numpy không bị lỗi tương tự như OpenCV 🤩) kết hợp với cv2.imdecode như sau. Mình lấy luôn tên hàm giống với cv2.imread để có gì “Find & Replace” cho nhanh 😊

import numpy as np
import cv2


def imread(filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8):
    try:
        n = np.fromfile(filename, dtype)
        img = cv2.imdecode(n, flags)
        return img
    except Exception as e:
        print(e)
        return None

Viết lại cv2.imwrite

Tương tự như trên, thay vì dùng cv2.imwrite thì mình chuyển sang sử dụng cv2.imencodenp.ndarray.tofile như sau:

import numpy as np
import cv2
import os


def imwrite(filename, img, params=None):
    try:
        ext = os.path.splitext(filename)[1]
        result, n = cv2.imencode(ext, img, params)

        if result:
            with open(filename, mode='w+b') as f:
                n.tofile(f)
            return True
        else:
            return False
    except Exception as e:
        print(e)
        return False

Sau khi viết được 2 hàm như trên, và mình chỉ cần thay hết các hàm của OpenCV thành các hàm này (tìm rồi xoá prefix cv2. thôi) là chương trình lại chạy như ngựa 😊

Kết luận

Encoding vẫn là vấn đề muốn thuở với các bạn làm việc với khách hàng Nhật như mình. Mình nghĩ trong tương lai một vài năm tới người Nhật vẫn chưa chuyển sang dùng UTF-8 được đâu, Shift_JIS vẫn là một phần tất yếu của cuộc sống. Hy vọng bài viết giúp ích cho các bạn trong công việc và giúp giải quyết phần nào khó khăn mà các bạn có thể cũng gặp phải như mình 😀

Tôi xin lỗi nếu bài viết có bất kỳ typo nào. Nếu bạn nhận thấy điều gì bất thường, xin hãy cho tôi biết.

Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.

Welcome

manhhomienbienthuy

Đây là thế giới của manhhomienbienthuy (naa). Chào mừng đến với thế giới của tôi!

Bài viết liên quan

Bài viết mới

Chuyên mục

Lưu trữ theo năm

Thông tin liên hệ

Cảm ơn bạn đã quan tâm blog của tôi. Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.