xulihang's blog 2019-12-08T09:26:24+00:00 xulihanghai@163.com 历史长河里 2019-12-08T08:46:50+00:00 xulihang blog.xulihang.me/as-part-of-the-history 《影响世界未来最重要的三件事》一文的开头,作者列举了为了搞懂一件事情需要弄懂的一连串事情,比如二战后的恐共思想-越南战争-30年利率下降-抵押市场-金融危机这样一条链路。

微博上看到了Fox Movietone News录制的上世纪20年代末上海一所小学的教学视频,发现那个时候的小学教育形式和我小时候接受的基本上是差不多的,那目前的教育经历了怎样的演变,又有哪些问题?

写论文需要做文献综述,我把过往所有相关的文献都收集起来查阅,文献与文献互相关联,似乎总是不能穷尽。

现代汉语的词汇,很多的翻译都来自日语。比如英文的evolution,严复翻译为天演,日语翻译为进化,后者为现代汉语采用。英文的marketing,日语按照发音用片假名表示为マーケティング,而台湾翻译为行销,大陆翻译为营销。实际上marketing的汉语翻译并没有很好地把原有词汇的意义表现出来,但因为前人已经有了翻译,后人大多直接沿用。

阅读《人类简史》,可以知道,人类一开始的生活是很简单的,在森林里过着比较自由的采集生活,而后来有了农业,人类定居下来,也开始有了文明的传承。

我们站在历史的潮头,是历史的创造者,同时也是历史的传承者。不过历史的细节是如此之多,我们或许只需知道重要的历史节点以及对我们当下做的事情具有重要影响的部分历史。

]]>
故事 2019-11-30T03:22:50+00:00 xulihang blog.xulihang.me/stories 读《乔布斯传》时,一段关于乔布斯2005年斯坦福演讲的内容让我印象深刻:

Alex Haley once said that the best way to begin a speech is “Let me tell you a story.” Nobody is eager for a lecture, but everybody loves a story. And that was the approach Jobs chose.

人们都喜欢听故事,但并不见得喜欢听讲座。大二时孙志祥老师讲翻译概论,因为理论比较深奥,很多同学听不懂。孙老师也知道这点,于是在上课时经常给大家讲故事,比如他读书时的故事,他华为同事的故事。

人从小就听各种故事,从寓言故事、历史故事到各种爱情故事、成功故事。电视剧、小说吸引人的也都是故事。

我希望从别人的故事中获得一些关于自己的人生道路的启发。不过,做到对名人故事如数家珍并不一定自己也能做到,自己的道路还是要自己去走。

]]>
甲方乙方 2019-11-28T04:59:50+00:00 xulihang blog.xulihang.me/party-a-and-party-b 甲方是服务的需求方,乙方是服务的提供方。一家公司可能既是甲方,也是乙方,比如翻译公司给客户提供翻译服务,是乙方,但它也需要找自由译员或者供应商提供具体一个语种的翻译,这时它是甲方。

乙方公司的负责人可能过去是甲方公司某个部门的负责人,他精通某项业务后,将该业务独立出来,为多家公司提供服务。

比如以下几种服务:

  • 翻译
  • 公关、营销、广告
  • 软件开发
  • 设计
  • 财务、会计

有需要行业知识、管理知识的公司,比如各种咨询公司(MBB)、专家联络公司(GLG、Alphasights)。

传统的软件企业靠一次性出售软件获利,随着SaaS的流行,卖软件变成了卖服务,也可以视为乙方。

]]>
外语专业就业探究 2019-11-24T08:55:50+00:00 xulihang blog.xulihang.me/exploration-of-careers-for-foreign-language-majors 本文主要针对外语专业的就业问题做一个探究。

找工作其实是很个人的一个事情,我们用“冰山模型”来分析,可以知道工作和处于冰山表面的知识、技能以及更深层的价值观、性格特质和动机有关。前者主要决定我们能做什么,后者主要决定我们更适合做什么。外语专业具有较高的语言能力、跨文化沟通能力和思考能力,可以从事与人和文字相关的工作,比如翻译、教师、编辑、营销、人力资源、公共关系等等。根据深层次的因素,我们选择合适的岗位。如果一个人正直不阿,那他不会喜欢需要夸大宣传的工作。如果他喜欢有成就感,那工作一般需要能看到明显的成果。

工作也要顺应时代的发展,比如跨境电商在近几年是一个风口,很多学校也成立了商务英语专业。不过国内很多大学还是走的偏理论的培养方案,除了基本的听说读写,主要就是翻译、文学和语言学课程,这对于提高学生的综合素质有用,学习的过程也比较快乐,但面对就业问题时,在由学生转换到社会人的过程中,学生往往会比较迷茫。

这时一种好的方法是看看毕业的学长学姐选择了什么样的工作。可以看就业中心的报告、找学院的学工处老师,或者看相关求职网站。这里介绍下怎么用领英了解本专业的就业。

访问学校的主页,点alumni查看校友,然后检索专业的关键词。

可以查看校友在哪里工作,从事什么行业,访问校友的页面可以知道他的职业发展历程。外语专业学生刚毕业主要还是从事前文列出的几个职业,但随后往往能从事不同的岗位。这就涉及一个职业的发展问题。这时我们可以看一些社会招聘广告来了解更资深的岗位有什么要求。

这里再讲下主要几种专业硕士研究生的就业。决定读专业硕士一般需要对就业有较清晰的认识。

翻译硕士可以去甲方或者乙方做应用型翻译,甲方一般要更高的综合能力,能够同时完成口译笔译工作,而乙方工作会接触各种各样的客户和内容,因为译员较多,分工相对明确。不过也有很多专门化的公司,只做游戏、漫画之类的本地化。也有翻译硕士选择做项目经理或者本地化工程师,更多的考验管理能力和技术工具的运用能力,平时会做一些审校工作。而做文学翻译、图书翻译一般会在出版公司做编辑。很多翻硕学生具有其它专业背景,可以从事医学翻译、机械翻译等专业领域翻译。另外还有自由职业,但除了要求翻译能力,还对客户管理、营销、记账等能力有要求。

学科教学、对外汉语硕士则主要从事教学工作,方向较为明确。

北大的计算机辅助翻译专业目前主要以技术写作和本地化人才为培养目标。技术文档写作有面向普通用户和面向开发者这两种。前者更加注重设计能力,需要掌握视频、网页等多媒体展示技术,也叫资料开发工程师。后者更加注重技术能力,对技术的深入了解。

硕士阶段发展的专业技能和文献综述等研究能力可以迁移到其它岗位,如果发展了其它突出的知识技能,比如编程,转行也是可能的。

具体的求职方法这里就不具体讲了,更多见相关链接。

相关链接:

]]>
BasicCAT开发笔记(十五):全文检索 2019-11-12T02:59:50+00:00 xulihang blog.xulihang.me/basiccat-developing-notes-15-full-text-search 在BasicCAT中,全文检索主要有两个用途:1. 在模糊匹配过程中先筛选出包含部分关键词的句子,减少需要计算编辑距离的句子数量,提高匹配响应速度。2. 加强翻译记忆管理器的检索功能。

此前写的相关文章:

这里主要基于B4X的KeyValueStore改造出一个专门的TMDB类,在数据库中增加启用SQLite FTS全文检索的表,并提供翻译记忆检索的相关操作。

下面是具体添加的内容:

  1. 初始化时添加一个FTS的Virtual Table,包含三列内容,原文,分词后的原文和译文

     Private Sub CreateTable
         sql1.ExecNonQuery("CREATE TABLE IF NOT EXISTS main(key TEXT PRIMARY KEY, value NONE)")
         sql1.ExecNonQuery("CREATE VIRTUAL TABLE IF NOT EXISTS idx USING fts4(key, source, target, notindexed=key)")
     End Sub
    
  2. 打开旧的数据库时会使用原有数据建立新的索引表

     Sub Initialize 
        '...
    	   
         If checkIsFTSEnabled=False Then
             CreateTable
             createIdx
         Else
             CreateTable
         End If
     End Sub
    
     Sub checkIsFTSEnabled As Boolean
         Try
             sql1.ExecQuery("SELECT * FROM idx")
             Return True
         Catch
             Log(LastException)
             Return False
         End Try
     End Sub
    
  3. 预先分词

    因为sqlite-jdbc的Sqlite不支持中文分词,只能事先进行分词。这里把汉语分为每个单字,并不包含多个字组成的词。

    根据语言代码分词:

     Sub getStringForIndex(source As String,lang As String) As String
         Dim sb As StringBuilder
         sb.Initialize
         Dim words As List=LanguageUtils.TokenizedList(source,lang)
         For index =0 To words.Size-1
             sb.Append(words.Get(index)).Append(" ")
         Next
         Return sb.ToString.Trim
     End Sub
    

    添加条目:

     Public Sub Put(source As String, targetMap As Map)
         Dim ser As B4XSerializator
         Dim bytes() As Byte=ser.ConvertObjectToBytes(targetMap)
         sql1.ExecNonQuery2("INSERT OR REPLACE INTO main VALUES(?, ?)", Array (source,bytes))
         sql1.ExecNonQuery2("INSERT OR REPLACE INTO idx VALUES(?, ?, ?)", Array (source,getStringForIndex(source,sourceLang),getStringForIndex(targetMap.Get("text"),targetLang)))
     End Sub
    
  4. 检索

    模糊匹配时,因为是以原文句子为检索内容,寻找包含原文词语多的句子,需要将原来的句子拆分为词,并用OR操作符连接。

    而检索翻译记忆时,则是根据用户输入的词汇进行检索,需要AND操作符连接。

    生成检索表达式:

     Sub getQuery(words As List,operator As String) As String
         Dim sb As StringBuilder
         sb.Initialize
         For index =0 To words.Size-1
             Dim word As String=words.Get(index)
             If word.Trim<>"" Then
                 sb.Append(word)
                 If index<>words.Size-1 Then
                     sb.Append(" "&operator&" ") ' AND OR NOT
                 End If
             End If
         Next
         Return sb.ToString
     End Sub
    

    获得检索结果,检索翻译记忆时matchAll是True,同时检索原文和译文,模糊匹配时matchAll是False,只检索原文:

     Public Sub GetMatchedMapAsync(text As String,isSource As Boolean,matchAll As Boolean) As ResumableSub
         Dim sqlStr As String
         Dim matchTarget As String
         Dim operator As String
         Dim lang As String
         Dim words As List
         words.Initialize
         If isSource Then
             lang=sourceLang
             matchTarget="source"
         Else
             lang=targetLang
             matchTarget="target"
         End If
         If matchAll Then
             matchTarget="idx"
             operator="AND"
             If text.StartsWith($"""$) And text.EndsWith($"""$) Then
                 words.Add(text)
             Else
                 words=getWordsForAll(text)
             End If
         Else
             operator="OR"
             words=LanguageUtils.TokenizedList(text,lang)
         End If
         text=getQuery(words,operator)
    		
         sqlStr="SELECT key, rowid, quote(matchinfo(idx)) as rank FROM idx WHERE "&matchTarget&" MATCH '"&text&"' ORDER BY rank DESC LIMIT 1000 OFFSET 0"
         Log(sqlStr)
         Dim SenderFilter As Object = sql1.ExecQueryAsync("SQL", sqlStr, Null)
         Wait For (SenderFilter) SQL_QueryComplete (Success As Boolean, rs As ResultSet)
         Dim resultMap As Map
         resultMap.Initialize
         Dim result As Object = Null
         If Success Then
             Do While rs.NextRow
                 result=GetDefault(rs.GetString2(0),Null)
                 If result<>Null Then
                     resultMap.Put(rs.GetString2(0),result)
                 Else
                     Log("not exist")
                     DeleteIdxRow(rs.GetInt2(1))
                 End If
             Loop
             rs.Close
         Else
             Log(LastException)
         End If
         Return resultMap
     End Sub
    
  5. 多个词语匹配结果的高亮展示

    <--->包裹匹配的词语,用正则得到匹配词和其它部分相分割的列表,可用于在Searchview中高亮显示。

     Sub splitByStrs(strs() As String,text As String) As List
         For Each str As String In strs
             Dim matcher As Matcher
             matcher=Regex.Matcher(str.ToLowerCase,text.ToLowerCase)
             Dim offset As Int=0
             Do While matcher.Find
                 Dim startIndex,endIndex As Int
                 startIndex=matcher.GetStart(0)+offset
                 endIndex=matcher.GetEnd(0)+offset
                 text=text.SubString2(0,endIndex)&"<--->"&text.SubString2(endIndex,text.Length)
                 text=text.SubString2(0,startIndex)&"<--->"&text.SubString2(startIndex,text.Length)
                 offset=offset+"<--->".Length*2
             Loop
         Next
         Dim result As List
         result.Initialize
         For Each str As String In Regex.Split("<--->",text)
             result.Add(str)
         Next
         Return result
     End Sub
    
  6. 中英混合检索

    有的时候检索内容同时包含中文和英文,这时需要对中文和英文进行区别处理。

    比如对于这么一条检索内容: “computer 电脑”,需要拆分为computer、电、脑这三个检索词。

    我们先用空格进行拆分,得到computer和电脑,之后根据汉字是多字节字符的特点,去除汉字结果。

     Sub removeMultiBytesWords(words As List)
         Dim newList As List
         newList.Initialize
         For Each word As String In words
             If word.Length>1 Then
                 If getBytesLength(word.CharAt(0))>1 Then
                     Continue
                 End If
             End If
             newList.Add(word)
         Next
         words.Clear
         words.AddAll(newList)
     End Sub
    

    然后再把检索内容拆分为单字,并去掉单个英文字母的结果。

     Sub removeCharacters(source As List)
         Dim newList As List
         newList.Initialize
         For Each text As String In source
             If text.Length=1 Then
                 If Regex.IsMatch("[a-z]",text.ToLowerCase)=True Then
                     Continue
                 End If
             End If
             newList.Add(text)
         Next
         source.Clear
         source.AddAll(newList)
     End Sub
    
]]>
SQLite全文检索 2019-11-11T01:34:50+00:00 xulihang blog.xulihang.me/sqlite-full-text-search 现在主流的数据库都提供了全文检索功能,比如PostgreSQL、Sql Server等。

SQLite也有全文检索模块FTS,最新版本是FTS5,不过本文主要讨论FTS3/4。FTS3于2007年加入SQLite,FTS4是2010年加入的对FTS3的加强。

全文检索因为使用了索引,可以大大提高数据检索速度。下面是官方文档中给出的示例:

CREATE VIRTUAL TABLE enrondata1 USING fts3(content TEXT);     /* FTS3 table */
CREATE TABLE enrondata2(content TEXT);                        /* Ordinary table */
SELECT count(*) FROM enrondata1 WHERE content MATCH 'linux';  /* 0.03 seconds */
SELECT count(*) FROM enrondata2 WHERE content LIKE '%linux%'; /* 22.5 seconds */

操作

建立索引表

建立一个使用fts4进行索引的虚拟表,包含key、source和target三列内容,key列不进行索引。

CREATE VIRTUAL TABLE IF NOT EXISTS idx USING fts4(key, source, target, notindexed=key)

从文本中抽取词语需要使用分词器(Tokenizer),默认使用simple分词方法,其它还有porter、unicode61和icu,需要在建表的时候指定:

CREATE VIRTUAL TABLE porter USING fts3(tokenize=porter);

simple方法会将文本全部小写,并利用标点和空格进行分词。比如”Right now, they’re very frustrated.”的分词结果是”right now they re very frustrated”。

而porter分词是一种去除词尾获得词干的方法,上面的句子使用porter分词的结果是”right now thei veri frustrat”,这样可以使用英语词的不同屈折变化进行检索,比如frustrated和frustration使用porter处理后的结果都是frustrat,都可以检索到该条内容。

对于中文、日语和藏语这样词汇间没有空格的语言,可以使用icu分词,但一般的SQLite编译版本都没有包含这一分词器。

增删与更新

INSERT INTO idx VALUES('I am Tony!', 'I am Tony', '我 是 托 尼')) '插入
DELETE FROM idx '删除整张表的内容
DROP TABLE data '删除整张表
UPDATE idx SET key = 'Download SQLite' WHERE rowid = 54 '根据rowid进行更新

查询

使用FTS的表可以使用MATCH进行全文检索,并且有rowid这一主键。下面是对各种查询语句的速度的一个比较:

-- The examples in this block assume the following FTS table:
CREATE VIRTUAL TABLE mail USING fts3(subject, body);

SELECT * FROM mail WHERE rowid = 15;                -- Fast. Rowid lookup.
SELECT * FROM mail WHERE body MATCH 'sqlite';       -- Fast. Full-text query.
SELECT * FROM mail WHERE mail MATCH 'search';       -- Fast. Full-text query.
SELECT * FROM mail WHERE rowid BETWEEN 15 AND 20;   -- Fast. Rowid lookup.
SELECT * FROM mail WHERE subject = 'database';      -- Slow. Linear scan.
SELECT * FROM mail WHERE subject MATCH 'database';  -- Fast. Full-text query.

WHERE和MATCH之间可以是表的名字,也可以是列的名字。如果要获得列号,可以使用rowid这一隐藏的列的名字。

SELECT key, rowid FROM idx WHERE source MATCH 'text'

可以用通配符修饰查询的文本,比如lin*匹配以lin开头的词,^lin*表示第一个词的开头是lin的内容。

可以使用双引号进行短语查询,用NEAR/间隔字数限制两个词之间相隔的词数:

SELECT * FROM docs WHERE docs MATCH 'database NEAR/2 "ACID compliant"';

可以使用三个操作符:AND、OR、NOT

SELECT docid FROM docs WHERE docs MATCH '("sqlite database" OR "sqlite library") AND linux';

有三个辅助函数:Snippet, Offsets and Matchinfo。

offsets函数可以显示匹配到的词在第几列,在文本中的偏移量。snippet可以高亮匹配到的词。matchinfo提供详细的匹配信息,比如有多少词被匹配。具体用法见文档。

组合示例

根据匹配度排序,取前1000个条目:

SELECT key, rowid, quote(matchinfo(idx)) as rank FROM idx WHERE source MATCH 'text' ORDER BY rank DESC LIMIT 1000 OFFSET 0

和普通模式的比较

普通的表可以设置主键,相同主键的内容不能添加两次。而FTS模式可以重复添加内容。

建立索引过程耗费内存,而且数据会占用更多空间。

关于中文索引

在没有icu分词的情况下对中文索引的一个折中办法就是事先对中文进行分词,然后导入数据库。可以建两列内容,一列是中文分词结果,一列是中文原文,只对中文分词结果进行索引。另外,检索文本也需要进行分词。

参考文献:

]]>
垂直投影与水平投影 2019-11-06T14:52:50+00:00 xulihang blog.xulihang.me/horizontal-and-vertical-projection 本文讲的是垂直投影和水平投影在图像处理中的应用。

垂直投影指将所有像素沿垂直方向投射到一个点(进行平均值、求和等操作),水平投影则是沿水平方向。

比如下图显示了一张做了二值化处理的图片的水平投影和垂直投影的直方图(这里清除了两边为零的数据)。

我们可以用OpenCV的reduce来实现垂直投影和水平投影。

C++和Java中的reduce操作需要5个参数,依次为原图矩阵、输出矩阵、维数、操作类型rtype、数据类型dtype。

Dim h,v as mat
h.initialize
v.initialize
cv2.reduce(thresh,h,1,0,cv2.cvType("CV_32S")) '水平投影
cv2.reduce(thresh,v,0,0,cv2.cvType("CV_32S")) '垂直投影

维数的取值:

  • 1,投射到一列
  • 0,投射到一行
  • -1,根据输出矩阵的大小自动选择

操作类型:

  • CV_REDUCE_SUM,求和
  • CV_REDUCE_AVG,求均值
  • CV_REDUCE_MAX,求最大值
  • CV_REDUCE_MIN,求最小值

数据类型需要根据最终得到的值进行设置,像求和的话就不能取太小,否则会报错。

接下来讲一个具体的例子,用于检测两个文字区域间是不是被黑线分隔。

比如下面两张图,分别是左右和上下方向存在分隔:

我们先对图像做二值化处理,并且反转颜色。之后做轮廓检测,对每个轮廓做reduce求和操作。如果轮廓是起到分隔作用的,那它是连接两端的连续的一段,坐标上每个点的值都要大于0。

下面是Python实现代码:

import cv2
import numpy as np

def infer(direction):
    index=0
    if direction==1: #horizontal
        end=height
    else: # vertical
        end=width
        
    for cnt in contours:
        mask = np.zeros_like(img)
        cv2.drawContours(mask, [cnt], -1, (255,255,255), 1)
        sum = cv2.reduce(mask, direction, cv2.REDUCE_SUM, dtype=cv2.CV_32S)
        blocked=True
        for j in range(end):
            if direction==1:
                value=sum[j][0]
            else:
                value=sum[0,j]
            print("j:"+str(j))
            print("value:"+str(value))
            if value==0:
                blocked=False
                break

        if blocked==True:
            cv2.imwrite(str(index)+'MyPic.jpg', mask)
            cv2.imwrite('y.jpg', sum)
            print(sum)
            print("blocked")
            return True
        index=index+1
    return False
        
img = cv2.imread('balloon.png', cv2.IMREAD_GRAYSCALE)
height, width = img.shape[:2]
ret, thresh = cv2.threshold(img,127,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

infer(0)

第一张图检测到的分割轮廓:

reduce后的结果:

[[510]
 [510]
 [510]
 [510]
 [510]
 [510]
 [510]
 [510]
 [510]
 [255]
 [510]
 [510]
 [510]
 [510]
 [255]
 [510]
 [510]
 [510]
 [510]
 [510]
 [510]
 [510]]

第二张图检测到的分割轮廓:

reduce后的结果:

[[10710  3315  3060   765   510  1020   510   510   765   510   510   510
    510   510   510   510   510   510   510   510   510   510   510   510
    510   510   510   510   510   510   510   510   510   510   510   510
    510   510   510   510   510   510   510   510   510   510   510   510
    510   510   510   510   510   510   510   510   510   510   510   510
    510   510   510   510   510   510   510   510   510   510   510   510
    510   510   510   510   510   510   510   510   510   510   510   510
    510   510   510   510   510   510   510   510   510   510   510   510
    510   510   510   510   510   510   510  1020  8670]]

参考资料:

]]>
B4J类库封装之JavaObject 2019-11-03T14:44:50+00:00 xulihang blog.xulihang.me/wrapping-for-B4J-with-javaobject 所谓类库封装就是将一个类库的功能进行封装,从而改变它原来的功能或是供另一种语言调用。本文主要讲的是后者。这一操作也有叫做绑定(bindings),封装出来的类库叫做wrapper。常见的例子有Java的JNI和Python的ctypes。

B4J的wrapper是用于在B4J中调用jar类库,因为B4J生成的语言是Java,所以还是比较容易处理的。

封装类库有两种方法,一种是直接使用Java编写,一种是使用JavaObject。JavaObject直接使用B4J编写,相对简单。官网教程:Accesing third party Jar with #Additionaljar and JavaObject - Picasso

JavaObject主要提供以下几种方法:

  • GetField,获得Field(变量)。
  • SetField,设置Field(变量)。
  • InitializeStatic,初始化静态类
  • InitializeNewInstance,新建一个类的实例
  • RunMethod,运行一个方法
  • CreateEvent,创建事件

后缀有JO的方法,返回的是JavaObject类,用于需要继续将返回对象当做JavaObject使用的情况。

下面以OpenCV的封装为例。

opencv是C++编写的计算机图形库,其提供的java类库就是一种绑定。有两个流行的java类库,一个是javacv,一个是官方的类库。后者是基于c++文件自动生成的1,和c++的API 接口非常接近。这里我使用官方的类库。

我们的目标是实现以下Java代码2的接口在B4J中的绑定:

import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;
import org.opencv.imgcodecs.Imgcodecs;

public class TestOpencvDemo {   
    @Test    
    public void TestMatRead() {
        Mat img = Imgcodecs.imread("/Users/wuxi/Pictures/medianBlur.png");
        Imgproc.medianBlur(img, img, 7);
        Imgcodecs.imwrite("/Users/wuxi/Pictures/medianBlur1.png",img);         img.release();     
	}
}

封装好后,以上代码对应的B4J代码:

Dim cv2 As opencv
cv2.Initialize
Dim mat As cvMat
mat=cv2.imread(File.Combine(File.DirApp,"test.jpg"))
cv2.medianBlur(mat,mat,7)
cv2.imwrite(File.Combine(File.DirApp,"out.jpg"),mat)

步骤

1. 分析

查看OpenCV的文档,可以知道它有很多的模块,比如core是核心功能模块,imgproc是图像处理模块,imgcodecs是图像读写模块。

因为B4J程序只会调用一小部分功能,我希望将会用到的功能整合进一个class。

Mat类是用于存储图像数据的矩阵,因为用到得较为频繁,可以单独建立一个class。

2. 编写

B4J中新建两个class模块,命名为opencv和cvMat,具体说明看代码和注释。

参数的命名和类型可以参照OpenCV的Java API文档

cvMat代码如下:

Sub Class_Globals
	Private matJO As JavaObject
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize(params() As Object)
	'Java 支持重载,一个方法可以有多种参数组合。
	'Mat的构建方法有多种,比如以下几个。
	'Mat()
	'Mat​(int[] sizes, int type)
	'Mat​(int rows, int cols, int type)
	'但B4J不支持,可以直接设置参数为一个array。
	matJO.InitializeNewInstance("org.opencv.core.Mat",params)
End Sub

'在供其他opencv方法调用时,需要提供Mat的JavaObject。
Public Sub getJO As JavaObject
	Return matJO
End Sub

Public Sub setJO(mat As JavaObject)
	matJO=mat
End Sub

opencv代码如下:

Sub Class_Globals
	Private Imgproc As JavaObject
	Private Imgcodecs As JavaObject
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize
	'初始化两个类,用于调用静态方法
	Imgproc.InitializeStatic("org.opencv.imgproc.Imgproc") 
	Imgcodecs.InitializeStatic("org.opencv.imgcodecs.Imgcodecs")
End Sub

Public Sub imread(path As String) As cvMat
	'使用RunMethod调用Java的方法
	Return matJO2mat(Imgcodecs.RunMethodJO("imread",Array(path)))
End Sub

Public Sub imwrite(path As String,img As cvMat)
	Imgcodecs.RunMethod("imwrite",Array(path,img.JO))
End Sub

Public Sub medianBlur(src As cvMat,dst As cvMat, ksize As Int)
	Imgproc.RunMethodJO("medianBlur",Array(src.JO,dst.JO,ksize))
End Sub

'将得到的mat javaobject封装为B4J中的cvMat类
Sub matJO2mat(jo As JavaObject) As cvMat
	Dim mat As cvMat
	mat.Initialize(Null)
	mat.JO=jo
	Return mat
End Sub

可以发现,wrapper是用于调用原有方法的很薄的一层内容。

3. 调用

Dim cv2 As opencv
cv2.Initialize
Dim mat As cvMat
mat=cv2.imread(File.Combine(File.DirApp,"test.jpg"))
cv2.medianBlur(mat,mat,7)
cv2.imwrite(File.Combine(File.DirApp,"out.jpg"),mat)

以上代码会将图片做模糊处理并另存。

查看medianBlur的Java API文档:

public static void medianBlur​(Mat src, Mat dst, int ksize)
Blurs an image using the median filter. The function smoothes an image using the median filter with the ksize×ksize aperture.
Parameters:
src - input 1-, 3-, or 4-channel image; when ksize is 3 or 5, the image depth should be CV_8U, CV_16U, or CV_32F, for larger aperture sizes, it can only be CV_8U.
dst - destination array of the same size and type as src.
ksize - aperture linear size; it must be odd and greater than 1, for example: 3, 5, 7 ... SEE: bilateralFilter, blur, boxFilter, GaussianBlur

可以知道这里的参数调用使用的是传参调用,传进去的目标mat会被修改,所以不用返回处理结果。

如果不想原来的mat被修改,就再建一个cvMat:

Dim cv2 As opencv
cv2.Initialize
Dim mat As cvMat
mat=cv2.imread(File.Combine(File.DirApp,"test.jpg"))
Dim blur As cvMat
blur.initialize(Null)
cv2.medianBlur(mat,blur,7)
cv2.imwrite(File.Combine(File.DirApp,"origin.jpg"),mat)
cv2.imwrite(File.Combine(File.DirApp,"out.jpg"),blur)

4. 打包

将项目编译为一个library类库或者打包为最新的b4xlib,放到库文件夹供使用。

还有很多JavaObject的方法没有使用,这里就不细讲了。

完整代码见此:https://github.com/xulihang/OpenCV-B4J

OpenCV的安装

这里再附带OpenCV的安装方法:

  1. 下载OpenCV的压缩包(地址),提取其中的java文件夹的内容,将jar放到B4J的类库目录,将dll或者so、dylib文件放到程序的目录。
  2. 使用以下代码加载OpenCV的动态库:
Sub load
	Dim System As JavaObject
	System.InitializeStatic("java.lang.System")
	System.RunMethod("load",Array(File.Combine(File.DirApp,"opencv_java411.dll")))
End Sub

参考文献:

]]>
K-Means聚类及在漫画中的应用 2019-11-01T12:51:50+00:00 xulihang blog.xulihang.me/kmeans-clustering 首先讲一下机器学习中分类和聚类的区别。分类是有监督学习,比如使用做好标记的数据训练贝叶斯分类器判断邮件是不是垃圾邮件。而聚类是无监督学习,从一堆没有标记的数据中发现分类。

K-Means聚类需要用户指定K个分类,取随机K个种子点(质心),跟种子点近的数据可以归为一类。之后重新确定质心,看和原来的质心差别是不是不大,不大则大致可以完成聚类。

下面是一个应用示例,用于漫画分镜和文字的检测。

漫画由一张张分镜构成,分镜中包含漫画的主要内容。我们可以用连通区域标记方法对这些区域进行标记。

这些区域可以按高度进行划分,依次为整张图片、分镜、分镜中的内容。

我们使用Python-opencv获取连通区域的矩形框,然后用Python的sklearn机器学习库来使用KMeans聚类:

import cv2
import numpy as np

img = cv2.imread('3.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
_, labels, stats, centroids = cv2.connectedComponentsWithStats(thresh)

X=[]

for stat in stats:
    h=stat[cv2.CC_STAT_HEIGHT]
    one=[] #单个数据点,可以添加多个数据
    one.append(h)
    X.append(one)
    
kmeans_model = KMeans(n_clusters=3) #设置分类为3
y_pred= kmeans_model.fit_predict(X)
print(y_pred) #输出分类结果

index=0

#不同分类设置不同颜色

color1=(255,0,0)
color2=(0,255,0)
color3=(0,0,255)

cls1=[]
cls2=[]
cls3=[]

for cls in y_pred:
    if cls==0:
        color=color1
        cls1.append(stats[index])
    elif cls==1:
        color=color2
        cls2.append(stats[index])
    else:
        color=color3
        cls3.append(stats[index])
		
    stat=stats[index]
    x=stat[cv2.CC_STAT_LEFT]
    y=stat[cv2.CC_STAT_TOP]
    w=stat[cv2.CC_STAT_WIDTH]
    h=stat[cv2.CC_STAT_HEIGHT]
    cv2.rectangle(img, (x, y), (x + w, y + h), color, 2)
    index=index+1
	
cv2.imwrite('labeled.png', img)
cv2.namedWindow("labeled.png",cv2.WINDOW_NORMAL)    
cv2.imshow('labeled.png', img)
cv2.waitKey()

最后输出结果如下:

]]>
Azure定制计算机视觉模型 2019-10-31T08:58:50+00:00 xulihang blog.xulihang.me/azure-custom-vision 微软Azure提供定制计算机视觉模型的功能,叫做Custom Vision(文档)。

目前主要提供两个功能,图像分类和目标检测。

在Azure里建立一个计算机视觉的资源就可以使用了,有免费的价格档位。

下面讲下定制的步骤:

  1. 访问https://www.customvision.ai/,建立项目
  2. 添加标签,上传图片并标注
  3. 训练,可以设定训练时间,一般快速训练只要几分钟即可完成
  4. 测试并优化

微软提供在线版标注工具,并且可以使用训练好的模型辅助标注。不过我的电脑屏幕太小了,标注界面看起来比较费劲,还是labelme之类的工具方便。

之前从百度easydl导出了yolo格式的标注数据,我基于微软提供的python sdk写了一个工具,可以批量上传图片和标注区域到azure上:https://github.com/xulihang/azure-custom-vision-toolkit

训练完成后可以查看预测结果,图中做的是对漫画气泡的检测:

模型优化好后可以发布,并通过提供的预测API进行调用。

]]>