TCAX 字幕特效制作工具官方论坛 | ASS | TCAS | Python | Aegisub | Lua

 找回密码
 新人加入
查看: 7094|回复: 5
打印 上一主题 下一主题

[特效算法] 拆分字形 [附工程] [复制链接]

Moderator

Effect Researcher.

Rank: 5Rank: 5

跳转到指定楼层
楼主
发表于 2015-9-10 21:48:09 |只看该作者 |倒序浏览
本帖最后由 面麻 于 2015-9-10 22:48 编辑

当初我在一个MAD作品里看到的效果,就是拆分字形,想用ASS来实现一下。
一共2个方法,都是灾厄前辈想的,代码也是他写的。
我在其中也学到了很多,很感谢灾厄。

可能代码里面有一些东西涉及到高层次的Python语法,尽量看吧。
注释也很多,如果还有问题,直接跟帖。
完整工程在下面,可以Prase一下看看。

两种方法可能还有点小bug,各有优劣。

test_split_font_temp.zip (17.56 KB, 下载次数: 2851)

第一种方法,是拆分矢量字体的绘图代码,先通过 'm' 字符判断大致拆分开,然后再考虑内外框分别是顺时针和逆时针的情况,下面的代码是以逆时针为内框。
需要说明的是,不同字体,内外框的绕行方向是不确定的,所以如果出现大片字形不正确,可以修改函数check_clockwise()的clockwise值。
其实,这个判断是比较复杂的,尤其是凹多边形很难判断,如果出现个别字形不正确,只能手动判断和修改绘图代码。
  1. from tcaxPy import *
  2. from util.gdiFont import *

  3. import re

  4. def tcaxPy_Init():
  5.     global font   # Font和GdiFont效果差不多
  6.     global fontsize
  7.     global GdiFont
  8.     fontsize = GetVal(val_FontSize)
  9.     font = InitFont(GetVal(val_FontFileName), GetVal(val_FaceID), fontsize, GetVal(val_Spacing), GetVal(val_SpaceScale), 0xFFFFFF, 0, 0)
  10.     GdiFont = gfInitFont(GetVal(val_FontFaceName), fontsize, GetVal(val_Spacing), GetVal(val_SpaceScale), 0, False)   #GDIfont

  11. def tcaxPy_User():
  12.     pass


  13. def tcaxPy_Fin():
  14.     FinFont(font)

  15. def checkacw(str):          # 判斷內框

  16.     Ppath = ppath(str)
  17.    
  18.     if Ppath[0]==Ppath[-1]:        #字體矢量有重複最後坐標點的習慣 貌似重複的基本是外框 不過總有bug的地方 總之取首尾不同的 扔掉最後重複
  19.         Ppath.pop()
  20.     return check_clockwise(Ppath)   # 设为内框

  21. #構建一個本項和後項結合的list 比方說poly為[(1,1),(2,2),(3,3),(4,4)] 則新list為[((1,1),(2,2)),((2,2),(3,3)),((3,3),(4,4)),((4,4),(1,1))]
  22. def segments(poly):       #網上照搬
  23.     return zip(poly, poly[1:] + [poly[0]])     

  24. def check_clockwise(poly):      #判斷順時針 網上搬 算法之前判斷用的極值求叉積法 總有凹多邊形不準確 這個遍歷所有點算叉積 最後以總和的正負來判斷
  25.     clockwise = False
  26.     if (sum(x0*y1 - x1*y0 for ((x0, y0), (x1, y1)) in segments(poly))) < 0:   # 叉積为負值是逆時針
  27.         clockwise = not clockwise
  28.     return clockwise


  29. def ppath(str):   # 從str 將路徑儲存為 [(1,1),(2,2),(3,3),(4,4)] 這種格式 b 標籤捨棄中間點 做直線考慮
  30.     if str[0] != 'm':
  31.         str = 'm'+ str
  32.     sb = re.split('m|l|b ',str)     # re是一个正则表达式Module,这里的split其实就是字符串的split函数的翻版,只是可以对分隔符用正则
  33.     ppath = []
  34.     for s in sb:
  35.         b = s.split()
  36.         if len(b) > 1:
  37.             if b[-1]=='c':
  38.                 ppath.append((int(b[-3]),int(b[-2])))
  39.             else:
  40.                 ppath.append((int(b[-2]),int(b[-1])))
  41.     ppath.pop()   # 所有路徑 首尾都相同 捨棄掉最後重複
  42.     return ppath

  43. def polycmp(poly1,poly2):    #判斷多邊形是否在另一個多邊形內
  44.     result = True
  45.     if poly2[-1]==poly2[0]:
  46.         poly2.pop()
  47.     for p1 in poly1:
  48.         x1,y1= p1
  49.         if not pointinpoly(x1,y1,poly2):  #遍歷多邊形所有點是否在多邊形內
  50.             result = False
  51.     return result

  52. def pointinpoly(x,y,poly): # 判斷一個點是否在多邊形內 網上搬

  53.     n = len(poly)
  54.     inside = False

  55.     p1x,p1y = poly[0]
  56.     for i in range(n+1):
  57.         p2x,p2y = poly[i % n]
  58.         if y > min(p1y,p2y):
  59.             if y <= max(p1y,p2y):
  60.                 if x <= max(p1x,p2x):
  61.                     if p1y != p2y:
  62.                         xints = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
  63.                     if p1x == p2x or x <= xints:
  64.                         inside = not inside
  65.         p1x,p1y = p2x,p2y

  66.     return inside


  67. def tcaxPy_Main(_i, _j, _n, _start, _end, _elapk, _k, _x, _y, _a, _txt):

  68.     ASS_BUF  = []        # used for saving ASS FX lines

  69.     #############################
  70.     # TODO: write your codes here #

  71.     dx = _x - int(_a / 2 + 0.5)
  72.     dy = _y - int(fontsize / 2 + 0.5)

  73.     outline = gfGetOutline(GdiFont, _txt, dx,dy)

  74.     draw = outline.split('m')
  75.     num = len(draw)
  76.     tcaxLog(draw)


  77.     acdl = []   #用來儲存內框矢量
  78.     cdl = []    #儲存外框矢量
  79.     if _i>-1 :
  80.         for i in range(num):     #遍歷所有m標籤矢量 判斷內外框 并進行儲存
  81.             if draw[i] == '':
  82.                 continue

  83.             if checkacw(draw[i]):     
  84.                 acd = 'm'+draw[i]
  85.                 acdl.append(acd)
  86.             else:
  87.                 cd = 'm'+draw[i]
  88.                 cdl.append(cd)   
  89.    
  90.     cdlcopy = cdl[:]   #複製一遍list
  91.     for acd in acdl:   
  92.         index = -1
  93.         for i in range(len(cdlcopy)):
  94.             if polycmp(ppath(acd),ppath(cdlcopy[i])):     #遍歷所有內框外框 判斷內框是否在外框範圍內
  95.                 if index == -1:
  96.                     index = i
  97.                 else:                                    
  98.                     if polycmp(ppath(cdlcopy[i]),ppath(cdlcopy[index])):   #若存在多個可匹配的範圍 取最小的框 如"回"字
  99.                         index = i
  100.         if index != -1:
  101.             cdl[index] += acd    #將內框矢量 加在外框矢量之後
  102.         else:
  103.             cdl.append(acd)  #若沒找到匹配的外框 則作為外框 這個有可能是順時針逆時針的判斷問題 也有可能是字體設計的問題 總之是保險

  104.     for cd in cdl:   #繪製
  105.         posX = randint(-_a, _a) + randint(-10, 10)
  106.         posY = randint(-fontsize , fontsize ) + randint(-10, 10)
  107.         EFT = an(7) + move(posX, posY, 0, 0, 0, 500) + p(4)
  108.         ass_main(ASS_BUF, SubL(_start, _end), EFT, cd)

  109.     #############################

  110.     return (ASS_BUF, None)
复制代码
第二种方法,是用粒子,这里面用到了判断连通性的Labeling算法,网络上搜索 'Labeling' 就可以找到相关资料,比如:http://blog.csdn.net/icvpr/article/details/10259577
具体的算法不做解释,但还是需要自己查看一下才能看懂下面的代码。
  1. from tcaxPy import *

  2. def tcaxPy_Init():
  3.     global font
  4.     global fontsize

  5.     fontsize = GetVal(val_FontSize)
  6.     font = InitFont(GetVal(val_FontFileName), GetVal(val_FaceID), fontsize, GetVal(val_Spacing), GetVal(val_SpaceScale), 0xFFFFFF, 0, 0)


  7. def tcaxPy_User():
  8.     pass


  9. def tcaxPy_Fin():
  10.     FinFont(font)

  11. def GetPixelPointFromPix(PIX, InitPosX, InitPosY):      # 将方形PIX[1][0]xPIX[1][1]范围的pixel扫描成一个list,保存位置、RGBA、label信息
  12.     pixel = []
  13.     label = 0
  14.     for h in range(PIX[1][1]):
  15.         posY = InitPosY + h
  16.         for w in range(PIX[1][0]):
  17.             posX = InitPosX + w
  18.             idx = 4 * (h * PIX[1][0] + w)
  19.             pixR = PIX[2][idx + 0]
  20.             pixG = PIX[2][idx + 1]
  21.             pixB = PIX[2][idx + 2]
  22.             pixA = PIX[2][idx + 3]
  23.             pixel.append([(posX, posY), (pixR, pixG, pixB, pixA), label])
  24.    
  25.     return pixel

  26. def DeleteRepeat(labelset):     # 删除list中的重复元素

  27.     labelset2 = []

  28.     [labelset2.append(label) for label in labelset if not label in labelset2]

  29.     return labelset2      


  30. def UnionTwoLists(list1, list2):     # 合并2个list,默认它们含有相同元素
  31.     for elem in list1:
  32.         if elem in list2:
  33.             continue
  34.         else:
  35.             list2.append(elem)

  36.     return list2


  37. def UnionLabelSet(labelset):        # 整理labelset,合并
  38.     labelset = sorted(labelset, key=lambda k: k[0]) # 排序
  39.     labelset = DeleteRepeat(labelset) # 刪除重複

  40.     labelset2 = []


  41.     for label in labelset:
  42.         if labelset2 == []:               #第一個 label 直接添加
  43.             labelset2.append(label)

  44.         else:
  45.             flag = 0
  46.             for i in range(len(labelset2)):   #遍歷之前的label找交集 若存在交集 向對應位置 添加未存在的元素
  47.                 #tcaxLog(label2)
  48.                 if (label[0] not in labelset2[i]) and (label[1] in labelset2[i]):
  49.                     flag = 1
  50.                     labelset2[i].append(label[0])
  51.                 elif (label[0] in labelset2[i]) and (label[1] not in labelset2[i]):
  52.                     flag = 1
  53.                     labelset2[i].append(label[1])
  54.                 elif (label[0] in labelset2[i]) and (label[1] in labelset2[i]):
  55.                     flag = 1
  56.             if flag == 0:   # 若不存在交集 則另存
  57.                 labelset2.append(label)

  58.     #tcaxLog(labelset2)           
  59.     return labelset2
  60.    

  61. def tcaxPy_Main(_i, _j, _n, _start, _end, _elapk, _k, _x, _y, _a, _txt):

  62.     ASS_BUF  = []        # used for saving ASS FX lines

  63.     #############################
  64.     # TODO: write your codes here #

  65.     PIX = TextPix(font, _txt)

  66.     InitPosX = _x - int(_a / 2 + 0.5) + PIX[0][0]
  67.     InitPosY = _y - int(fontsize / 2 + 0.5) + PIX[0][1]

  68.     pixel = GetPixelPointFromPix(PIX, InitPosX, InitPosY)

  69.     num = len(pixel)
  70.     label = 0
  71.     labelset = []

  72.     # 1-pass
  73.     for i in range(num):
  74.         if pixel[i][1][3] != 0:     # 如果是有效像素
  75.             if i == 0:              # 如果是第1个像素,没有左面和上面的相邻像素,需要单独考虑
  76.                 label += 1
  77.                 pixel[i][2] = label
  78.             elif i % PIX[1][0] == 0:                # 如果是第1列像素,没有左面的相邻像素,需要单独考虑
  79.                 uppixelindex = i - PIX[1][0]        # 取上面的相邻像素index
  80.                 if pixel[uppixelindex][2] != 0:      
  81.                     pixel[i][2] = pixel[uppixelindex][2]    # 如果上面的相邻像素是有效像素,就把上面像素的label赋给当前像素
  82.                 else:                                       # 如果上面的相邻像素不是有效像素,就把label加1,然后赋给当前像素
  83.                     label += 1
  84.                     pixel[i][2] = label
  85.             elif i <= PIX[1][0] - 1:        # 如果是第1行像素,没有上面的相邻像素,需要单独考虑
  86.                 leftpixelindex = i - 1
  87.                 if pixel[leftpixelindex][2] != 0:
  88.                     pixel[i][2] = pixel[leftpixelindex][2]
  89.                 else:
  90.                     label += 1
  91.                     pixel[i][2] = label
  92.             else:                           # 同时考虑上面和左面的相邻像素是否是有效像素
  93.                 uppixelindex = i - PIX[1][0]
  94.                 leftpixelindex = i - 1
  95.                 if pixel[leftpixelindex][2] != 0 and pixel[uppixelindex][2] != 0:                   # 如果上面和左面的相邻像素都是有效像素,
  96.                     pixel[i][2] = min(pixel[leftpixelindex][2], pixel[uppixelindex][2])             # 就把它们的label的最小值赋给当前像素
  97.                     if pixel[leftpixelindex][2] != pixel[uppixelindex][2]:                          # 如果它们的label不相等,
  98.                         labelset.append(sorted([pixel[leftpixelindex][2], pixel[uppixelindex][2]])) # 就把它们的label组成一个list加入labelset
  99.                 elif pixel[leftpixelindex][2] != 0 and pixel[uppixelindex][2] == 0:                 # 如果上面的相邻像素不是有效像素,
  100.                     pixel[i][2] = pixel[leftpixelindex][2]                                          # 就把左面像素的label赋给相邻像素
  101.                 elif pixel[leftpixelindex][2] == 0 and pixel[uppixelindex][2] != 0:                 # 如果左面的相邻像素不是有效像素,
  102.                     pixel[i][2] = pixel[uppixelindex][2]                                            # 就把上面像素的label赋给相邻像素
  103.                 else:                                                                               # 如果如果上面和左面的相邻像素都不是有效像素,
  104.                     label += 1                                                                      # 把label加1,赋给当前像素
  105.                     pixel[i][2] = label
  106.     #tcaxLog(pixel)                                       
  107.     labelset = UnionLabelSet(labelset)       

  108.     #print(labelset)                       
  109.       
  110.     # 2-pass    #這個地方寫法冗長 比較亂 沒仔細想

  111.     for i in range(len(pixel)):   #按labelset 修改相同 label
  112.         if pixel[i][2] != 0:
  113.             for j in range(len(labelset)):
  114.                 if pixel[i][2] in labelset[j]:                                            
  115.                     pixel[i][2] = min(labelset[j])
  116.                  
  117.     #tcaxLog(min(labelset[j]))
  118.     rndx = []
  119.     rndy = []
  120.     cor = []
  121.     labelbuf = []

  122.     for i in range(len(pixel)):   # 將不連續的label修改成最小連續數列

  123.         label = pixel[i][2]
  124.         if label != 0:
  125.             if label not in labelbuf:
  126.                 labelbuf.append(label)
  127.             pixel[i][2] = labelbuf.index(label) +1


  128.     for i in range(len(labelbuf)):  # 按照label個數生成隨機位置和顏色list
  129.         rndx.append(randint(-50, 50))
  130.         rndy.append(randint(-100, 100))
  131.         cor.append(RandRGB())

  132.     #tcaxLog(sorted(labelbuf))

  133.     #if _i == 0 and _j == 2:
  134.     #    tcaxLog(pixel)
  135.   
  136.     for i in range(len(pixel)):   #繪製  
  137.         label = pixel[i][2]

  138.         if label != 0:

  139.             EFT = move(pixel[i][0][0] + rndx[label-1], pixel[i][0][1] + rndy[label-1], pixel[i][0][0], pixel[i][0][1], 0, 500) + alpha(255 - pixel[i][1][3]) + color1(cor[label-1])
  140.             ass_main(ASS_BUF, SubL(_start, _end, 0, Pix_Style), EFT, DrawPoint())


  141.     #############################

  142.     return (ASS_BUF, None)
复制代码
6

查看全部评分

Rank: 4

沙发
发表于 2015-9-11 08:29:32 |只看该作者
好东西 新人回覆

Administrator

TCAX Dev.

Rank: 7Rank: 7Rank: 7

板凳
发表于 2015-9-12 22:55:57 |只看该作者
给你32个赞.

Rank: 4

地板
发表于 2015-10-6 12:54:08 |只看该作者
赞一个!

Rank: 4

5#
发表于 2016-5-1 20:23:58 |只看该作者
话说,怎么使用这些代码?

Rank: 4

6#
发表于 2018-2-11 21:53:42 |只看该作者
谢谢楼主分享

Rank: 4

7#
发表于 2020-7-31 12:04:06 |只看该作者
github上的NyuFX源码也有拆分字形的lua脚本,也是根据楼主所说的第1种方法来的:
https://github.com/Youka/NyuFX/b ... ons/shape.split.lua

Administrator

Shanzhai Pro.

Rank: 7Rank: 7Rank: 7

8#
发表于 2020-7-31 20:18:54 |只看该作者
Seekladoom 发表于 2020-7-31 12:04
github上的NyuFX源码也有拆分字形的lua脚本,也是根据楼主所说的第1种方法来的:
https://github.com/Youka ...

嗯 不太一样 国外不太讨论这种问题 没汉字
其实主要是一个拆分笔画的问题
但笔画的话 不太好实现
于是想 把连通的部分 拆出来
两种方法吧
一种通过字体读到的矢量情报来拆 代码没什么印象了
字体矢量连通图像 是这么一个顺序 外圈轮廓画完
逆时针再画一圈 在ass图形上 就镂空了
问题是整体代码 不是画完外圈紧接着画内圈
还有像“日”这样的字 一个外圈 接很多个内圈
所以这个代码就是怎么找 正确的外圈和内圈的归类

一种读字体像素情报进行归类
labeling 这是很经典的一个算法 学一点图像处理的话 应该都会教
https://zh.wikipedia.org/wiki/%E ... F%E6%A0%87%E8%AE%B0

嘛 这么多年过去了 现在想想 可能找个笔画的数据库 读一读 有笔顺再识别可能好一点

Rank: 4

9#
发表于 2020-7-31 21:54:07 |只看该作者
本帖最后由 Seekladoom 于 2020-7-31 22:22 编辑
saiyaku 发表于 2020-7-31 20:18
嗯 不太一样 国外不太讨论这种问题 没汉字
其实主要是一个拆分笔画的问题
但笔画的话 不太好实现

关于笔画的数据库,我自己知道的方法有两种:

方法1:商业字体厂商(比如方正字库和汉仪字库)有那种专门的字体骨架,但是那种东西是商业机密,就算拿到了也只能自己玩玩,不能对外公开。

方法2:自己去给字体画字体骨架,临时做OPED的话一般也就是几十百八来个字,偶尔做做还行,但长期搞就顶不住。

但如果有人有毅力把大厂的字体,比如Adobe的思源黑体和思源宋体的骨架(Adobe的字体骨架自行仿制画出来不用担心会有侵权问题,毕竟思源字体的开源协议是OFL协议,基本上允许你随便用。国内厂商这块就捏得比较死,要当心侵权问题。)全给画一遍,然后再公开出来,那就方便了,至少凑合做特效用用应该还是可以的。稍微降下档次的话,先把GB2312-80的简体字体的6763外加常用字扩充的101个字做出来,也就是6864字,这个工作量就小一点,就看有没有人有毅力做并且肯公开了。BIG5的话,繁体的汉字数是13000左右,工作量将近是简体的2倍,要画的时间就稍微长一点。

当然也不是真的做不到,可以试着去学习字体骨架和笔画拼接技术这块的相关知识,把工作量分段来完成,现在知乎中印社区也有不少关于字体这块的各种技术经验分享了,可以去这里多搜搜看。特别是知乎,这上面能提高人的工作效率的各种手段非常多,可以极大降低字幕制作者的工作压力。

Rank: 4

10#
发表于 2022-5-13 14:43:50 |只看该作者
本帖最后由 Seekladoom 于 2022-5-13 14:44 编辑

【Aegisub】极度高效的拆字算法、连通域提取
您需要登录后才可以回帖 登录 | 新人加入

GitHub|TCAX 主页

GMT+8, 2024-11-23 20:22

Powered by Discuz! X2

© 2001-2011 Comsenz Inc.

回顶部
RealH