综述
我们在进行人脸属性识别深度学习算法研究过程中除了使用开源带标签的数据以外,都会根据具体使用场景与需求用到大量自收集的图像数据(开源/爬虫/自拍等),然这些数据一般是没有人脸对应属性标注标签的。而我们在研究人脸各种检测算法时最终训练需要的数据就是图像+标签,所以如何快速标注这些特定数据便是数据收集工作的重点。本文主要讲一下如何通过python+Flask+SocketIO+Html技术实现局域网多人在线协同标注人脸各种数据的系统,在此做一个分享。
标注目标确定
- 待标注图片:带有人脸的照片(单人脸/人脸区域在整个图像的占比足够多/各种场景下的人脸)
- 可标注属性:人脸所有可通过目测判断且单个数据标注数量较少的分属性值(分属性个数小于10)
- 标签文件:txt文本
- 标注文本格式:
图片文件相对路径 属性值1...属性值n
- 数据命名规范:图片文件根目录与标签文件同名(除后缀名以外)
标注形式
- 标注操作界面以在线网页的形式打开
- 根据网页上对标注规则的解读以及待标注图片的观察进行手动输入标注
- 在局域网内,多人可同时进行在线标注,且每个人之间的标注操作互不干扰
- 后台对所有待标注图片进行遍历显示,并将标注好的数据更新保存至标签文件
多人在线协同标注系统开发所需的关键技术
标注网页界面设计技术
- 实现功能:待标注图片显示、标注规则图片示例、当前标注图片路径、剩余标注图片数量、标注结果提交按钮、相关标注操作及规则说明
- 关键代码
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<!-- 网页标题 -->
<title>人脸图片标注平台</title>
<script type="text/javascript" src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
<script type="text/javascript" src="//cdn.bootcss.com/socket.io/1.5.1/socket.io.min.js"></script>
</head>
<body>
<!-- 显示尺寸 -->
<div style="height:1200px; width:1000px ;background-color:#E0E0E0; margin-left:auto; margin-right:auto;">
<br>
<!-- 具体标注属性标题,本示例为人脸姿态标注 -->
<h1 align="center">人脸姿态标注平台</h1>
<h2 id="t"></h2>
<br>
<div style="width:500px; border:1px solid black;"></div>
<table><tr>
<!-- 待标注图片显示 -->
<td> <img src="{{image_path}}" id="image_show" width="300" height="300"/></td>
<!-- 标注示例图片显示 -->
<td> <img src="./static/images/label_sample.png" width="571" height="300"/></td>
</tr></table>
<div>
<br>
<!-- 第三方算法预测,本示例为百度开放算法接口预测,主要用作标注参考 -->
百度预测:
yaw:<input type="text" id="yaw_baidu_new" name="yaw_baidu" style="width:80px; height:23px; font-size:20px" readonly="readonly" >
pitch:<input type="text" id="pitch_baidu_new" name="pitch_baidu" style="width:80px; height:23px; font-size:20px" readonly="readonly" >
roll:<input type="text" id="roll_baidu_new" name="roll_baidu" style="width:80px; height:23px; font-size:20px" readonly="readonly" >
<!-- 标签标注栏 -->
<form action="#" method="post" enctype="multipart/form-data">
修改标签:
yaw:<input type="text" id="yaw_new" name="input_yaw" style="width:80px; height:23px; font-size:20px">
pitch:<input type="text" id="pitch_new" name="input_pitch" style="width:80px; height:23px; font-size:20px">
roll:<input type="text" id="roll_new" name="input_roll" style="width:80px; height:23px; font-size:20px">
<br>
</form>
<br>
</div>
<!-- 标注提交按钮:提交当前图片标注信息,并切换至下一张待图片 -->
<input type="button" name="submit" value="提交" onclick="submit()" style="width:220px; height:40px; ">
<!-- 其他操作处理按钮:可根据标注情况自定义功能,如不确定标注、删除数据等 -->
<input type="button" name="submit" value="其他操作" onclick="otherOpr()" style="width:220px; height:40px; ">
<br>
<br>
<!-- 标注数值简要提示 -->
yaw 左右转头(左+ 右-) pitch 抬头低头(上+ 下-) roll 左右歪头(左- 右+)
<br>
<br>
<!-- 当前标注图片路径显示 -->
图片路径:<input type="text" id="image_path_new" name="image_path" readonly="readonly" style="height:20px;width:449px">
<br>
<!-- 剩余标注数据数量显示 -->
剩余图片:<input type="text" id="image_numbers" name="image_numbers" readonly="readonly" style="height:20px;width:50px">
<br>
<br>
<br>
<!-- 标注操作说明 -->
操作步骤:<br>
1.填入“修改标签”的三个角度值<br>
2.点击“提交”<br>
(如果角度不用修改,直接点击“提交”即可,会使用默认的参数)<br>
<br>
标注说明:
<br>
1.百度预测这一栏为百度预测的结果(不一定准确),可以作为参考对比,方便我们修改标签<br>(一般小角度误差不用理,主要看误差较大的角度,然后手动更正提交即可)<br>
<br>
2.填入的这三个角度值范围:-90~90<br>
<br>
</div>
</body>
</html>
网页界面与后台通信交互技术
- 实现功能:待标注数据预处理文件读取、待标注数据标注状态文件更新、待标注数据标注文件更新
- 关键代码
<script type="text/javascript">
// 建立socket连接,等待服务器“推送”数据,用回调函数更新图表
namespace = '/test';
// socket连接路径
var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace, {transports: ['websocket']});
// 更新显示后台标注相关数据
$(document).ready(function() {
socket.on('server_response', function(res) {
console.log(res.data);
$('#t').text(res.data);
});
socket.on('image_info', function(res) {
console.log(res.data[0]);
console.log(res.data[1]);
console.log(res.data[2]);
console.log(res.data[3]);
console.log(res.data[4]);
console.log(res.data[5]);
console.log(res.data[6]);
console.log(res.data[7]);
// 标注属性显示
$('#image_path_new').val(res.data[0]);
$('#yaw_new').val(res.data[1]);
$('#pitch_new').val(res.data[2]);
$('#roll_new').val(res.data[3]);
$('#yaw_baidu_new').val(res.data[4]);
$('#pitch_baidu_new').val(res.data[5]);
$('#roll_baidu_new').val(res.data[6]);
$('#roll_baidu_new').val(res.data[6]);
$('#image_numbers').val(res.data[7]);
//对当前标注与参考标注出现差异时,显示为红色字体,更为醒目
//yaw
if(Math.abs(parseFloat(res.data[1]) - parseFloat(res.data[4])) >= 8 ) {
$('#yaw_new').css("color", "red");
} else {
$('#yaw_new').css("color", "black");
}
//pitch
if(Math.abs(parseFloat(res.data[2]) - parseFloat(res.data[5])) >= 8 ) {
$('#pitch_new').css("color", "red");
} else {
$('#pitch_new').css("color", "black");
}
//roll
if(Math.abs(parseFloat(res.data[3]) - parseFloat(res.data[6])) >= 8 ) {
$('#roll_new').css("color", "red");
} else {
$('#roll_new').css("color", "black");
}
document.getElementById("image_show").src=res.data[0];
});
});
// 当提交按钮按下时,会将当前标注数据发送至后台,并更新相关标注状态及标签文件
function submit() {
var image_pathname = document.getElementById('image_path_new').value;
var input_yaw = document.getElementById('yaw_new').value; //get input yaw value
var input_pitch = document.getElementById('pitch_new').value;
var input_roll = document.getElementById('roll_new').value;
var command = "submit"
//标注数据合法性校验,只能输入-90~90之间的整数,防止乱入字符
var re = /^-?[0-9]+.?[0-9]*$/;
if (((!re.test(input_yaw)) || (!re.test(input_pitch)) || (!re.test(input_roll))) || ((Math.abs(parseFloat(input_yaw)) > 90) || (Math.abs(parseFloat(input_pitch)) > 90) || (Math.abs(parseFloat(input_roll)) > 90))) {
alert("输入的数据不合法");
} else {
//携参向后台发送命令
socket.emit('request_for_response', {'param': [input_yaw, input_pitch, input_roll, image_pathname, command]});
}
}
//其他控制操作
function otherOpr() {
//可自定义
}
</script>
# 打开网页时初始化显示标注数据
@socketio.on('connect', namespace='/test')
def test_connect():
# 设置操作空间,保证多人协同标注时每个人操作的独立性
print('client connect: ', request.sid)
session['room'] = str(request.sid)
room = session.get('room')
join_room(room) # join room
# 预处理待标注数据,更新标注状态
all_info = data_process()
image_numbers = len(all_info)
if image_numbers > 0:
context = all_info[0]
# 设置当前标注状态为正在标注
flag_value = '2'
update_original_flag(context['image_path'], flag_value)
session['name'] = context['image_path']
# 将当前标注数据显示到网页前端
send_data = [context['image_path'], context['yaw'], context['pitch'], context['roll'],
context['yaw_baidu'], context['pitch_baidu'], context['roll_baidu'], str(image_numbers)]
print('send_data: ', send_data)
socketio.emit('image_info',
{'data': send_data}, namespace='/test', room=room)
else:
# 标注完成,不再发送新的数据
send_data = [None, None, None, None,
None, None, None, str(image_numbers)]
print('send_data: ', send_data)
print('\n')
room = session.get('room')
socketio.emit('image_info',
{'data': send_data}, namespace='/test', room=room)
# 网页数据接收操作
@socketio.on('request_for_response',namespace='/test')
def receive_html_data(data):
# 解析接收参数
value = data.get('param')
print('receive_html_data: ', value)
receive_data = {
'yaw':value[0], 'pitch':value[1] ,'roll':value[2] , 'image_path':value[3]
}
command = value[4]
if (command == 'submit'):
# 更新标注状态
update_label_angle(receive_data)
image_pathname = value[3]
flag_value = '1'
update_original_flag(image_pathname, flag_value)
# 获取下一个待标注数据并发送至前端网页显示
all_info = data_process() # image list
image_numbers = len(all_info) # the number of remain images
if image_numbers > 0:
context = all_info[0] # select first image
session['name'] = context['image_path'] # recording need to recovery image name
# 更新标注状态
flag_value = '2'
update_original_flag(context['image_path'], flag_value)
# send new image infomation to html
send_data = [context['image_path'], context['yaw'], context['pitch'], context['roll'],
context['yaw_baidu'], context['pitch_baidu'], context['roll_baidu'], str(image_numbers)]
print('send_data: ', send_data)
print('\n')
# 获取当前操作终端主体,保持主体间的操作独立
room = session.get('room')
socketio.emit('image_info',
{'data': send_data}, namespace='/test', room=room)
else:
# 标注完成操作
send_data = [None, None, None, None,
None, None, None, str(image_numbers)]
print('send_data: ', send_data)
print('\n')
room = session.get('room')
socketio.emit('image_info',
{'data': send_data}, namespace='/test', room=room)
elif (command == 'otherOpr'):
# 其他具体操作,可自定义
不同标注人之间操作独立技术
- 实现功能:标注人之间的操作互不干扰、不同标注人正在标注的数据也会不同且唯一
- 关键代码
# 设置操作终端,保证多人协同标注时每个人操作的独立性
session['room'] = str(request.sid)
room = session.get('room')
join_room(room)
# 获取当前操作终端主体,保持主体间的操作独立
room = session.get('room')
socketio.emit('image_info', {'data': send_data}, namespace='/test', room=room)
标注交互中间数据处理技术
- 实现功能:待标注数据及属性值初始化、标注数据状态动态变化
- 关键代码
def data_process():
# 待标注原数据列表、对比标注信息及标注状态
current_txt_path = './label_compare.txt'
# 读取当前标注信息
all_info = []
lines = open(current_txt_path, 'r')
for line in lines:
line = line.rstrip()
words = line.split()
label_flag = words[8]
if label_flag == '0':
# 整合所有未标注数据
image_path, yaw, pitch, roll , yaw_baidu, pitch_baidu, roll_baidu = \
words[0], words[1], words[2], words[3], words[5], words[6], words[7]
one_image_info = {'image_path':image_path, 'yaw': yaw, 'pitch': pitch, 'roll': roll,
'yaw_baidu':yaw_baidu, 'pitch_baidu':pitch_baidu, 'roll_baidu':roll_baidu}
all_info.append(one_image_info)
else:
continue
return all_info
# 更新训练数据标签
def update_label_angle(headpose_dict):
pose_value = headpose_dict
image_name = pose_value['image_path']
yaw_value = pose_value['yaw']
pitch_value = pose_value['pitch']
roll_value = pose_value['roll']
# 更新训练标签文件
target_txt_path = './train.txt'
print('update: ', target_txt_path)
newLines = []
lines = open(target_txt_path, 'r')
for line in lines:
line2 = line
line = line.rstrip()
words = line.split()
old_image_path = words[0]
if old_image_path == image_name:
cont_labels = np.array([yaw_value, pitch_value, roll_value])
pose_str = ' '.join(list(map(str, cont_labels.tolist())))
new_lines = '{} {}\n'.format(image_name, pose_str)
line2 = line2.replace(line2, new_lines)
newLines.append(line2)
# 互斥写入标签数据
with open(target_txt_path, 'w') as f:
fcntl.flock(f.fileno(), fcntl.LOCK_EX) # file lock, after f.close(), unlock
f.writelines(newLines)
f.close()
# 标注状态更新
# flag_value:
# 0: 未标注
# 1: 已标注
# 2: 正在标注
def update_original_flag(image_pathname, flag_value):
original_txt_path = './label_compare.txt'
# 标注状态标志位更新
newLines_2 = []
print('update: ', original_txt_path)
lines_22 = open(original_txt_path, 'r')
for line_2 in lines_22:
new_lines_2 = line_2
line_2a = line_2
line_2a = line_2a.rstrip()
words_2 = line_2a.split()
words_2[8] = flag_value # change flag value
old_image_path_2 = words_2[0]
if old_image_path_2 == image_pathname:
cont_labels_2 = np.array(words_2[1:4])
pose_str_2 = ' '.join(list(map(str, cont_labels_2.tolist())))
cont_labels_3 = np.array(words_2[5:9])
pose_str_3 = ' '.join(list(map(str, cont_labels_3.tolist())))
new_lines_2 = '{} {} | {}\n'.format(image_pathname, pose_str_2, pose_str_3)
newLines_2.append(new_lines_2)
# 互斥写入标注状态标志位
with open(original_txt_path, 'w') as f:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
f.writelines(newLines_2)
f.close()
开发所需库
python:
numpy
fcntl
flask(Flask, request, render_template, session)
threading(Lock)
flask_socketio(SocketIO, join_room, leave_room)
javascript:
jquery.min.js
socket.io.min.js
标注工具完整工程地址
晚些时候上传…
系统使用
晚些时候补充…
至此,一个简易完整的多人在线协同数据标注系统便完成了,大家可以愉快的标注数据了。可以将界面设计的更漂亮些,功能更完备些。当然一般大型公司的AI团队都会开发自己的标注系统,这些系统更为专业和全面,有的甚至开放了出来,比如百度。本系统的优势就是可快速上手和部署,既可保证AI项目的开发效率,也可保证训练数据隐私,所以更适合小型AI团队在数据收集阶段的开发工作。