❝最近、「2021広東省産業インテリジェント製造イノベーションコンペティションインテリジェントアルゴリズムコンペティション:タイル表面欠陥の品質検査」で、角度の偏差が異なるいくつかの写真に遭遇しました。衛星画像と同様に、解像度は非常に高いですが、ターゲットは非常に小さいため、自動角度調整、ウィンドウ処理、および元の画像の対応する座標マッピングが必要になります。
❞
写真を読む
大きな画像の場合、直接使用すると変換よりも約 30% 遅くcv2.imread
なりますので、ここでの読み取りを使用することをお勧めします。PIL
numpy array
Image.open
import numpy as np
import cv2
from PIL import Image
# org_img = cv2.imread(BASE_DIR + img_file)
org_img = Image.open(BASE_DIR + img_file)
org_img = cv2.cvtColor(np.asarray(org_img), cv2.COLOR_RGB2BGR)
外枠検出
1. グレースケール画像に変換する
# 灰度图
greyPic = cv2.cvtColor(org_img, cv2.COLOR_BGR2GRAY)
2.画像を二値化する
ここでのしきい値は平均画像値を使用します。これはほとんどのシナリオに対応でき、特別な場合には自分で調整できます。
# threshold(src, thresh, maxval, type, dst=None)
# src是输入数组,thresh是阈值的具体值,maxval是type取THRESH_BINARY或者THRESH_BINARY_INV时的最大值
# type有5种类型,这里取0:THRESH_BINARY ,当前点值大于阈值时,取maxval,也就是前一个参数,否则设为0
# 该函数第一个返回值是阈值的值,第二个是阈值化后的图像
ret, binPic = cv2.threshold(greyPic, greyPic.mean(), 255, cv2.THRESH_BINARY)
3. メディアンフィルタリング
median = cv2.medianBlur(binPic, 5)
4. 輪郭を見つける
# findContours()有三个参数:输入图像,层次类型和轮廓逼近方法
# 该函数会修改原图像,建议使用img.copy()作为输入
# 由函数返回的层次树很重要,cv2.RETR_TREE会得到图像中轮廓的整体层次结构,以此来建立轮廓之间的‘关系'。
# 如果只想得到最外面的轮廓,可以使用cv2.RETE_EXTERNAL。这样可以消除轮廓中其他的轮廓,也就是最大的集合
# 该函数有三个返回值:修改后的图像,图像的轮廓,它们的层次
contours, hierarchy = cv2.findContours(median, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
5. 最小外接長方形を取得します。
maxArea = 0
# 挨个检查看那个轮廓面积最大
for i in range(len(contours)):
if cv2.contourArea(contours[i]) > cv2.contourArea(contours[maxArea]):
maxArea = i
hull = cv2.convexHull(contours[maxArea])
hull = np.squeeze(hull)
# 得到最小外接矩形的(中心(x,y), (宽,高), 旋转角度)
rect = cv2.minAreaRect(hull)
# 通过box会出矩形框
box = np.int0(cv2.boxPoints(rect))
映像の角度を調整する
角度偏差を求め、アフィン行列を計算し、box
外接長方形の座標を変換します。
center = rect[0]
angle = rect[2]
if angle > 45:
angle = angle - 90
# 旋转矩阵
M = cv2.getRotationMatrix2D(center, angle, 1)
h, w, c = org_img.shape
# 旋转图片
dst = cv2.warpAffine(org_img, M, (w, h))
# 坐标变换
poly_r = np.asarray([(M[0][0] * x + M[0][1] * y + M[0][2],
M[1][0] * x + M[1][1] * y + M[1][2]) for (x, y) in box])
写真をトリミングする
x_s, y_s = np.int0(poly_r.min(axis=0))
x_e, y_e = np.int0(poly_r.max(axis=0))
# 设置预留边框
border = 100
x_s = int(max((x_s - border), 0))
y_s = int(max((y_s - border), 0))
x_e = int(min((x_e + border), w))
y_e = int(min((y_e + border), h))
# 剪裁
cut_img = dst[y_s:y_e, x_s:x_e, :]
ウィンドウのセグメンテーション
画像を補正した後、必要に応じてウィンドウに分割できます。ウィンドウ サイズ、オーバーラップ率、出力ディレクトリを指定すると、小さな画像を大量に取得できます。
def slice(img, img_file, window_l=1024, overlap=0.2, out_dir=""):
# 切割图片 生成文件 xxx_000_000.jpg
h, w, c = img.shape
step_l = int(window_l - window_l * overlap) # 步长
x_num = int(np.ceil(max((w - window_l) / step_l, 0))) + 1
y_num = int(np.ceil(max((h - window_l) / step_l, 0))) + 1
for i in range(x_num):
for j in range(y_num):
x_s, x_e = i * step_l, i * step_l + window_l
y_s, y_e = j * step_l, j * step_l + window_l
# 修正越界
if x_e > w:
x_s, x_e = w - window_l, w
if y_e > h:
y_s, y_e = h - window_l, h
assert w >= window_l
assert h >= window_l
new_img_file = img_file[:-4] + '_%03d_%03d.jpg' % (i, j)
im = img[y_s:y_e, x_s:x_e, :]
cv2.imwrite(out_dir + new_img_file, im)
return
バッチ処理
関数をカプセル化し、ディレクトリ全体をスキャンし、後で座標を復元できるように、対応する構成ファイルを元のイメージとともに保存します。
def adjust_angle(org_img, img_file, border=100):
h, w, c = org_img.shape
# 统一尺度,如果尺寸小于 4000,放大一倍
scale = 1
if w < 4000 or h < 4000:
scale = 2
w = int(w * scale)
h = int(h * scale)
org_img = cv2.resize(org_img, (w, h), interpolation=cv2.INTER_LINEAR)
x_s, y_s, x_e, y_e, rect, new_img = getCornerPoint(org_img)
# 去除边框
x_s = int(max((x_s - border), 0))
y_s = int(max((y_s - border), 0))
x_e = int(min((x_e + border), w))
y_e = int(min((y_e + border), h))
img = new_img[y_s:y_e, x_s:x_e, :]
data = dict()
data['name'] = img_file
data['xyxy'] = [x_s, y_s, x_e, y_e]
data['rect'] = rect
data['border'] = border
data['scale'] = scale
return data, img
BASE_DIR
元画像ディレクトリ、OUT_ADJUST
角度調整後のディレクトリ、adjust.json
設定ファイルに設定します。
result_json = []
img_list = os.listdir(BASE_DIR)
for img_file in tqdm(img_list):
org_img = Image.open(BASE_DIR + img_file)
org_img = cv2.cvtColor(np.asarray(org_img), cv2.COLOR_RGB2BGR)
data, img = adjust_angle(org_img, img_file, border=100)
result_json.append(data)
cv2.imwrite(OUT_ADJUST + img_file, img)
slice(img, img_file, TARGET, overlap=OVERLAP, out_dir=OUT_SLICE)
with open(OUT_DIR + 'adjust.json', 'w') as fp:
json.dump(result_json, fp, indent=4, ensure_ascii=False)
座標復元
1.スライス画像一覧を読み込む
with open("instances_test2017_1024.json", 'r') as f:
test_imgs = json.load(f)['images']
test_imgs_dict = {}
for i, obj in enumerate(test_imgs):
img_name = obj['file_name']
test_imgs_dict[img_name] = i
2. 元のファイル情報を読み取る
with open(OUT_DIR + 'adjust.json', 'r') as fp:
img_info = json.load(fp)
img_info_dict = {}
for i, obj in enumerate(img_info):
img_name = obj['name']
img_info_dict[img_name] = i
3. 推論結果ファイルの読み込み
多数のサブグラフの推論結果をまとめると、mmdetection
マルチスレッドDataLoader
と大容量のビデオ メモリを最大限に活用してbatch size
推論プロセスを高速化できます。
with open("result_1024-20.pkl", 'rb') as f:
pred_set = pickle.load(f)
4. 角度調整マップに座標をマージします
画像の長さと幅が取得され、同じウィンドウ処理パラメータに基づいて、各サブ画像の参照座標の合計をx_s
復元できますy_s
。
test_imgs_dict
部分グラフのファイル名の辞書とpred_set
予測結果のリストが保存されます。形式のファイル名を通じてXXX_000_000.jpg
、2 レベルのマッピング後に対応する推論結果セットを取得できます。
def merge_result(info, pred_set, test_imgs_dict, img_file, window_l=1024, overlap=0.2):
assert info['name'] == img_file
# 这里只需要取图片长宽信息,避免读图操作太慢,直接读取配置文件
x1, y1, x2, y2 = info['xyxy']
w = x2 - x1
h = y2 - y1
step_l = int(window_l - window_l * overlap) # 步长
x_num = int(np.ceil(max((w - window_l) / step_l, 0))) + 1
y_num = int(np.ceil(max((h - window_l) / step_l, 0))) + 1
result = [np.array([[], ] * 5).T.astype(np.float32), ] * 6 # 分类数为6, bbox.shape 为(0, 5)
for i in range(x_num):
for j in range(y_num):
x_s, x_e = i * step_l, i * step_l + window_l
y_s, y_e = j * step_l, j * step_l + window_l
# 修正越界
if x_e > w:
x_s, x_e = w - window_l, w
if y_e > h:
y_s, y_e = h - window_l, h
assert w >= window_l
assert h >= window_l
new_img_file = img_file[:-4] + '_%03d_%03d.jpg' % (i, j)
pred = pred_set[test_imgs_dict[new_img_file]] # 获取预测结果
for label_id, bboxes in enumerate(pred):
# 坐标修正 x_s, y_s 划窗基坐标
bboxes[:, 0] = bboxes[:, 0] + x_s
bboxes[:, 1] = bboxes[:, 1] + y_s
bboxes[:, 2] = bboxes[:, 2] + x_s
bboxes[:, 3] = bboxes[:, 3] + y_s
# 合并到大图
result[label_id] = np.vstack((result[label_id], bboxes))
return result
5. 元の画像への座標マッピング
まず、元の画像情報を取得しinfo
、外部長方形パラメータ、回転角度、拡大縮小率、境界サイズなどを取得し、逆アフィン行列を構築し、M
すべての検出フレームに対して座標変換を実行します。
def generate_json(pred, info, img_file, score_threshold=0.05, out_dir="", vis=False):
base_x, base_y, x2, y2 = info['xyxy']
rect = info['rect']
scale = info['scale']
border = info['border']
x1, y1, x2, y2 = (border, border, x2 - border, y2 - border)
poly = np.asarray([(x1, y1), (x2, y1), (x2, y2), (x1, y2)])
center = tuple(rect[0])
angle = rect[2]
if angle > 45:
angle = angle - 90
# 逆旋转还原
M = cv2.getRotationMatrix2D(center, -angle, 1)
# 遍历完所有分片, nms
json_results = []
for label_id, bboxes in enumerate(pred): # 6个分类
bboxes = nms(np.array(bboxes[:, :4]), np.array(bboxes[:, 4]), iou_threshold=0.5)[0]
# 坐标转换到原始图片
bboxes[:, 0] = bboxes[:, 0] + base_x
bboxes[:, 1] = bboxes[:, 1] + base_y
bboxes[:, 2] = bboxes[:, 2] + base_x
bboxes[:, 3] = bboxes[:, 3] + base_y
for ann in bboxes:
x1, y1, x2, y2, score = ann
if score < score_threshold:
continue
poly_r = np.asarray([(M[0][0] * x + M[0][1] * y + M[0][2],
M[1][0] * x + M[1][1] * y + M[1][2]) for (x, y) in
[(x1, y1), (x1, y2), (x2, y1), (x2, y2)]])
# 还原小图片缩放
ann = poly2ann(poly_r, score, scale=scale)
data = dict()
data['name'] = img_file
data['category'] = label_id + 1
data['bbox'] = [float(ann[0]), float(ann[1]), float(ann[2]), float(ann[3])]
data['score'] = float(score)
json_results.append(data)
return json_results
最後に、nms
一連の後処理を経て、元の画像にマッピングできます。
完璧な仕上がり!