2019-01-10 21:58:02 caihh2017 阅读数 1097

主要内容

本文是作者在复现YOLOv1算法时,所遇到的数据导入的问题的处理,数据导入及处理部分也是任何进行深度学习任务的必备操作,之后可能会写YOLOv1整个复现过程的详细解释,建议对照源代码进行参考,源代码可以留言想我索要,等完善好后,也会统一放置在GitHub上~

文本主要参考的是此项目GitHub,大家可以自行研究。

1、程序总体框架

  1. 读入图像及标签
  2. 图像随机预处理
  3. 标签编码

2、各部分具体实现

2.1读入图像及标签

这里我所使用的数据集为 COCO_train2014 数据集。
考虑到使用YOLO算法,其标签应包含box属性以及对应类别。

(1)标签文件说明

对于数据集标签文件的详细说明,可以参考这篇文章
以下只为本文所需要的内容进行说明
标签文件为json文件,可以直接在python中读取为字典类型
我们所用的标签类型为:object instances(目标实例)
整个结构体类型为:

{
    "info": info,
    "licenses": [license],
    "images": [image],
    "annotations": [annotation],
    "categories": [category]
}

我们用到的是images和annotations类型,其都为列表类型,列表长度与图片数量相同
images记录的就是图片的信息,将其单独列出可看到:

{	
	"license": 5,
	"file_name": "COCO_train2014_000000057870.jpg",
	"coco_url": "http://images.cocodataset.org/train2014/COCO_train2014_000000057870.jpg",
	"height": 480,
	"width": 640,
	"date_captured": "2013-11-14 16:28:13",
	"flickr_url": "http://farm4.staticflickr.com/3153/2970773875_164f0c0b83_z.jpg",
	"id": 57870
}

annotations记录的就是识别目标的标记信息,将其单独列出可看到:

{
	'segmentation': [[312.29, 562.89, 402.25, 511.49, 400.96, 425.38, 398.39, 372.69, 388.11, 332.85, 318.71, 325.14, 295.58, 305.86, 269.88, 314.86, 258.31, 337.99, 217.19, 321.29, 182.49, 343.13, 141.37, 348.27, 132.37, 358.55, 159.36, 377.83, 116.95, 421.53, 167.07, 499.92, 232.61, 560.32, 300.72, 571.89]], 
	'area': 54652.9556, 
	'iscrowd': 0,
	'image_id': 480023,
	'bbox': [116.95, 305.86, 285.3, 266.03], 
	'category_id': 58,
	'id': 86}
}

categories记录的就是识别目标的类别信息,将其单独列出可看到:

{
	'supercategory': 'outdoor',
	'id': 11,
	'name': 'fire hydrant'}
}

可以看到bbox选项可能就是我们所用到的属性,但并不确定,以及segmentation猜测有可能是目标特征点,再就是annotations与images的对应,是顺序对应还是id对应也不确定,所以需要后续进行实验验证。

(2)整合标签文件

由上节可知,标签文件为字典类型,参数繁杂,所需要的参数分布不统一,所以可以考虑进行整合,方便操作
整合成总列表的行数代表图片序号(并非与图片ID对应),各列分别为:

图像文件名 类别ID Box属性
COCO_train2014_000000057870.jpg [11,…] [[116.95, 305.86, 285.3, 266.03],[…],…]

这里应注意,考虑到一张图片中可能会有多个box,所以类别IDBox属性应该是一个列表,且所处的位置应存在唯一对应关系,程序实现如下,其中的文件位置自行调整:

import json

label_list = []
# 打开标签文件
with open("E:/DATA/COCO2014/annotations_trainval2014/annotations/instances_train2014.json", 'r') as f:
    label_dict = json.loads(f.read())
num = len(label_dict['images'])

for img in label_dict['images']:
    img_name = img['file_name']
    img_id = img['id']
    # 储存类别ID和box属性的子列表
    sublist_ID = []
    sublist_box = []
    # 搜寻
    for anno in label_dict['annotations']:
        if anno['image_id'] == img_id:
            sublist_ID.append(anno['category_id'])
            sublist_box.append(anno['bbox'])
    # 整合到总列表
    label_list.append([img_name, sublist_ID, sublist_box])

由于数据量庞大,且嵌套了两个循环,所以计算时间会非常长。
在实际进行训练时不应该把时间浪费在这上面,所以考虑每一个图片建立单独的文件,从而在训练时方便快速调用,每一个文件名可储存图像信息(图像名称),文件内部储存类和box信息(分为两行),故修改程序如下:

import json

label_list = []
# 打开标签文件
with open("E:/DATA/COCO2014/annotations_trainval2014/annotations/instances_train2014.json", 'r') as f:
    label_dict = json.loads(f.read())
num = len(label_dict['images'])
# 循环一次搜寻图片对应信息
for img in label_dict['images']:
    img_name = img['file_name']
    img_id = img['id']

    # 储存类别ID和box属性的子列表
    sublist_ID = []
    sublist_box = []
    # 搜寻
    for anno in label_dict['annotations']:
        if anno['image_id'] == img_id:
            sublist_ID.append(anno['category_id'])
            sublist_box.append(anno['bbox'])
    # 写入文件
    with open("E:/DATA/COCO2014/label/"+img_name[:-4]+".txt", 'w') as f:
        f.write(str(sublist_ID))
        f.write('\n')
        f.write(str(sublist_box))

(3)验证box对应关系的正确性

我们随便打开一个图片和标签文件,运用opencv画矩形框,验证box属性四个值分别对应,矩形左上角点坐标,和矩形的宽和高,程序实现如下,主要包括处理储存的标签信息和绘制矩形框。

import cv2
# 打开图像文件
img_name = 'COCO_train2014_000000016672'
img = cv2.imread('E:/DATA/COCO2014/train2014/train2014/' + img_name + '.jpg')

# 打开标签文件
with open("E:/DATA/COCO2014/label/"+img_name+".txt", 'r') as f:
    lab = f.readlines()
catas = lab[0].replace(']', '').replace('[', '').replace(',', '').split()
catas = [int(x) for x in catas]

boxes = lab[1].replace(']', '').replace('[', '').replace(',', '').split()
boxes = [int(float(x)) for x in boxes]

for i in range(len(boxes)//4):
    x, y, w, h = boxes[i*4:i*4+4]
    cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 3)
cv2.imshow("image", img) 
cv2.waitKey(0)

可以看到图像是没有问题的,说明验证正确。
在这里插入图片描述

(4)编写数据读入程序

做完了上述准备工作,接下来就是编写正式的数据读入部分了。
为了方便后续处理,从这一步开始,我们开始运用类的方法,为了程序的可读性,应尽可能把所有程序放在一起,整体方便参考,但代码量会较多,所以我这里会采取局部简化,对于介绍过的内容写class 和def名称,只对当前进行的详细代码进行完整的记录,在接下来的实例中就可以明白了。
首先建立数据读入及处理的总类,pytorch框架的Dataset类,并建立初始化函数。
输入变量包括图像文件路径标签文件路径
这里用到了一个__getitem__函数,可以增加迭代器,为了保证后续产生数据。
后面的main函数暂时用来进行debug

import torch.utils.data as data
import os
import cv2

class Dataset(data.Dataset):
    def __init__(self, img_path, ann_path):
        self.grid_num = 14
        self.image_size = 448
        self.img_path = img_path
        self.ann_path = ann_path
        # 读入图片文件夹下的所有图片名称
        img_list = []
        for _, _, files in os.walk(img_path):
            img_list = [file[:-4] for file in files if file[-4:] == '.jpg']
        self.img_list = img_list
        self.num_samples = len(self.img_list)

    def __getitem__(self, idx):
        # 读入标签
        file_label = '%s%s.txt' % (self.ann_path, self.img_list[idx])
        with open(file_label, 'r') as f:
            lab = f.readlines()
        catas = lab[0].replace(']', '').replace('[', '').replace(',', '').split()
        labels = [int(x) for x in catas]
        boxes = lab[1].replace(']', '').replace('[', '').replace(',', '').split()
        boxes = [(float(x)) for x in boxes]
        boxes = np.array(boxes).reshape((-1, 4))
        boxes = torch.Tensor(boxes)
        labels = torch.Tensor(labels).long()
        # 读入图像
        file_img = '%s%s.jpg' % (self.img_path, self.img_list[idx])
        img = cv2.imread(file_img)

        return print(labels), print(boxes), print(img.shape)


def main():
    dataset = Dataset(img_path='E:/DATA/COCO2014/train2014/train2014/', ann_path='E:/DATA/COCO2014/label/')
    dataset[1]


if __name__ == '__main__':
    main()

看到输出结果如下,说明编写成功。

[25, 25]
[385, 60, 214, 297, 53, 356, 132, 55]
(426, 640, 3)

2.2 图像随机变化

同上,通过上述预处理工作,基本可以对数据进行接下来的训练了,但为了保证训练泛化能力,通常还会在图像预处理期间加入图像随机变化,具体解析可百度详解,这里就只介绍程序实现。

class Dataset(data.Dataset):
    def __init__(self, img_path, ann_path):
        ...

    def __getitem__(self, idx):
        # 读入标签
		#...
        # 读入图像
        #...

        # 图像随机处理
        # 颠倒
        img, boxes = self.rand_flip(img, boxes)
        # 放缩
        img, boxes = self.rand_scale(img, boxes)
        # 滤波
        img = self.rand_blur(img)
        # 亮度
        img = self.rand_bright(img)
        # 色调
        img = self.rand_hue(img)
        # 饱和度
        img = self.rand_saturation(img)
        # 随机剪切
        img, boxes, labels = self.rand_crop(img, boxes, labels)

(1)翻转

关键在于翻转后box属性中的坐标值的计算,考虑框在左右、上下完全颠倒,可自行推算出计算公式。

    def rand_flip(self, im, boxes):
        if random.random() < 0.5:
            im[:, :, 0] = np.flip(im[:, :, 0]).copy()
            im[:, :, 1] = np.flip(im[:, :, 1]).copy()
            im[:, :, 2] = np.flip(im[:, :, 2]).copy()
            h, w, _ = im.shape
            boxes[:, 0] = w - boxes[:, 0] - boxes[:, 2]
            boxes[:, 1] = h - boxes[:, 1] - boxes[:, 3]
        return im, boxes

处理结果
在这里插入图片描述

(2)放缩

随机放缩图片的0.8~1.2倍的宽度和宽度。
box宽度和高度方向的属性也要做对应放缩。

    def rand_scale(self, im, boxes):
        if random.random() < 0.5:
            scalew = random.uniform(0.8, 1.2)
            scaleh = random.uniform(0.8, 1.2)
            h, w, _ = im.shape
            im = cv2.resize(im, (int(w*scalew), int(h*scaleh)))
            scale_boxes = torch.Tensor([[scalew, scaleh, scalew, scaleh]]).expand_as(boxes)
            boxes *= scale_boxes
        return im, boxes

(3)滤波

滤波较为简单,直接运用opencv滤波函数滤波

    def rand_blur(self, im):
        if random.random() < 0.5:
            im = cv2.blur(im, (5, 5))
        return im

(4)亮度

这里用到了一个hsv通道转换,关于hsv的详情可以自行百度

    def rand_bright(self, im):
        if random.random() < 0.5:
            im_hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
            h, s, v = cv2.split(im_hsv)
            adjust = random.choice([0.5, 1.5])
            v = v * adjust
            v = np.clip(v, 0, 255).astype(im_hsv.dtype)
            im_hsv = cv2.merge((h, s, v))
            im = cv2.cvtColor(im_hsv, cv2.COLOR_HSV2BGR)
        return im

(5)色调

同理上

   def rand_hue(self, im):
        if random.random() < 0.5:
            im_hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
            h, s, v = cv2.split(im_hsv)
            adjust = random.choice([0.5, 1.5])
            h = h * adjust
            h = np.clip(h, 0, 255).astype(im_hsv.dtype)
            im_hsv = cv2.merge((h, s, v))
            im = cv2.cvtColor(im_hsv, cv2.COLOR_HSV2BGR)
        return im

(6)饱和度

同理上

    def rand_saturation(self, im):
        if random.random() < 0.5:
            im_hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
            h, s, v = cv2.split(im_hsv)
            adjust = random.choice([0.5, 1.5])
            s = s * adjust
            s = np.clip(s, 0, 255).astype(im_hsv.dtype)
            im_hsv = cv2.merge((h, s, v))
            im = cv2.cvtColor(im_hsv, cv2.COLOR_HSV2BGR)
        return im

(7)裁剪

对于裁剪操作,首先应随机确定裁剪后的图像高度宽度以及位置。
根据位置变化情况,首先依次判定box中心是否还位于图片内部,否则取消该box和所属类别。
如果发现box全出了,说明裁剪的太巧了,把所有box都排开了,这时,返回原有的图像box。
否则,则更新box属性信息到新图片中,对于超过图像的box宽度也要进行修正到边框。
最后将图片更新为裁剪之后的图片

    def rand_crop(self, im, boxes, labeles):
        if random.random() < 0.5:
            h, w, c = im.shape
            rand_h = int(random.uniform(0.6 * h, h))
            rand_w = int(random.uniform(0.6 * w, w))
            rand_x = int(random.uniform(0, w - rand_w))
            rand_y = int(random.uniform(0, h - rand_h))

            cen = boxes[:, 0:2]+boxes[:, 2:]/2
            cen = cen - torch.Tensor([[rand_x, rand_y]]).expand_as(cen)
            mask1 = (cen[:, 0] > 0) & (cen[:, 0] < w)
            mask2 = (cen[:, 1] > 0) & (cen[:, 0] < h)
            mask = (mask1 & mask2).view(-1, 1)
            boxes_crop = boxes[mask.expand_as(boxes)].view(-1, 4)
            labeles_crop = labeles[mask.view(-1)]
            if len(boxes_crop) == 0:
                return im, boxes, labeles
            boxes_crop = boxes_crop - torch.Tensor([[rand_x, rand_y, 0, 0]]).expand_as(boxes_crop)

            for i, value in enumerate(boxes_crop):
                boxes_crop[i, 0] = torch.clamp(value[0], 0, max=w)
                boxes_crop[i, 1] = torch.clamp(value[1], 0, max=h)
                boxes_crop[i, 2] = torch.clamp(value[2], 0, max=w - value[0])
                boxes_crop[i, 3] = torch.clamp(value[3], 0, max=h - value[1])

            img_crop = im[rand_y:rand_y+h, rand_x:rand_x+w]
            return img_crop, boxes_crop, labeles_crop
        return im, boxes, labeles

2.3 图像预处理

图像预处理主要是包括:

  1. 标准化box属性,因为考虑后面的图像尺寸有可能发生变化,所以可以将box单位化,实际上表达的是在图片上的相对位置,所以图像的简单的等比例放大缩小就不会影响box属性了,但对于特定的图像变化,则需要同时对box属性进行处理,这部分放在后面介绍。
  2. 转换通道。由于opencv读取的图片通道格式为BGR,而pytorch处理图片则采取RGB的形式,所以需要通道转化。
  3. 统一尺寸,考虑卷积网络的第一层输入层应该为统一的输入图像尺寸,所以需要resize
  4. 减去均值,这一部分主要是为了更容易优化参数,具体可以自行百度(图像处理减去均值)
  5. 编码,这部分主要是将读取的标签信息处理成卷积网络最后一层的输出格式,方便计算损失函数,具体在下一小节说明。

程序如下:

import torch.utils.data as data
import os
import cv2
import numpy as np
import torch

class Dataset(data.Dataset):
    def __init__(self, img_path, ann_path): 
		#...
    def __getitem__(self, idx):
        # 读入标签
        #...
        # 读入图像
        #...
       	# 随机图像处理
        #...
        # 图像预处理
        # 标准化box属性
        boxes = torch.Tensor(boxes)
        h, w, _ = img.shape
        boxes /= torch.Tensor([w, h, w, h]).expand_as(boxes)
        # 转换通道
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        # 统一尺寸
        img = cv2.resize(img, (self.image_size, self.image_size))
        # 减去均值
        img = img - np.array([123, 117, 104])
        # 编码
        target = self.encoder(boxes, labels)
        return img, target

2.4 标签编码

这一部分主要是将预处理好的图像和box进行整合编码,与卷积网络最后一层输出层格式相同。

输入变量为两个:boxeslabels
输出变量应为17x17x95的张量。
17x17为划分的网格尺寸。
在本文所使用的数据集中,总分类数为90,考虑每个网格只有一个box属性,包括box中心点两个坐标值,box长和宽值和判定概率五个参数,所以共计95个参数。
编码的整体思路就是:
循环boxes和labels
读取box属性
确定中心点所在方格
计算中心点所在方格的相对坐标
按照变量顺序依次写入编码张量
程序如下:

class Dataset(data.Dataset):
    def __init__(self, img_path, ann_path):
        #...

    def __getitem__(self, idx):
        #...
		# 编码
        target = self.encoder(boxes, labels)
        
    def __len__(self):
        return self.num_samples
        
    def encoder(self, boxes, labels):
        target = torch.zeros((self.grid_num, self.grid_num, 95))
        cell_size = 1 / self.grid_num
        for box, label in zip(boxes, labels):
            # box 中心点坐标
            cen_loc = box[:2] + box[2:]/2
            # 中心点所在方格位置
            ij = np.ceil((cen_loc/cell_size))-1
            # 中心点相对坐标
            loc_xy = (cen_loc - ij*cell_size)/cell_size
            # 写入编码
            target[int(ij[1]), int(ij[0]), :2] = loc_xy
            target[int(ij[1]), int(ij[0]), 2:4] = box[2:]
            target[int(ij[1]), int(ij[0]), 4] = 1
            target[int(ij[1]), int(ij[0]), label] = 1
        return target

注意到程序中多了一个 __len__函数,表征的是图像总个数,self.num_samples直接在init里面定义为总图像个数即可。

2.5 产生数据子集

这里用到的就是pytorch框架里面的DataLoader函数,具体详解可以百度,这里直接调用即可

	dataset = Dataset(img_path='E:/DATA/COCO2014/train2014/train2014/', ann_path='E:/DATA/COCO2014/label/')
    train_loader = DataLoader(dataset, batch_size=64, shuffle=False, num_workers=0)

这样,batch数据的任务也就完成了
通过上述流程,也就可以进行自己数据集的建立和导入,从而进行自己数据的训练任务了。

本文中还会有很多错误,还请大家积极指出共同讨论~

2018-03-28 15:58:05 yanchujian88 阅读数 2678

1:车牌号码识别系统

识别步骤:车牌定位,字符分割,字符识别

车牌定位方法,基于直线边缘检测的方法,基于阈值迭代的方法,基于彩色信息的方法,基于灰度检测的方法,基于神经网络的方法。

字符识别:特征提取与模板匹配。

整个识别过程包括了图像预处理和车牌号码识别。

而图像预处理包括了二值化,去噪,车牌定位,字符分割和字符细化。二值化其实就是将灰色图像转换为0-1255)像素图像。去噪就是去除一些噪声,易于识别,采用超限邻域平均法对二值化进行去噪。车牌定位就是车牌提取,这里采用横纵扫描的方法提取车牌。在字符分割之前标准化车牌图像。字符分割:将字符一个一个分割开,利用字间的间隔一般大于自馁间隔这一特点将二者区分。字符细化:目的是要得到与原来区域形状近似的,由简单的弧和曲线组成的图像。

车牌号码识别:常用的方法有基于模板匹配和基于神经网络的方法,模板匹配有较强的容错能力,适于有较强干扰的场合,但识别速度慢,很难满足实时性要求。基于神经网络的方法具有较快的识别速度,尤其对二值图速度更快,可以满足实时性要求。


2:一维条形码识别

   一维条形码是由自足规则排列的粗细不同、黑白相间的条空及数字和字符组成的标记。

通常一维条形码只在一个方向上表达信息,一般是水平方向,对于每一种物品,编码唯一,目前使用频率最高的几种一维条形码码制有EANUPC、三九码、交叉二五码和EAN128。我国采用的编码标准是EAN码。

EAN条形码有两个版本。一个是13位标准条形码;另一个是8位缩短条形码。EAN-13条形码由代表13位数字码的条形码符号组成。这里处理的13为条形码。

    EAN条形码由左侧空白区、起始符、左侧数据符、中间分隔符、右侧数据符、校验符、终止符、右侧空白区组成。其结构如下图所示:



                       商品条形码结构

                  商品条形码符号构成示意图

具体组成这里不仔细讲了。

  一维条形码识别系统设计步骤主要分为条形码预处理与条形码识别两个步骤。图像预处理包括灰度化、二值化、矫正处理和去噪过程。图像识别通过计算左侧数据和由测数据得出最后的条形码值。

     今天就先写到这了,后续会将这些项目的具体实现方法以及其他项目介绍上传。

2016-03-15 09:12:16 fshmeng 阅读数 2433

        最近经朋友介绍,做了一个高铁路边上杆号自动识别的项目,项目自我感觉难度非常大,因为有大量的特殊场景,涉及到图像二值化、分割、识别问题也非常多。项目时间又很紧,成功交付以后感触颇多,再想想这么多年网上查资料的多,共享的少,所以想通过这篇文章把整个项目的解决思路和大家共享一下。

        说实话,目前网上与模式识别相关的文章,大部分都只是些知识点的介绍,或者用例的简单介绍,而所谓项目就是客户提供了一系列的照片,由你自己分析特征、尝试提取、根据结果调整算法,最终生成的可执行程序提供给客户,而且这个结果如果很粗糙或者错误率很高,客户根本是不会接受的。写这篇文章的目的就是和大家共享下这个过程,特别很多刚接触这块的,如何把图像处理、模式识别书中孤立的知识点组合起来,达到最佳效果,从而解决实际问题的。

         这个项目大的方面是高铁6C项目的一个子模块。给我们的具体要求就是客户会有很多组照片,每组照片是一台相机同步高速拍摄,在实际客运列车运行中进行,时速300/250/200,相机有海量的存储器,整个过程拍摄下来后,一组内就有超过2万张照片,这样靠人工分类的工作量非常大,所以需要将每张照片中线杆的杆号能够自动识别出来从而实现将海量照片以杆子的编号进行分类。限于篇幅,这篇文章里面主要介绍线杆的提取部分,其他部分以后再陆续介绍。

         接下来结合实际图,谈谈整个项目过程吧。有个比较有意思的事可以说下,在项目最初洽谈阶段,对方提供了一个小图集给我们。并且还告知我们以前有个另外公司的软件做相同的事情,但是识别率很低,基本上不能接受。我们看了样图之后,认为可以做这个项目。虽然以前也做过不少的工程项目,知道最后要实际应用的图集和样图肯定有偏差,但是当最后拿到实际数据后,复杂程度还是有些比较出乎我们意料的。


                         一个高铁线杆例图


为了保护用户数据,我们找了一张类似的样图。整个图片本身是非常清晰的。杆子由近到远排列,在每个杆子的下面刷有杆号,基本上由数字组成,少量会前面有包含英文字母。因为是海量数据,为了节省存储空间,所以最后图像全部是灰度图像,相比彩色图片而言,这给识别也增加了一定的难度,因为彩色图片会有更多的特征可以提取。

针对样图,我们制定主要思路如下:

1.      对输入图像进行预处理,包括降噪(如果有必要的话),二值化等。

2.      找出线杆,继而分割出文字区域。

3.      对文字区域的数字及字母进行识别。

4.      根据杆子的顺序规律,可以进一步优化结果,以提高整体精确度

5.      输出最终识别结果。

 步骤4只是结果的优化,本文主要谈下实际项目中模式识别的解决思路,这个不再具体介绍。中间输出的结果分别是先合适的线杆,因为图像里面有多个线杆,需要找到最靠近的字迹、最清晰的那个线杆,然后在线杆内找到白底+中间文字那部分,所以有2个中间结果至关重要,线杆和文字区域。

        如果对于上面例图,估计对有经验的来说,都不是什么大问题,先二值化一把,微调下二值化的参数,一般就能得到理想的结果,即杆子是黑色的而背景大部分是白色的,这样能把杆子分割出来。然后的处理,只需要竖直投影法投影一下就行了,找到图像中最宽同时也是最高的柱子,就可以得到所需要的线杆。在得到线杆部分后,像这张图文字区域是白色包围文字区域,整个柱子的颜色灰度值也比较大,所以再次应用二值化还可以轻松将整个线杆中文字的区域置分割出来,然后进行文字识别就行了。在拿到实际大量数据之前,我们也写了一个简单的程序,并且测试了样图,证明我们的思路基本上是可行的。

       大家其实也知道了,最终的图集肯定不可能这么轻松,会有大量的工作,否则之前尝试的那家公司最终怎么会只有3成的准确率,但说实话当拿到测试图集时还是吓了我一大跳,因为真实的图集效果和给的样图确实差距还是非常大的,下面我罗列些典型的恶劣情况给大家看下:


       

(a)  车站内出发,无合适目标                                                 (b) 雨中拍摄的照片                                                 (c) 雨中拍摄的照片,文字被遮挡

      

(d) 相机有大片污渍,且火车在转弯                             (e)  视野内无合适的杆子,只有干扰杆子                                (f)  火车转弯中,杆子相对倾斜

      

(g)    火车转弯中且无合适杆子                                   (h)大斜率的转弯且有玻璃投影,且多个文字区域          (i)     清晨照片,二值化很难

    

(j) 傍晚的照片                                                                  (k) 背景有干扰的照片                                                 (l)   多个杆子并排在一起

        诸位,看了以上这些图,应该理解我为啥在前面说,给的少量样图没有覆盖所有情况。。不能说样图不对,只是人家找的是最理想、最清晰的发过来了,实际的图受到运行时间、相机维护、拍摄位置等等这些因素影响的,锁定最终的文字区域就已经困难重重。综合上面这些困难情况,大致的困难如下:

(1)     大部分相机在拍摄时,会存在污渍和玻璃反光影响问题。污渍会导致很多图中的识别区域被污染,从而影响甚至误导识别结果。最夸张的图集中,每个杆子间间隔的7-8张照片中,只有1-2张是清晰可见的,只要这两张失败当前杆子就会失败。玻璃反光会给二值化带来很大挑战,事实证明普通设置阈值的二值化无法胜任这个工作,经特殊二值化才行。

(2)     早晚拍摄问题。清晨和傍晚拍摄的相片,文字区域对比度太低了,导致整个文字区域根本就不存在之前说的白色文字区域。有人可能觉得可以通过对比度自适应调整来优化,但二者之间灰度值差值实在太小了,在经过几次尝试后,很快就否决了这个方法。具体可以看下下面截取的这块:

       

 (3)     杆子不是竖直的。很多情况下杆子会倾斜,特别在转弯情况下倾斜度会很大,之前说过竖直投影找杆子的方法,也完全可以否定了。倾斜的杆子还带来另外一个问题,直接把倾斜文字区域给tesseract识别的话,识别率会大幅下降,所以找杆子现在还多了一个新任务,计算斜率,供文字区域纠偏使用。

(4)     杆子背景复杂。由于火车是移动的,背景也是变的,还有隔音板等情况,很多图中不再是一根孤零零的竖直杆子,特别杆子底部也很难确定。另外客户提供的图是灰度图也增加了难度,如果彩色图就会相对简单很多,可以提取的特征值也会更多,在多个特征值情况下可以夹逼出想要的区域。还有一个情况就是会存在多个杆子并列情况。

(5)     文字区域数量变化大。文字区域不再是例图中的一个明显区域,有可能同时有2个或更多,有可能杆子上没有文字区域。

(6)     不能完全利用数字的前后规律。杆子虽然是按一部分规律前进的,但这个规律经常会被打断,所以还是要识别尽可能准确,规律只能用于少数文字被污渍遮挡时的结果优化。

       在确定上述几条困难后,重新审视下之前提到的方法,从思路看还是应该那样,先找到线杆,再定位出文字区域,最后结合前面杆子的识别文字区域内的数字。只是现在可以确定的是,这三大步步骤没错,但每一步都很难做到结果百分之百正确,特别由于污渍、光照2个因素影响,定位文字区域和字符识别两步的准确率肯定会受影响。而且原来设想的简单通过二值化方法已经行不通了。经过进一步的分析后,虽然找杆子也有很多困难和挑战,但相对来说特征还是更多点,只有这一步做到接近百分之百的成功,后续才可能总的准确率超过5成,所以我先介绍如何在上述那些困难中准确的找到杆子。

在动手编程找杆子之前,就需要先总结下这个项目中杆子的特征,总结如下:

(1)     杆子都是相对地面竖直的黑色柱体

(2)     大部分情况下,杆子的左右两侧会有大部分空白或者部分空白

(3)     图中会有多个这样的柱子,离最近的是希望识别的柱子,远的是干扰。

因为采用直接二值化方法把线杆分割出来的方法已经不可行了,所以最后我们换了一种思路,就是把线杆的竖直边缘寻找出来,然后根据两条平行边缘的灰度值与背景灰度值之差来确定线杆。最后设计的搜索杆子方案如下:

(1)     利用Sobel算子增强出线杆的竖直边缘。

(2)     对(1)的结果进行二值化处理。

(3)     使用hough变换检测直线。

(4)     利用上面提到的特征1和2,标记疑似的杆子两边的线

(5)     因为会有多个平行杆子,我们选择了最粗的杆子并从中提取杆号。

 

我们之所以采用这种方法提取边缘,而不是采用比如canny之类的边缘提取方法,是因为Canny之类的边缘提取结果对于我们这个项目有两个弊端,一是提取的结果包含了太多我们不需要的边缘,二是提取出来的边缘即使在线杆两侧也未必是直线。而Sobel算子一方面增强了边缘,另外,方法具有方向性,所以可以只提取我们所需要的竖直线。另一个优点是这种方向性也可以帮我们甄别提取的边缘是线杆的左边缘还是右边缘,最后的结果也证明了这种方法给我们的识别带来了极大的便利性。

经过Sobel算子增强后的边缘经过二值化处理后可以直接利用hough变换检测直线。说到hough变换检测直线,这也是让我萌生写这篇分享的原因之一,hough的原理是将图像空间的像素转变到参数空间,然后在参数空间中对直线/圆/椭圆的点进行统计,最后通过阈值判决是否是符合要求的形状,所以其实hough出来的结果是相对杂乱无章的,不可能一个hough就能得到想要的结果,只能说想要的结果在hough检测中,需要再找出来,而目前国内相关技术文章只介绍这个方法,如何做下一步就不谈了。

以下各图显示了线杆的提取过程:

         

           (a)    通过Sobel算子对竖直边缘增强                                                                                                                      (b)    二值化增强结果

           

(c)  提取出来所有的直线,可以看出主要都是竖线                                                                    (d)  经过进一步优化处理后提取的线杆,其中绿色的左边缘,红色的是右边缘。

 

以上大概就是线杆提取的过程。 限于篇幅,文字区域提取将在今后在进一步详细介绍。 

      项目采用的编程环境是vs2013的opencv+tesseract,由于时间紧,来不及训练一个识别器,否则还可以提供下识别的准确性,因为所需识别字符相对来说比较简单,做起来其实也不复杂,可用的方法也很多,比如决策树,SVM,神经网络以及最近比较流行的深度学习,效果应该都不错。但是因为时间关系,我们先是直接试了一下Tesseract。Tesseract大家都比较熟悉了,识别的结果可以接受,再加上调用方便,这样为项目节省了很多时间。使用opencv的好处,也在于opencv很多算法可以迅速调用,结果不理想时替换也方便,毕竟自己从头写每个算法还是太耗时了。这里说给题外话,所谓实际项目接触的一般都是非图像处理、模式识别相关方面人士,专业性会比较差,哪怕对方也是计算机相关的,后来会提到开始给的测试图,还是产生了不少误会,导致最终算法走了些弯路。而做过实际项目的都知道,非专业人士,很多需求他们觉得很轻松随口就提了下,而有时候这些需求根本是无法实现的,因为毕竟目前模式识别技术还是有瓶颈,识别能力有限。像我这个项目问对方有没有对性能有要求,对方回复是没有要求,几个小时内一组做出来就行,但计算下就知道了,哪怕8个小时,换成时间是3600×8=28800秒。     而一组是2万张照片的话,每张这样处理时间是1.44秒。这意味着,每张照片图像分割到最后字符识别,总的时间耗费不能超过1.44秒,而做过图像相关的项目都知道,很多算法是非常耗时的,所以选择用opencv+tesseract+自己编写算法,这样才可能在处理速度上达到用户的需求,其实还有不少加速处理的技巧,我们最后结果是一个小时内可以跑4万到5万张图片。 而且这个结果是单进程,单线程条件下实现的,相信如果采用多线程等方法后,速度还可以进一步提高。。处理时间还好,用户最关心的还是识别率问题,用户找过来时提到一点,其实他们已经找过其他公司尝试做过,但出来的识别率3成不到,所以客户放弃了他们。这个就是模式识别的残酷性,可能对方也花了大量时间和精力,但用户这些不会考虑这些的,人家只会看最终结果。所以建议在项目开始之前还是好好评估下自己能力和用户需求,如果达不到不必强求,毕竟我也经常遇到并不靠谱的需求。

      其实这些需求里面,还有一个隐藏的需求就是代码稳定性,当然所有客户是默认这点没有问题,但如果最终算法很完美,处理时间也能控制在一秒之内,但一组就有2万张图,可能跑过几十张异常了就挂了,或者内存泄漏跑了几千张malloc失败导致挂了或者处理不下去了(做c/c++都知道会有内存泄漏问题),这样同样客户是不会接受的。说到这个,从我读书到实际工作这么多年,国内学校的研究生导师和博导对编程能力这块很经常忽视,说不客气点甚至有的是轻视,认为只要算法出来就行,我接触的有人甚至这么说,算法出来了找几个有经验的工程师优化下就行了,对这点我实在无法苟同。诚然图像处理、模式识别算法的设计很重要,但程序是实现你思维的途径,最基本编程能力都没,比如写几行一堆错debug都要半天,怎么实现你思维;复杂的项目必须由很多工作组成,代码能力不够怎么驾驭几千行甚至上万行代码;或者代码由不懂你算法的工程师来优化,怎么保证他优化后的代码和你原来执行结果百分之百相同,而且当有不一致时如何定位错在哪里;再比如大部分图像处理书为了方便理解,取某一个点都是用pixel[y*width+ x]这种方法来表示的,学生按这样写出来的算法,光遍历个图像就有大量的乘法,前面提到的一秒的处理要求如何能达到?所以编程能力和算法设计是同等重要的,像这个项目,我在项目之初就注意到这点,所以在内存分配和释放上非常当心,并且出错处理都加了打印和保护(网上经常有人说,大学老师教出来的都是只有if没else的,出错处理都要到公司才学会),可以接受某个图无识别结果但不能跑挂,因为我这个项目时间很紧,只有2个半月的时间,基本上最终代码出来自己试跑几次后就要给客户了,事实上我也在试跑几次后就开始没有任何内存泄漏,在处理几十张图像后内存始终在很小范围内波动而不是累加的,然后又解了几个中间跑挂的问题后,后期当客户测试时都是使用新的图集进行测试的,没有收到任何跑挂的反馈








2018-03-26 16:27:37 silentsilent12345 阅读数 568

本人由于项目需求开始学习CUDA并行编程,CUDA_by_examples是一本入门级神书,但是我在尝试编写其中的第一个例程Julia时却没那么顺利,原因是我们安装的vs中不包含图像处理需要的glut、glut32等库文件,多次查资料修改后运行成功。在这里将自己的整个配置过程写下来供需要的人参考。还是学生,第一次写博客,尽量表达清楚。

第一步:下载vs2015(请自行到官网下载,这里我用的是2015专业版)

第二步:下载DirectX以及cuda_8.0.61_windows

DirectX下载:https://pan.baidu.com/s/1nbIFSEj-Eq6DMJXbWpDShA

cuda 8.0.61下载:https://pan.baidu.com/s/1rgd1wBzuq05bkMNoxthA4w

第三步:安装vs2015(按照步骤安装即可)

第四步:安装cuda,首先安装DirectX(安装过程全部默认即可),然后安装cuda_8.0.61_windows(默认安装在c盘,无特殊情况请毋更改)。

到此准备工作结束,这时打开vs创建一个CUDA工程,将书中的代码敲入到工程中,你会发现会因无法找到book.h以及cpu_bitmap.h头文件而报错,书中的原代码如下

#include ”.. /common/book.h"
#include “.. /common/cpu_ bitmap.h"

其中的..指的是上级目录,因为我们没有它的common文件,所以无法包含。

解决办法:

将下面的common文件放到你的工程文件夹下,然后添加头文件时引入即可,比如我将common文件放在文件夹D:julia文件夹下,则我的头文件包含为

#include"D:\julia\common\book.h"
#include"D:\julia\common\cpu_bitmap.h"

commom下载地址:https://download.csdn.net/download/silentsilent12345/10309292

到此第一个问题解决,然后我们运行程序,发现报错无法找到glut32.lib或无法找到glut32.dll

解决办法:

下载下面的glut文件夹,里面包含glut.dll,glut.lib,glut32.dll,glut32.lib四个文件,将glut.dll跟glut32.dll均复制到c:\windows\system32和c:\windows\SysWOW64两个文件夹下(一定要这两个文件夹下都放上这两个dll文件!),然后将glut.lib跟glut32.lib复制到vs的Lib目录下(vs安装目录下的VC\lib目录下,例如我的路径为D:\vs2015\VC\lib)

glut和glut32下载地址:https://download.csdn.net/download/silentsilent12345/10309245

然后打开你的工程,项目-》属性-》连接器中的常规界面下,将你上面lib文件路径添加到附加库目录,点击确定,应用



输入界面下的附加依赖项添加glut32.lib,glut.lib,点击确定,应用




此时环境配置工作完成,再次运行环境,若成功则可正确使用,若发现还是报错,出现无法找到_imp_gl。。。无法找到等类似错误,这是opengl自己的一个bug,下面给出解决方案:

有两种发放,自己试下那种方法适用于自己:

第一种:在文件最开始添加下列代码

#ifdef GLUT_DISABLE_ATEXIT_HACK
#define GLUT_DISABLE_ATEXIT_HACK
#endif 

第二种:打开项目-》属性-》将配置改为所有配置,在CUDA C/C++下的host界面的PreProcessor Definitions(预处理定义)下添加GLUT_BUILDING_LIB


然后将配置改为活动,在配置属性-》连接器的系统界面的子系统选择如下系统。


点击确定,应用,再次运行程序,程序可正确运行。

2014-01-24 21:48:28 xiaozi988628 阅读数 236

        平时拿c语言做什么呢?笔者曾经参与过某图像处理前端处理项目小组,仅仅负责其中的firmware集成部分,本文就从此出发,写一点自己的感悟,不对之处,还望指正。

        刚接触整个firmware时,我就很好奇整个的编译过程,循着makefile文件,一点一点梳理整个过程。

        其次,熟悉firmware的软件架构。

        最后,步入正题,c语言的基础。

 

        好了,废话也就不多说了,今天就说说结构体。

        最基本的结构体里是封装了不同类型的数据,可以作为“超数组”来用。如果再封装上函数指针,是不是看着像“类”,方法和数据封装在一起。

没有更多推荐了,返回首页