张春成
2022/09/10阅读:37主题:默认主题
鸡同鸭讲的 BUG
鸡同鸭讲的 BUG
Python-opencv 遇到了一个中文支持的 BUG,它不能读取目录中带有中文的图像文件。
这背后的原因是编码和解码过程不一致所导致的乱码现象。
现象描述
简单来说,就是当文件的路径包含中文的时候,cv2 的 imread 函数不能正常读取图像的 RGB (BGR) 矩阵,下面是测试程序。
# %%
import os
import cv2
from pathlib import Path
# %%
# Make file names, eng for name in english chars, chs for name in chinese chars
# And make sure they exist
engName = Path(os.environ['HOME'], 'Pictures', 'BingImageOfTheDay.jpg')
engName, engName.is_file()
chsName = Path(os.environ['HOME'], 'Pictures', '图片.jpg')
chsName, chsName.is_file()
# %%
# The imread works fine, it reads BGR color images
mat1 = cv2.imread(engName.as_posix())
mat1.shape
# %%
# The imread fails
mat2 = cv2.imread(chsName.as_posix())
type(mat2)
# %%
问题追踪
经过一系列查询,可以逐步将可能出问题的原码定位到 Opencv 的原码中,位置为:modules/imgcodecs/src/loadsave.cpp#L420L421 和 modules/imgcodecs/src/grfmt_jpeg.cpp#L240L242 和 modules/imgcodecs/src/grfmt_base.cpp#L65L67
opencv/loadsave.cpp at b0dc474160e389b9c9045da5db49d03ae17c6a6b · opencv/opencv
opencv/grfmt_jpeg.cpp at b0dc474160e389b9c9045da5db49d03ae17c6a6b · opencv/opencv
opencv/grfmt_base.cpp at b0dc474160e389b9c9045da5db49d03ae17c6a6b · opencv/opencv



可以看到,对于这个玩意,opencv 的 c++ 原码并没有对文件名做任何“操作”。但这并没有达到无为无不为的境界,而是把一个问题埋在了系统里,这个问题就是文字编码。
/// file: loadsave.cpp
/// set the filename in the driver
decoder->setSource( filename );
/// file: grfmt_base.cpp
/// set the m_filename
bool BaseImageDecoder::setSource( const String& filename )
{
m_filename = filename;
m_buf.release();
return true;
}
/// file: grfmt_jpeg
/// open the m_filename, take jpeg for example
m_f = fopen( m_filename.c_str(), "rb" );
if( m_f )
jpeg_stdio_src( &state->cinfo, m_f );
简单来说,就是 python 3 对字符串的处理是统一使用 Unicode 编码,这本来是好事,但这个好事只停留在 python 内部。这是因为它运行在系统提供的环境下,而 windows 系统对于中文区系统的文字编码方法有点微妙,因为在没有选择 Beta 版的 Unicode UTF-8 支持的情况下,系统会以 GBK 对中文字符进行编码。

这就导致一个问题,那就是 python 在存储字符串的时候使用的编码是 utf-8,而系统的其他成员在默认情况下,使用 gbk 对它进行解码,这一定会导致冲突,样例如下。
import sys
# Output: utf-8 or gbk, depends on OS setting
sys.getdefaultencoding()
# Encoding conflict, 中秋节: 涓绉嬭妭
s = '中秋节'
decode = s.encode('utf-8').decode('gbk', errors='ignore')
print('{}: {}'.format(s, decode))
问题延展
凡是涉及到通信的系统,都需要对编码和解码端进行约定和匹配。因为这个现象并不限于中文字符的编码。下面是一个针对数字的编解码测试样例,它涉及以下的编、解码排列组合
-
编码方案:4 个字表示一个数,这个数可以是整数(int)或浮点数(float); -
编码方案:比特位的排列方向为 ”大端(big)“ 和 ”小端(little)“; -
解码方案:解码方法有四种,分别是 '>f', '<f', '>i', '<i', 其中 ‘>’ 代表大端,’<’ 代表小端,‘i‘代表整数,’f’代表浮点数。
实验的对比结果如下
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
import struct
import numpy as np
import pandas as pd
# -------------------------------------
# Material setting
ints = [65536 + e for e in range(2)]
floats = [3.14 + e for e in range(2)]
orders = ['little', 'big']
# -------------------------------------
# Encode
data = []
columns = []
# Encode int
for order in ['little', 'big']:
for num in ints:
d = num.to_bytes(4, order)
data.append(d)
columns.append('{}-{}'.format(order, num))
# Encode float
for order in ['>f', '<f']:
for num in floats:
d = struct.pack(order, num)
data.append(d)
columns.append('{}-{:.2f}'.format(order, num))
d = b''.join(data)
# -------------------------------------
# Decode
dtypes = ['>f', '<f', '>i', '<i']
data=[]
for dt in dtypes:
decode = np.frombuffer(d, dtype=dt)
data.append([dt, decode.dtype] + decode.tolist())
print('{}: {}, {}'.format(dt, decode, decode.dtype))
df = pd.DataFrame(data, columns=['dt1', 'dt2']+columns)
df.to_csv('data.csv')
df
作者介绍