做机器视觉调试的人,八成都有过这种经历:想在图像上临时画个框、标个区域、模拟一个目标形状,结果在 Halcon 里翻来覆去找不到合适的代码段。Halcon 画图看似简单,但 Region、XLD、窗口显示这几个概念一搅在一起,新手特别容易懵。今天这个“小案例”我经常拿来给新人练手——用算子打印一个斜式楼梯和打印一个正方形,而且每个图形都用两种不同思路实现。别小看这两个图形,它们几乎把 Halcon 绘图相关的核心算子都串了一遍:矩形区域生成、区域合并、多边形轮廓构造、旋转矩形、窗口显示设置。把这套代码吃透,后面写 ROI 标注、标定板模拟、缺陷区域叠加,你都会顺手很多。
先说一个容易绕进去的概念:“打印”这两个字在 Halcon 里其实对应了三种完全不同的技术路线。第一种是直接在窗口上“画”出来,用的是disp_rectangle1、disp_line这类显示算子;第二种是生成一个 Region 区域对象,再丢给dev_display显示;第三种是生成一个 XLD 轮廓对象,再用dev_display显示。同样是屏幕上出现一条线或一个方块,底层数据模型是完全不一样的。Region 是像素级区域,XLD 是亚像素精度的轮廓线,窗口直画则是临时像素叠加,保存图像时不一定会留下记录。这个案例里我刻意选了后两种来对比,因为它们在后续图像处理中能继续被使用,实战价值更高。
1. 案例拆解:先搞懂“打印”在 Halcon 里到底是什么意思
1.1 一个高频动作背后的两种对象
先别急着写代码,得把 Halcon 里图形对象的家底摸清楚。做视觉项目时,你往窗口上显示的所有图形,归纳起来就三类:图像(Image)、区域(Region)、轮廓(XLD)。图像就是相机拍回来的或者读进来的像素图;区域是一个个像素点组成的集合,通常用游程编码保存,适合做填充、统计、形态学处理;XLD 是几何轮廓,保存的是亚像素精度的点坐标序列,适合做边缘提取、测量、模板匹配。
画楼梯和画正方形,如果用“区域”来做,那本质是构造若干个矩形区域再合并;如果用“XLD”来做,那就是算出一串顶点坐标,再构造一条多边形轮廓线。两种方式画出来的图形,在窗口上肉眼看可能一模一样,但数据类型完全不同,后续能调用的算子也完全不同。比如你想统计这个正方形的面积,用 Region 直接area_center;你想测量楼梯斜边的长度精度,用 XLD 后配合轮廓测量算子更顺手。所以“两种方法”不是炫技,而是让你理解 Halcon 两条主线的底层逻辑。
1.2 为什么每个图形都要准备两种画法
我再举个例子你就明白了。项目里经常要画一个 ROI 区域,比如电路板上的一个元器件。如果只是给人眼看,disp_rectangle1画个框就够了;但如果你要把这个框交给后面的检测算子去裁剪图像、计算尺寸,那必须生成一个 Region 对象,甚至转成 XLD 去做亚像素测量。我在实际项目里见过不少新人,一开始用disp_rectangle1把框画在窗口上,看起来很正常,结果后面想用reduce_domain去抠图,发现根本没有对象可用,只能重新写一遍代码。
所以这个案例的意义就在这里:同样的“打印”需求,用gen_rectangle1生成区域是一条路,用gen_contour_polygon_xld生成轮廓是另一条路。两条路你都得会走,因为不同场景下,它们各自的优势才会显现——区域适合做像素级处理,XLD 适合做几何级处理。下面的楼梯和正方形,我就分别用这两条路各做一遍,你对比着看。
2. 斜式楼梯:区域堆叠法与XLD轮廓法
2.1 思路先行:斜式楼梯的几何本质
斜式楼梯,说白了就是一组矩形台阶沿着斜线方向逐级错开。在图像坐标系里,行坐标向下增大、列坐标向右增大,所以一个“向右下延伸”的楼梯,每一级台阶都可以看作一个矩形:水平方向占一段宽度,垂直方向占一段高度,后一级在前一级的右下方偏移。
把问题拆成小矩形来看,是最直观的思路。每个台阶的宽设为StepW,高设为StepH,从起点(StartRow, StartCol)开始,第一级矩形的左上角就是起点,右下角是(StartRow+StepH, StartCol+StepW);第二级左上角整体向右下移动一格台阶距离,变成(StartRow+StepH, StartCol+StepW),右下角相应移动。循环 N 次,就得到 N 个矩形。把这些矩形合并成一个整体区域,这就是“斜式楼梯”的区域版本。
2.2 方法一:用区域堆叠法画斜式楼梯
区域堆叠法的核心算子就两个:gen_rectangle1负责生成每个台阶矩形,union2负责把矩形逐级合并。代码很直接,我贴出来逐行解释。
* 打开显示窗口 dev_open_window (0, 0, 640, 480, 'black', WindowHandle) dev_set_color ('yellow') dev_set_draw ('fill') * 楼梯参数 StepW := 60 StepH := 30 StepN := 8 StartRow := 50 StartCol := 50 * 生成第一级台阶 gen_rectangle1 (RegionStair, StartRow, StartCol, StartRow + StepH, StartCol + StepW) * 逐级生成并合并 for I := 2 to StepN by 1 gen_rectangle1 (RegionStep, StartRow + (I - 1) * StepH, StartCol + (I - 1) * StepW, StartRow + I * StepH, StartCol + I * StepW) union2 (RegionStair, RegionStep, RegionStair) endfor dev_display (RegionStair)注意看循环里的坐标计算:第 I 级台阶的左上角行坐标是StartRow + (I-1)*StepH,列坐标是StartCol + (I-1)*StepW;右下角行坐标是StartRow + I*StepH,列坐标是StartCol + I*StepW。这样每一级都比前一级在行方向多出StepH、列方向多出StepW,自然形成了斜向排列。
有一点你要特别留意:union2的输入输出共用了RegionStair变量。Halcon 允许这样写,它会把前一次合并结果和新台阶取并集,再赋回给同一个变量。如果你刚开始学,建议拆开写清楚:
union2 (RegionStair, RegionStep, RegionUnion) copy_obj (RegionUnion, RegionStair)虽然多两行,但不容易把自己绕晕。区域合并完,显示出来是一整块填充的阶梯状色块,边界非常平整。想只看轮廓的话,显示前加一句dev_set_draw ('margin'),区域就只描边不填充了。
2.3 方法二:用XLD轮廓一次性生成锯齿多边形
区域堆叠法要循环 N 次生成矩形再合并,代码稍长。如果用 XLD 轮廓法,你可以直接算出楼梯外轮廓的所有转折点坐标,一次调用gen_contour_polygon_xld生成一条闭合锯齿多边形,干净利落。
先想清楚要哪些点。以闭合区域为例,起点放在楼梯的左上角(StartRow, StartCol)。然后沿楼梯的斜边前进,每一级先向右平移一个StepW,再向下平移一个StepH。重复 N 次后,到达整个楼梯的右下角尖点。最后为了封闭图形,从右下角水平向左拉到底边,再垂直向上回到起点。顶点序列长度是2*StepN + 2。
看一下代码实现:
* 参数与前面保持一致 StepW := 60 StepH := 30 StepN := 8 StartRow := 50 StartCol := 50 * 构造顶点坐标元组 Rows := [] Cols := [] Row := StartRow Col := StartCol for K := 1 to StepN by 1 Col := Col + StepW Rows := [Rows, Row] Cols := [Cols, Col] Row := Row + StepH Rows := [Rows, Row] Cols := [Cols, Col] endfor * 闭合:底边左端和左上角起点 Rows := [Rows, Row, StartRow] Cols := [Cols, StartCol, StartCol] * 生成XLD轮廓 gen_contour_polygon_xld (ContourStair, Rows, Cols) dev_set_color ('green') dev_set_line_width (2) dev_display (ContourStair)这里有两个细节值得说道。第一,空元组变量Rows := []在 HDevelop 里是可以正常拼接的,不会报错,这招在动态构造坐标点序列时非常常用。第二,gen_contour_polygon_xld会把Rows和Cols里每一对坐标按顺序连起来,最后一个点不自动连回第一个点,所以闭合必须手动把起点坐标再追加一次。我上面最后一条Cols := [Cols, StartCol, StartCol]就是把底边左端点(Row, StartCol)和起点(StartRow, StartCol)一起加进去,完成了轮廓闭合。
跑完这段,窗口里会出现一条绿色锯齿线。因为 XLD 本身是轮廓线,不填充内部,所以视觉上和区域堆叠法的“填充色块”风格明显不同。想看填充效果,可以先用gen_region_polygon把轮廓转成区域,再显示:
gen_region_polygon (RegionStairFromContour, Rows, Cols) dev_set_draw ('fill') dev_display (RegionStairFromContour)2.4 两种方法的效果差异与选型建议
做完两种楼梯,我建议你并排显示对比一下。区域堆叠法显示的是填充区域,边缘像素严格落在整数坐标上;XLD 轮廓法显示的是亚像素精度的线条,放大后边缘更细腻。这两种对象在后续处理里各有优势:如果要计算楼梯的面积占比、做形态学开闭运算,用区域版本;如果要测量斜边长度、角度,或者拿轮廓去做匹配,用 XLD 版本。
另外提一句性能。当台阶数量非常多时,比如要画几百级台阶,循环里反复union2会有点慢。工程上更推荐一次性用数组构造点坐标,再用gen_region_polygon或gen_contour_polygon_xld直接生成整个对象,避免多次迭代合并。这也是我把 XLD 轮廓法作为第二种方法推荐给你的核心原因——它更接近大批量绘图场景下的最优解。
3. 正方形:轴对齐画法与旋转画法(同样是两种思路)
3.1 先理清Halcon里的平面直角坐标规则
画正方形没什么难的,但很多人在 Halcon 里一画就歪,根子在于没搞懂它的坐标规则。Halcon 图像坐标系的原点在左上角,行坐标(Row)向下增大,列坐标(Column)向右增大。这一点和数学课堂上常见的直角坐标系不一样,和 OpenCV 的图像坐标系倒是同源。于是“长宽相等”的正方形在 Halcon 里就是:行列方向的跨度相等。一个轴对齐正方形,左上角(Row1, Col1)、右下角(Row2, Col2),必须满足Row2 - Row1 = Col2 - Col1。
很多新手在这翻车,不是不知道边长相等,而是把行列坐标的顺序写反了,或者用笛卡尔坐标系的习惯去套,结果画出来的矩形横竖比不对。记住一个土办法:先写行、后写列,所有矩形算子的参数顺序都是Row在前,Column在后。gen_rectangle1的参数是(Row1, Column1, Row2, Column2),千万别记成(x1, y1, x2, y2)。
3.2 方法一:基于gen_rectangle1和gen_rectangle2的区域绘制
正方形第一种实现,用区域生成算子。如果你只需要一个不旋转的正方形,gen_rectangle1就是最直接的。设中心点坐标(CenterRow, CenterCol),半边长A,那么左上角是(CenterRow-A, CenterCol-A),右下角是(CenterRow+A, CenterCol+A)。
* 轴对齐正方形,半边长100 CenterRow := 300 CenterCol := 300 A := 100 gen_rectangle1 (SquareAxis, CenterRow - A, CenterCol - A, CenterRow + A, CenterCol + A) dev_set_color ('cyan') dev_set_draw ('margin') dev_display (SquareAxis)这里我特意在显示前加了dev_set_draw ('margin'),这样区域只显示四条边界,看起来就是个标准的正方形线框。如果去掉这句,默认'fill'模式会把整个内部填充成纯色,反而看不出正方形轮廓。
那如果要画一个任意角度的正方形呢?用gen_rectangle2,参数是中心点坐标、旋转角Phi、两个半轴长度Length1和Length2。因为正方形两边一样长,所以Length1和Length2都等于半边长A。Phi是长轴相对于列轴正方向的夹角,单位是弧度,不是角度。这事特别容易踩坑,我见过有人直接填 45,结果画出来的矩形角度完全不对,因为 45 在这里被当成 45 弧度了。要转弧度用 HDevelop 自带的rad()函数。
* 旋转30度的正方形 Phi := rad(30) gen_rectangle2 (SquareRot, CenterRow, CenterCol, Phi, A, A) dev_set_color ('magenta') dev_display (SquareRot)gen_rectangle2的旋转中心就是输入的中心点,所以正方形不会飘走。Phi为正时,图形绕中心逆时针旋转;为负则顺时针。想画 45 度旋转,写rad(45)即可。这也是“第一种正方形”里顺带演示了旋转能力,一个算子就能覆盖所有情况。
3.3 方法二:基于四个角点的XLD轮廓绘制
正方形第二种实现,用 XLD 轮廓。既然正方形四个角点坐标可以自己算出来,那就用gen_contour_polygon_xld把这四个点连起来。
轴对齐的情况最简单。四个角点按左上、右上、右下、左下的顺序连一圈,最后记得闭合回左上点:
RowTL := 100 ColTL := 100 Side := 200 RowBR := RowTL + Side ColBR := ColTL + Side gen_contour_polygon_xld (SquareContour, [RowTL, RowTL, RowBR, RowBR, RowTL], [ColTL, ColBR, ColBR, ColTL, ColTL]) dev_set_color ('red') dev_set_line_width (2) dev_display (SquareContour)这段代码没有用循环,直接把四个角点方向的行/列元组写进了算子参数,非常紧凑。注意第五个点就是第一个点,不写的话轮廓会缺一条边。
如果正方形要旋转任意角度,那就得老老实实算旋转后的顶点坐标。这里正好复习一下三角函数。以中心点为旋转轴,四个顶点在初始状态(不旋转)分别位于中心点的右、上、左、下四个方向,距离都是半边长A。绕中心旋转Phi后,每个顶点的行列坐标用下面的循环算:
CenterRow := 300 CenterCol := 300 A := 100 Phi := rad(30) SquareRows := [] SquareCols := [] for Idx := 0 to 3 by 1 Angle := Phi + Idx * rad(90) SquareRows := [SquareRows, CenterRow - A * sin(Angle)] SquareCols := [SquareCols, CenterCol + A * cos(Angle)] endfor * 闭合轮廓 SquareRows := [SquareRows, SquareRows[0]] SquareCols := [SquareCols, SquareCols[0]] gen_contour_polygon_xld (SquareContourRot, SquareRows, SquareCols) dev_set_color ('blue') dev_set_line_width (2) dev_display (SquareContourRot)为什么行偏移是-A*sin(Angle),列偏移是A*cos(Angle)?因为行坐标向下为正,图像坐标系里绕中心逆时针旋转时,行方向的分量恰好是负正弦。你可以代入Phi=0验证:四个点分别出现在右、上、左、下,闭合后就是个轴对齐正方形。把Phi改成rad(30),它就原地旋转 30 度,和之前gen_rectangle2画出来的旋转正方形完全重合。我把两种方法画出的图形重叠在一起对比过,轮廓完全一致,这也说明了 XLD 手算坐标是可靠的。
3.4 为什么尺寸会“看起来不对”:窗口比例与dev_set_part
画正方形很容易碰到一个诡异问题:代码里明明给了Side:=200,窗口里看起来却是扁的。这个锅通常不在你的坐标计算,而在窗口显示比例。Halcon 窗口默认会把整个客户区映射到当前显示范围,如果你的窗口宽高比和图像/显示范围不一致,一个数学意义上的正方形就会被拉伸成长方形。
解决思路很直白:让窗口的物理宽高比和图像行列范围保持一致。最省事的写法是用dev_open_window_fit_image,它会根据图像的宽高自动创建合适比例的窗口:
read_image (Image, 'printer_chip/printer_chip_01') dev_open_window_fit_image (Image, 0, 0, -1, -1, WindowHandle) dev_display (Image)如果图像还没读或者你想直接画在空白窗口上,就手动设定显示范围,用dev_set_part把窗口显示区域固定成一个行数和列数相等的范围。比如:
dev_open_window (0, 0, 512, 512, 'black', WindowHandle) dev_set_part (0, 0, 511, 511)dev_set_part的参数依次是显示范围左上角的行、列,右下角的行、列。设定后,无论窗口物理尺寸是多少,显示内容都会按你给定的行列范围等比缩放。我调试画图类代码时,几乎每次都会先检查这一句,很多“正方形变扁”“矩形变斜”的诡异现象都出在显示映射上,而不是几何计算上。
4. 实战过程与二次优化
4.1 从画图到保存:一套完整的调试小工程
光在窗口里看到图形还不算完,实际项目中你经常需要把“画好了图”的结果保存下来,发给同事确认,或者留档给测试脚本。Halcon 里最常见的做法是先用dump_window_image把窗口当前内容(包括叠加的区域、轮廓)抓成一幅图像,再用write_image写到磁盘。
* 在窗口中把图像和图形一起显示出来 read_image (Image, 'printer_chip/printer_chip_01') dev_open_window_fit_image (Image, 0, 0, -1, -1, WindowHandle) dev_display (Image) * 叠加显示楼梯和正方形 * ... 省略前面生成的 RegionStair / ContourStair / SquareAxis / SquareContourRot ... dev_display (RegionStair) dev_set_color ('cyan') dev_display (SquareAxis) dev_set_color ('blue') dev_display (SquareContourRot) * 保存窗口截图 dump_window_image (WindowImage, WindowHandle) write_image (WindowImage, 'png', 'stair_and_square_result')有一点要提醒你:dump_window_image抓取的是窗口当前可见内容,如果你把图形显示在窗口外区域,或者图形被后续显示的内容覆盖了,保存结果里就看不到。所以保存前务必确认显示顺序——先显示图像,再显示区域/轮廓,最后的对象会叠在最上层。
4.2 颜色、线宽、填充模式的细节控制
很多人画图不注意显示属性,结果线太细看不见,或者颜色和背景融为一体。Halcon 提供了一组dev_set_*算子来控制这些属性,我用表格给你整理一下常用的:
| 算子 | 作用 | 常用值 |
|---|---|---|
dev_set_color | 设置后续显示对象的颜色 | 'red'、'green'、'cyan'等 |
dev_set_colored | 设置多对象显示时的自动配色数量 | dev_set_colored (6)表示最多6种颜色 |
dev_set_line_width | 设置轮廓线的像素宽度 | 正整数,一般2~5 |
dev_set_draw | 设置区域显示模式 | 'fill'填充,'margin'只描边 |
dev_set_shape | 设置区域显示形状 | 'rect'、'convex'、'original'等 |
这些属性是“粘性”的,设置一次会一直生效,直到再次修改。所以我习惯在显示代码块开头统一设置一次颜色和线宽,不想让后面代码意外改变显示效果。顺带说明,dev_set_draw ('margin')只影响区域对象的显示方式,不影响 XLD 轮廓,XLD 永远按线框方式显示,所以你想让楼梯以填充区域出现,就用区域版本;以线条出现,就用 XLD 版本。
4.3 引入循环与数组后的批量绘制技巧
如果不止画一组楼梯或正方形,而是要画一批怎么办?比如模拟一排楼梯缺陷,或者在一张图上标注多个 ROI。这时候再用逐个变量写就太累了,建议上循环和元组数组。
拿正方形来说,你可以把所有中心点坐标放在两个元组里,循环生成多个区域对象:
CenterRows := [100, 300, 500] CenterCols := [100, 200, 300] A := 80 for Idx := 0 to |CenterRows| - 1 by 1 gen_rectangle1 (SquareSingle, CenterRows[Idx] - A, CenterCols[Idx] - A, CenterRows[Idx] + A, CenterCols[Idx] + A) if (Idx == 0) copy_obj (SquareSingle, SquareMulti) else concat_obj (SquareMulti, SquareSingle, SquareMulti) endif endfor dev_set_colored (3) dev_display (SquareMulti)这里|CenterRows|是求元组长度,concat_obj把多个独立对象拼成一个对象数组,最后dev_set_colored (3)让三个正方形自动显示成不同颜色。这个技巧在批量创建 ROI 时特别有用,配合select_shape做筛选时,对象数组又是标准的输入格式,一步到位。
5. 常见问题排查与避坑手册
5.1 画出来的形状缺了一块或多了一块
如果你用区域堆叠法画楼梯,发现阶梯在某些地方叠加异常,十有八九是坐标增量算错了。我最开始写的时候,把第二级台阶的左上角写成了StartRow + I*StepH而不是(I-1)*StepH,结果第二级台阶直接“跳”了一格,中间就塌了一块。排查方法很简单:把循环里的I值打印出来,人工代入检查前三级台阶的坐标,看是不是连续递增的。区域合并类问题优先怀疑坐标,其次才是union2的调用顺序。
5.2 图形不显示,或者显示一瞬间就消失
新手经常遇到两个现象:一是dev_display显示了区域,但紧接着被下一次dev_display清掉;二是 XLD 轮廓线太细,压根看不清。Halcon 窗口同一时间只能维护一组显示对象,重复调用dev_display会替换之前的显示内容。如果你想把多个图形同时保留,要么在显示前把多个对象合并成一个对象数组,一次性dev_display显示;要么用disp_*系列直画算子做临时标注,但要注意这类直画内容在窗口重绘后会消失。至于线太细,直接dev_set_line_width (3),并且选一个跟背景反差大的颜色。
5.3 坐标越界、负数坐标与显示裁剪
图像坐标系里有边界,图形坐标超出范围时,窗口会把超出部分裁掉,只显示范围内的片段。坐标负值也一样,可能画出来“少半边”。所以画图前先确认目标图像的大小,或者统一把起点设在图像内的安全区域,比如四周预留 50 像素的边距。如果你需要在任意位置画图但不想手动算边界,可以先用get_image_size拿到图像的宽高,再根据图形尺寸动态计算合适的起始坐标。这个小习惯能省掉很多边界排查时间。
5.4 旋转矩形画成正方形时角度与Length1/2的搭配
gen_rectangle2画正方形时,如果设置Phi=rad(45),但Length1和Length2不相等,那画出来就是菱形感的长方形,不是正方形。有些人会误以为是角度写错,其实根本没这个事。要保证图形是旋转正方形,前提永远是两个半轴长度相等。我之前排查过一个案例,某同事把Length1和Length2设成了宽和高的半值,以为是半径,结果 45 度旋转后图形看着特别别扭。别怀疑自己的角度,先检查Length1是否等于Length2。
5.5 问题排查速查表
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 正方形显示为长方形 | 窗口宽高比与显示范围比例不一致 | 用dev_set_part固定行列范围,或dev_open_window_fit_image |
| 旋转角度不对 | Phi填了角度值忘了转弧度 | 使用rad(角度)转换 |
| 楼梯某级缺一块 | 循环里坐标增量多算了一格 | 检查(I-1)与(I)的坐标公式 |
| XLD 看不到 | 线宽太细、颜色与背景太接近 | dev_set_line_width (3)+ 高对比色 |
| 多个图形不同时存在 | 后一次dev_display覆盖了前一次 | 用concat_obj合并对象数组后统一显示 |
| 区域显示是实心,想要边框 | 显示模式是fill | 显示前加dev_set_draw ('margin') |
| 保存结果没有图形 | dump_window_image时图形被后续刷新覆盖 | 保存前最后统一显示一次所有对象 |
6. 一些后续可以继续玩的扩展方向
画完楼梯和正方形,如果你觉得意犹未尽,我建议你继续往这几个方向折腾一下,每个方向都能加深对 Halcon 绘图机制的理解。
第一个方向:把画好的区域和 XLD 互相转换。区域转 XLD 用gen_contour_region_xld,XLD 转区域用gen_region_contour_xld。你画出来的楼梯区域,可以用gen_contour_region_xld转成亚像素轮廓,再配合fit_line_contour_xld拟合楼梯斜边的直线方程;同理,手算出来的旋转正方形轮廓,可以转成区域后用area_center算中心和面积。这种互转是视觉算法的常规操作,练熟了对后面做测量、匹配帮助很大。
第二个方向:试试交互式绘图算子。Halcon 里还有一组draw_*算子,比如draw_rectangle1、draw_rectangle2、draw_polygon,它们可以在窗口里用鼠标拖拽交互式画图,返回用户画的几何参数。我经常在调试阶段用它手动框选一个 ROI,再拿框选结果去做区域裁剪。理解了gen_*是怎么参数的,再看draw_*的返回值就会特别轻松,因为两者共享同一套几何参数体系。
第三个方向:把画出来的图形变成“缺陷模拟器”。我在一个模拟项目里干过类似的事——在一张干净的 PCB 图像上叠加一个斜式楼梯形状的灰色区域,用来模拟划痕缺陷;再用旋转正方形模拟倾斜的贴片元件区域。这样几乎零成本就能批量生成带标注的测试图像,用来调检测算法再合适不过。简单的paint_region就能把区域刷进图像,你可以试试把前面两道楼梯用不同灰度值刷到背景图上,跑一个阈值分割看效果。
我个人带新人时特别爱用这两组小图形当入门题,因为它们短小却完整覆盖了区域、轮廓、坐标变换、显示控制这些最常用的知识点。踩过几次坑之后,我现在的习惯是:任何绘图代码,先问自己一句“我到底要区域对象还是轮廓对象”,想清楚了再动手。只要能回答这个问题,斜式楼梯、正方形,还是更复杂的任意多边形,写起来都只是同样的套路换几个坐标而已。