Laravel起步

使用Laravel从头重新实现图像标注工具。此节只简单学习使用Laravel吧。

环境介绍和安装

我就使用手头的windows10系统 + xampp(php7)为开发环境吧。

为了方便使用php命令,xampp/php目录加入了系统PATH路径中。

参考 https://getcomposer.org/doc/00-intro.md#installation-windows 安装composer,路径也加入到系统PATH路径中。

然后安装Laravel,命令composer global require "laravel/installer",会将laravel安装在其global工作目录即C:\Users\[windows username]\AppData\Roaming\Composer下。

Laravel项目开创

参考文章Create an Admin Interface in LaravelPHP laravel系列之迷你博客搭建 – PHP 中文社区 这两个例子都非常完整。

composer create-project laravel/laravel roidrawer-project

创建完就可以把 roidrawer-project/public目录添加到apache配置文件中了。

后面参考上面的例子搭建用户管理系统也可以,不过我觉得使用 https://github.com/Zizaco/entrust 来搭建项目所用用户管理系统。

最后,我觉得 https://tutorials.kode-blog.com/laravel-5-tutorialhttps://tutorials.kode-blog.com/laravel-5-admin-panel 这两个教程也不错。

图像识别领域在线测试的方法

上一篇文章图像标注工具的一些做法讲述了数据集的标注,此篇讲解使用一个数据集测试的方案。

很简单,我们的测试集肯定要恒定以方便结果之间的比对。我的做法就是对测试集来源数据集做一次快照以固化数据标定内容,这样也方便之后对数据集进行修订,我把快照的结果称为测试模板。如果实在是原始数据集更新了数据标注,就需要再次快照成一个测试模板,而这基于两个测试模板分别进行的测试的结果显然将不具备太大的比较意义。

服务器端呢,直接提供几个API让客户端能连接上创建测试任务下载测试数据和提交测试结果便可以了。

测试过程其实也好办,本地构建一个简单的推断程序,通过服务器端API创建任务,依次下载测试集数据执行推断,并将推断结果提交到服务器即可。一个测试任务的结果全部提交后,服务器可以自动给出召回率准确度和其他指标。

图像标注工具的一些做法

我主要面向图像分类和BoundingBox检测标注,没有考虑图像的语义标注、多边形标注的分割任务等,也没有考虑视频和语音类媒体。

我把(待)标注的数据集成为数据仓库,按照任务分为:
01二类:只包含正样本负样本两个分类;
带删除的01二类:附带一个删除操作将样本剔除;
多类分类:多个分类的分类标注;
单类ROI:单一目标类型的BoundingBox标注;
多类ROI:多类目标的BoundingBox标注,相比单类需要给出每个BBOX的类型了;
多标签ROI:每个BoundingBox可以给定多个标签。

其中,我可以把多个“单类ROI”加上类型合成一个“多类ROI”,这样的话,对于标注多类的任务就可以先拆解成多个单类的任务以减少标注时选择类别的操作。

为了提高操作效率,除了拖曳BoundingBox外,尽量使用键盘完成,比如选择类别标签和提交结果转到上一张下一张等等。具体,就是使用空格和回车键提交结果并转下一张,使用数字键和部分字母键直接给刚拖曳得到的Box赋予类别信息,和游戏一样使用方向键和WASD键来执行转到上一张下一张,甚至还设置了右键提交的选项,拖曳box完成后右键即执行提交。

这些操作优化都是针对正常途径操作的,对于修改操作等提供了到达方式但是没有这些优化操作。

经过这些优化,我自认为这个标注系统的操作还是很高效的,每人一小时甚至可以标注上千张图片。因为提交自动转下一张,这个操作甚至导致我成瘾难以推出任务。

图像标注工具简单列表

究竟需要什么样的标注工具:单人or多人协同?最后数据保存格式?运行环境?分类任务or/and检测任务?效率和速度?等等。确定了这些才可以去挑选工具或自制工具。

有图像标注工具推荐或者分享吗? – 知乎
What is the best image labeling tool for object detection? – Quora

1
tzutalin/labelImg: LabelImg is a graphical image annotation tool and label object bounding boxes in images
py+qt单机软件,保存结果PASVAL VOC XML格式。

2
hzylmf/od-annotation: 目标检测数据标注工具
python flask写就的B/S架构的工具,支持多人。结果可导出为PASCAL VOC XML格式。

3
Image Labeler Label ground truth in a collection of images
Matlab工具,支持多种标注类型。

4
LabelMe. The Open annotation tool
MIT出品,提供手机APP,提供Matlab接口。

5
wkentaro/labelme: Image Polygonal Annotation with Python.
高仿MIT LabelMe的PyQT单机软件。

6
lanbing510/ImageLabel: 图像标注工具 Image Label Tool
单机多边形标注工具

7
VGG Image Annotator (VIA)
VGG组提供的多边形标注工具,html+js写就,单机浏览器下使用。

8
OpenCV探索之路(二十五):制作简易的图像标注小工具 – Madcola – 博客园
网友自写简易工具

9
Labelbox: Data labeling platform for expert artificial intelligence applications
提供商业服务,支持COCO和VOC XML格式。

10
Dataturks – ML data annotations and labeling doesn’t need to suck
提供商业服务

11
NaturalIntelligence/imglab: Web based tool to label images for objects. So that they can be used to train dlib or other object detectors. Integrated with Face++

12
精灵标注助手-人工智能数据集标注必备工具

13
RectLabel – Labeling images for bounding box object detection and segmentation

14
OCLAVI | Object Classification and Annotation for Computer Vision Models
提供商业服务

15
Edgecase AI data labeling | Bounding Box
提供商业服务

16
puzzledqs/BBox-Label-Tool: A simple tool for labeling object bounding boxes in images
python tkinter 单机工具

17
LabelD by sweppner

18
A Universal Labeling Tool for Computer Vision: Sloth

19
LIBLABEL: Lightweight Semantic/Instance Annotation Tool Autonomous Vision Group | MPI for Intelligent Systems

20
wadefelix/ROIDrawer 把我的也贴一贴。计划重写中。。。
B/S架构,支持多人,支持导出PASCAL VOC XML。关注效率,针对左右键盘右手鼠标这种操作方式特别优化设计,除了拖曳bounding box外重复操作尽量使用键盘按键完成。

 

微信里的图像音视频

文件名

在目录Tencent/MicroMsg/目录下的一个ID字符串目录下。
voice2目录存放的是语音。在经过两级2位十六进制字母散列的目录中文件是按“秒时分日月年”的顺序组成文件名msg_后的前12位数字。
video目录存放是视频文件和相关的图片。目录中文件是按“时分秒日月年”的顺序组成文件名前12位数字。比如”083909140318afaaef195653.mp4″,就是“08点39分09秒在14日03月18年”在手机上生成的。同个小视频若转发到多个对话会出现多个文件和文件名。

image2目录中存放的用户聊天图片,也是经过两级2位十六进制字母散列的目录中,但是文件名貌似随机无规律。

上面都是自动保存的原文件等。如果视频图片保存下载到Tencent/MicroMsg/WeiXin文件夹下,则视频直接使用保存时的unixtime时间戳,而图片则是mmexport加上unixtime时间戳。但是注意下载的图片可image2里自动缓存的图片清晰度高,保存的视频无区别。

对于使用微信发送消息框直接拍摄的图片和视频则也是保存在Tencent/MicroMsg/WeiXin文件夹下,文件名时wx_camera_加上unixtime时间戳。

微信amr音频文件转mp3

微信amr音频使用silk v3编码,工具集 https://github.com/kn007/silk-v3-decoder/tree/master/windows
BAT批处理程序示例

setlocal enabledelayedexpansion

for /r D:\\micromsgvoice2 %%i in (*.amr) do (
set fullfilename=%%i
C:\silk2mp3\silk_v3_decoder.exe !fullfilename! msg.pcm -Fs_API 44100
C:\silk2mp3\lame.exe -ar -s 24 msg.pcm !fullfilename:.amr=.mp3!
)

另外,我这里还有个自动通过ftp从手机下载音频文件的脚本 https://github.com/wadefelix/Scripts100/blob/master/fetchmicromsgvoice2.py

改DICOM这种平面结构数据为对象结构

DICOM格式中所有信息条目类似于平铺地保存于一张表格中,与协议中按对象组织的信息条目组合不对应。

例如fo-dicom中的示例也都是这样编辑条目

var file = await DicomFile.OpenAsync(@"test.dcm"); // Alt 2

var patientid = file.Dataset.Get(DicomTag.PatientID);

file.Dataset.Add(DicomTag.PatientsName, "DOE^JOHN");

能不能把Patient Identification Module弄成对象,即将上面的示例改成下列形式呢?

var file = await DicomFile.OpenAsync(@"test.dcm"); // Alt 2
var dicomfileobj = file.Dataset;
var patientid = dicomfileobj.Patient.ID;
dicomfileobj.Patient.Name = "DOE^JOHN";

答案当然是能。

按照DICOM规范把所有Macro/Module定义成对象,DICOM Atributes就定义成C#对象的Properties,getter和setter内封装fo-dicom的Add等方法。用反射自动解码和编码DICOM即可。

具体实现此处不表了。

面向windows的支持plugins的caffe

https://github.com/wadefelix/caffe/tree/pwin

prototxt文件整合是在CMake脚本里完成,所以修改proto文件的话要用CMake再生成。

另外protocal buffer版本换成了最新的3.5.2,预编译的protobuf对扩展的支持没搞定可能不太好搞。编译protobuf时注意修改其运行库是和caffe一致的多线程DLL

已携带了和Linux下pCaffe分支一样有的 fast_rcnnSSDyolo

另外,因为电脑所限,我只编译了纯CPU版本,所以GPU版本不确定,也没有支持CUDA的Dynamic Parallelism动态并行特性。

使用tcl语言unknown指令操作形如LDAP的dn形式对象的方法

这是很久之前的一个工作了,简单回忆下思路。

我们的某系统中以树的形式保存了复杂系统的各种配置信息,并且给每个节点分配了形如LDAP的dn那样的名字,这就是一个dn:cn=somenone,ou=Group,dc=example,dc=com,但是我们把这个顺序反过来了,这样更符合国人的思考方式吧,就成了dc=com,dc=example,ou=Group,cn=somenone。这个系统又基本以增删改查的形式提供管理接口,以及别的一些管理接口。我们就以此为基础对常用功能开发了一套更简单的管理方法,使用的语言就是tcl,充分利用了tcl提供的expect等优秀功能,特别是我直接利用了tcl的unknown。使用unknown的机制给我们的dn添加各种动作,等于实现了它的对象化。
举例:

# Group中增加一个名为newuser的人
dc=com,dc=example,ou=Group.AddUser newuser
# 实际上我们的对象都是查出来的或根据规则动态生成的
set mygroup dc=com,dc=example,ou=Group
# 那之前增加用户的指令就成了
$mygroup.AddUser newuser

set userobj $mygroup,cn=newuser
# 修改年龄
$userobj.SetAttribute age 23

这里面就用了unknown,因为形如dc=com,dc=example,ou=Group.AddUser这个命令肯定查不到啊,我们定制个unknown处理函数,检查一下命令的格式,发现它符合我们的规则,就调用我们为它写的专门的处理函数。那处理函数在哪里呢?这点我们肯定也要采取些技巧了啊。
把它写在ou.tcl里,当然对于dc=com,dc=example,ou=Group,cn=newuser我们就写在cn.tcl中。这里我们补充一下,我们的系统里并不会出现LDAP这样有两级都叫dc,树的每一层都是不同的类型。这样我们只需要检查一下dn最终那一级是什么类型就去加载相应的tcl文件进来就可以了。那这个tcl文件就有dc=com,dc=example,ou=Group.AddUser这个命令么?当然没有了,这个dn好多呢,怎么可能每个都写一个过程呢。dc=com,dc=example,ou=Group.AddUser newuser这个命令被我们的unknown处理函数翻译成了AddUser dc=com,dc=example,ou=Group newuser,哦,原来那个函数叫AddUser,就放在ou.tcl文件里,轻松多了吧。

真他娘的的是个人才!

VS2017编译caffe

caffe的windows分支支持的VS版本是2013和2015,而我安装的是Visual Studio Community 2017, 直接使用cmake是不行的。

根据报错信息,凡是不行的地方让VS2017直接使用VS2015的配置,就可以使用caffe工程提供的预编译的二进制的依赖库了。

CMake脚本的修改

cmake/WindowsDownloadPrebuiltDependencies.cmake需要增加

set(DEPENDENCIES_URL_1912_27 "${DEPENDENCIES_URL_BASE}/v${DEPENDENCIES_VERSION}/${DEPENDENCIES_NAME_1900_27}${DEPENDENCIES_FILE_EXT}")
set(DEPENDENCIES_SHA_1912_27 "17eecb095bd3b0774a87a38624a77ce35e497cd2")

1912就是VS2017的,1900是2015的。

依赖包中OpenCV的配置

预编译的OpenCV中也缺少VS2017的信息,.caffe\dependencies\libraries\OpenCVConfig.cmake中增加VS2017的信息

  elseif(MSVC_VERSION EQUAL 1900)
    set(OpenCV_RUNTIME vc14)
  elseif(MSVC_VERSION EQUAL 1912)
    set(OpenCV_RUNTIME vc14)
  endif()

BLAS选择OpenBLAS,我这苏菲也没有N记卡,不用CUDA。选择VS2017 amd64编译起,cmake搞起,成功编译出工程, VS打开sln,编译也成功。Debug配置下
C:\Users\renwe\Projects\caffe\build>.\tools\Debug\caffe-d.exe --help,Release配置下C:\Users\renwe\Projects\caffe\build>.\tools\Release\caffe.exe --help成功运行。

YEAH!对了,

python版本选择

我看了caffe仅支持2.7和3.5版本的python,那VS2016里安装的python3.6我就放弃吧,有miniconda这种神器我何必自己折腾呢。选择了安装了miniconda2版本(想着Caffe2还不支持py3)。

mnist走一波

caffe工程目录下(我的C:\Users\renwe\Projects\caffe> )开PowerShell:

.\data\mnist\get_mnist.ps1
.\examples\mnist\create_mnist.ps1
.\examples\mnist\train_lenet.ps1

注意1:PowerShell运行脚本的权限,默认可能是不能执行的,修改命令Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
注意2:VS工程配置的问题,可执行文件都在各自的Debug和Release字目录中。适当修改下ps1脚本就OK了。
最后训练完测试完

I0331 02:00:46.701665 15772 solver.cpp:310] Iteration 10000, loss = 0.00280334
I0331 02:00:46.701665 15772 solver.cpp:330] Iteration 10000, Testing net (#0)
I0331 02:00:56.045547 19868 data_layer.cpp:73] Restarting data prefetching from start.
I0331 02:00:56.420552 15772 solver.cpp:397]     Test net output #0: accuracy = 0.9901
I0331 02:00:56.420552 15772 solver.cpp:397]     Test net output #1: loss = 0.0309957 (* 1 = 0.0309957 loss)
I0331 02:00:56.420552 15772 solver.cpp:315] Optimization Done.
I0331 02:00:56.420552 15772 caffe.cpp:260] Optimization Done.

WordPress搜索自定义文章类型post_type等

post_type直接使用 Custom Post Type Maker 创建和管理,可以控制是否对默认全站搜索可见。

如何限定只在自定义post_type中进行搜索呢?

WordPress搜索是支持post_type参数的,我们可以通过定制theme里的searchform.php定制下搜索框给加上这个post_type参数即可,可以通过下拉菜单或者radio标签等来控制这个参数。

比如下面这样

<?php $post_type_q = get_query_var ( 'post_type' ); ?>
<input type="radio" name="post_type" value="any" <?php echo $post_type_q=='any'?'checked':'';?> >全站
<input type="radio" name="post_type" value="post" <?php echo $post_type_q=='post'?'checked':'';?> >文章
<input type="radio" name="post_type" value="product" <?php echo $post_type_q=='product'?'checked':'';?> >产品

如何合并全文搜索和某个自定义post_meta搜索呢?

用下面三个filter控制吧,仅在搜索product时搜索这个barcode这个meta信息。

function add_join_include_barcode($joins) {
 if ( !is_admin()) {
  global $wpdb;
  if ( isset( $_GET['post_type'] ) && !empty( $_GET['post_type'] ) && $_GET['post_type'] == 'product' ) {
   $joins = $joins . " INNER JOIN {$wpdb->postmeta} ON ({$wpdb->posts}.ID = {$wpdb->postmeta}.post_id)" ;
  }
 }
 return $joins;
}
function add_where_include_barcode($where) {
 if ( !is_admin()) {
  global $wpdb;
  if ( isset( $_GET['post_type'] ) && !empty( $_GET['post_type'] ) && $_GET['post_type'] == 'product' ) {
   $where.= " OR ({$wpdb->postmeta}.meta_key='barcode' AND {$wpdb->postmeta}.meta_value like '%".esc_sql( $_GET['s'])."%')";
  }
 }
 return $where;
}
add_filter('posts_join','add_join_include_barcode');
add_filter('posts_where','add_where_include_barcode');
function search_distinct() {
 return "DISTINCT";
}
add_filter('posts_distinct', 'search_distinct');

新建文章自动添加meta信息字段

add_action( 'add_meta_boxes_product', 'add_product_meta_data' );
function add_product_meta_data( WP_Post $cf ) {
    // Add the meta data you want the custom post type to have
    $cf_meta_data = [
        'barcode',
    ];

    foreach ( $cf_meta_data as $meta ) {
        add_post_meta( $cf->ID, $meta, '', true );
    }
}

用单选框复选框形式修改某taxonomy

先通过注册add_meta_boxes这个action创建输入框,还需要注册save_post这个action来保存此meta信息。


function product_seller_meta_box_markup($object)
{
    wp_nonce_field(basename(__FILE__), "meta-box-nonce");
    ?>
<div>
<form>
    <?php 
    $sellerterms = get_the_terms( $post->ID, 'seller' );
    $seller = null;
    if ( ! empty( $sellerterms ) && ! is_wp_error( $sellerterms ) ) {
        $seller = $sellerterms[0]->name;
    }
    $allsellers = array('狗家','猫家','狮家');
    for($i = 0; $i < count($allprices); ++$i) {
        echo '<input type="radio" name="product_seller" value="'.$allsellers [$i].'"'.($seller==$allsellers[$i]?' checked ':'').'>'.$allsellers[$i].'<br>';
    }
    ?>
    </form>
</div>
<?php
}
function add_meta_box_seller() {
  add_meta_box("seller-meta-box", "卖家", "product_seller_meta_box_markup", "product", "side", "low", null);
}
add_action("add_meta_boxes", "add_meta_box_seller"); 

function save_product_seller($post_id, $post, $update) {
  if (!isset($_POST["meta-box-nonce"]) || !wp_verify_nonce($_POST["meta-box-nonce"], basename(__FILE__))) return $post_id;
  if(!current_user_can("edit_post", $post_id)) return $post_id;
  if(defined("DOING_AUTOSAVE") && DOING_AUTOSAVE) return $post_id;

  if('product' != $post->post_type)
    return $post_id;

  $seller = null;

  if(isset($_POST["product_seller"]))
  {
    $seller = $_POST["product_seller"];
  }
  if ($seller!==null) wp_set_object_terms( $post_id, $seller , 'seller' );
}

add_action("save_post", "save_product_seller", 10, 3);