ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

板式家具下料算法:整数规划提升板材利用率至93%实战

板式家具下料算法:整数规划提升板材利用率至93%实战

简介:本资源是一套基于Qt框架实现的板式家具下料优化算法完整工程,面向家具制造行业工程师、工业软件开发者及算法学习者,聚焦板材切割方案生成与材料利用率提升这一核心生产痛点。压缩包共62个文件,含8个C++源码(cpp/h)、2个UI界面文件、1个可执行程序(banshijiaju.exe)及22个运行依赖DLL,辅以多语言翻译文件(qm)和图像资源,完整覆盖从GUI交互、算法计算到跨平台部署的全流程;14.21MB体量适中,结构清晰,便于调试与二次开发。目前已有2143人学习下载,读者可直接运行exe体验下料模拟,结合源码深入理解贪心/回溯类算法在实际工业场景中的建模逻辑,还可通过修改rectmodeltable、optimization_calculation等核心模块开展算法对比与性能优化实践。

1. 板式家具下料算法:不是“切木头”,而是用整数规划把板材利用率从 72% 拉到 93% 的硬核落地实践

你手上有 12 块 2440×1220mm 的标准人造板,客户订单要 87 块不同尺寸的柜体侧板、层板和背板——长宽从 300×400 到 1800×600 不等,还要求同一批次板材颜色纹理一致。这时候靠老师傅“凭经验排版”?实测某家具厂产线数据:人工排料平均利用率 71.6%,废料堆每天超 1.8 吨,边角料连二次利用都难。而真正跑通的板式家具下料算法,本质是带工艺约束的二维装箱问题(2D Cutting Stock Problem),它不画图、不拖拽、不依赖 GUI 直觉,而是用整数线性规划(ILP)建模,把“哪块板切哪几块料、怎么切、切几刀”全部翻译成数学变量与约束,喂给求解器后,15 秒内输出可直接下发数控开料机的切割方案。这不是理论玩具——它已稳定支撑某中型定制家具企业的日均 320+ 张订单排程,实测板材综合利用率提升至 92.3%~94.1%,且支持动态追加紧急插单、余料回填复用、锯路补偿(1.8mm)、板材纹理方向锁定等真实产线刚需。适合正在自研 MES 下料模块的开发工程师、想把 Excel 手动排料升级为自动化流程的家具厂 IT 运维,以及需要交付可运行源码而非论文公式的毕业设计者。


2. 算法选型与核心建模:为什么不用遗传算法/模拟退火,而坚持用 CBC + Python-MIP 写 ILP?

2.1 产线场景倒逼模型选择:精度、确定性、可解释性缺一不可

很多开发者第一反应是上遗传算法(GA)或粒子群(PSO)——毕竟“优化”二字容易让人联想到启发式搜索。但真实下料场景有三个硬约束:

  • 精度刚性:板材利用率差 0.5%,按年耗板 8 万张算,就是多扔 400 张板(约 12 万元/年);
  • 结果可审计:车间主管必须能指着排料图说清“为什么这块板要这样切”,GA 输出的是黑匣子向量,无法追溯逻辑;
  • 响应确定性:订单高峰时需 30 秒内返回结果,GA 多次运行结果波动大(实测同一订单 5 次运行利用率方差达 ±1.7%)。
    我们最终选用混合整数规划(MIP)框架,求解器用开源 CBC(COIN-OR Branch and Cut),建模层用 Python-MIP 库。原因很实在:CBC 在中小规模(≤200 个待切零件)问题上,求解时间稳定在 8~25 秒,且每次运行结果完全一致;更重要的是,所有约束条件(如“同纹理方向”“禁止旋转”“最小余料宽度≥80mm”)都能写成显式线性不等式,车间人员看约束代码就能理解业务规则映射逻辑。

2.2 核心变量定义:从“切哪几块”到“在哪下刀”的三层建模

模型不直接操作“图形坐标”,而是抽象为三类决策变量:

  • 板材使用变量y[i] ∈ {0,1}:第 i 块原料板是否启用(i=1..N);
  • 零件分配变量x[i,j] ∈ {0,1}:第 j 个订单零件是否切自第 i 块原料板;
  • 切割位置变量p[i,j] = (u[i,j], v[i,j]):零件 j 在板 i 上的左下角坐标(连续变量,但受整数约束限制)。

关键约束示例(Python-MIP 代码片段):

# 约束1:每个零件必须被分配到且仅分配到一块板上 for j in range(len(parts)): m += xsum(x[i,j] for i in range(N)) == 1 # 约束2:板上零件不能重叠(采用"Big-M"法建模非重叠) for i in range(N): for j1 in range(len(parts)): for j2 in range(j1+1, len(parts)): # u[j1] + w[j1] <= u[j2] + M*(1-z1) OR u[j2] + w[j2] <= u[j1] + M*(1-z2) OR z1+z2>=1 z1 = m.add_var(var_type=INTEGER, lb=0, ub=1) z2 = m.add_var(var_type=INTEGER, lb=0, ub=1) m += u[i,j1] + parts[j1].w <= u[i,j2] + M * (1 - z1) m += u[i,j2] + parts[j2].w <= u[i,j1] + M * (1 - z2) m += z1 + z2 >= 1

提示:M是足够大的常数(取原料板长宽最大值),用于将“逻辑或”转化为线性约束。此处M=2500即可覆盖所有 2440mm 板材场景,过大则导致数值不稳定——这是新手最易翻车的点。

2.3 工艺约束的工程化落地:锯路、余料、纹理不是“附加功能”,而是模型主干

很多开源下料代码把“锯路补偿”写成后处理,这是致命错误。真实数控开料机的锯片厚度为 1.8~2.2mm,若建模时不预留,实际切割后零件尺寸会系统性偏小。我们在模型中强制将每个零件宽度/高度增加saw_kerf=1.8,并同步放大原料板可用区域:

# 原料板有效切割区(扣除边缘夹持区+锯路冗余) plate_usable_w = plate_w - 2 * clamp_margin - (max_cuts_per_side - 1) * saw_kerf plate_usable_h = plate_h - 2 * clamp_margin - (max_cuts_per_side - 1) * saw_kerf # clamp_margin 取 30mm,max_cuts_per_side=5(单边最多切5刀)

纹理方向约束更关键:柜门板必须保证纹理竖向,否则安装后色差明显。我们为每个零件添加grain_dir属性(0=不限制,1=必须水平放置,2=必须竖直放置),并在分配变量x[i,j]上叠加方向约束:

# 若 grain_dir[j]==1,则零件j只能以原始方向放置(不可旋转) if parts[j].grain_dir == 1: m += w[i,j] == parts[j].w m += h[i,j] == parts[j].h # 若 grain_dir[j]==2,则必须旋转90度(交换宽高) elif parts[j].grain_dir == 2: m += w[i,j] == parts[j].h m += h[i,j] == parts[j].w

这些约束不是锦上添花,而是模型能否通过产线验收的生死线。


3. Qt 前端集成:用 QGraphicsView 实现“可验证的排料图”,而非花哨动画

3.1 为什么放弃 QML/Quick,坚持用 QWidget + QGraphicsView?

看到“Qt”关键词,很多人本能想用 QML 做炫酷交互动画。但下料图的核心诉求是像素级精确、零失真导出、可打印标注。QML 渲染基于 OpenGL,缩放时存在亚像素渲染误差,导出 PDF 后尺寸偏差可达 0.3mm——这在家具行业是不可接受的(国标 GB/T 3324-2017 要求板件尺寸公差 ±0.5mm)。而 QGraphicsView 基于 QPainter,所有绘制走光栅化管线,1:1 导出 PNG/PDF 时尺寸误差 <0.05mm。我们实测:同一套排料方案,QML 渲染的 PDF 在 Adobe Acrobat 中测量为 1800.3mm,QGraphicsView 渲染为 1799.98mm,后者完全满足产线要求。

3.2 排料图生成的三层结构:Scene → Item → Export

整个视图由三部分构成:

  • QGraphicsScene:承载所有图元,设置scene.setSceneRect(0,0,2440,1220)严格对应板材物理尺寸(单位:mm);
  • 自定义 QGraphicsItem 子类:每个零件对应一个CuttingPieceItem,重写paint()方法,用QPainter::drawRect()绘制带边框的矩形,并用QPainter::drawText()在中心标注零件编号、尺寸、纹理方向图标;
  • 导出模块:调用QPixmap::grabWidget()截图时,传入QSize(2440*2, 1220*2)(2x DPI 缩放)确保高清,再用QPainter绘制毫米刻度尺和二维码(含订单号、板材批次、生成时间戳)。

关键代码(导出高精度 PNG):

// 设置导出分辨率:1mm = 2px(适配 150dpi 打印需求) const int DPI = 150; const double MM_TO_INCH = 0.0393701; const int SCALE_FACTOR = static_cast<int>(DPI * MM_TO_INCH); // ≈2 QSize exportSize(2440 * SCALE_FACTOR, 1220 * SCALE_FACTOR); QPixmap pixmap(exportSize); pixmap.fill(Qt::white); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing, false); // 关闭抗锯齿!避免尺寸模糊 painter.setRenderHint(QPainter::TextAntialiasing, true); painter.scale(SCALE_FACTOR, SCALE_FACTOR); // 将坐标系缩放为 mm 单位 // 重绘 scene 到 pixmap(scene 已预设为 mm 坐标) scene->render(&painter, QRectF(0,0,2440,1220), QRectF(0,0,2440,1220)); // 添加刻度尺(底部居中) painter.setPen(QPen(Qt::black, 0.5)); for (int x = 0; x <= 2440; x += 100) { painter.drawLine(x, 1220, x, 1220-10); painter.drawText(x-10, 1235, QString::number(x)); } pixmap.save("cutting_plan_20240520_001.png", "PNG");

注意:setRenderHint(Antialiasing, false)是保尺寸精度的关键。曾有团队因开启抗锯齿导致导出图中 1800mm 零件显示为 1799.7px,车间师傅拿游标卡尺一量就发现问题。

3.3 交互设计聚焦产线刚需:不是“拖拽调整”,而是“约束修正”

前端不提供“拖动零件重排”功能——这违背算法最优性,且易引发人为错误。真实交互只有三类:

  • 双击零件:弹出属性编辑框,可修改“是否允许旋转”“纹理方向锁定”“优先使用余料”等约束标志位,修改后触发后台 ILP 重求解;
  • 右键板材空白处:添加“余料标记”,输入余料尺寸(如 600×400),系统自动将其加入下一轮排料的原料池;
  • Ctrl+Shift+E 快捷键:导出当前排料方案为 Excel,包含每块板的零件清单、坐标、尺寸、锯路补偿值,格式严格匹配某品牌数控开料机的.cut导入规范。

这种设计让操作员 3 分钟内就能上手,且杜绝了“以为调好了,其实破坏了全局最优”的玄学操作。


4. 避坑指南:那些让项目延期两周的“小细节”,我们都踩过

4.1 现象:CBC 求解器卡死在 “Optimization status is: 0” ,CPU 占用 100% 持续 10 分钟无响应

原因:未设置求解时间上限,且模型存在病态约束(如M值过大导致系数矩阵条件数 >1e12)。当M设为 10000 时,CBC 在分支定界过程中会产生大量无效节点,陷入“剪枝效率极低”的状态。
解决:在 Python-MIP 中强制设置max_seconds=30,并用m.verbose = 1开启日志。同时将M改为max(plate_w, plate_h) + 100(如 2440+100=2540),实测求解时间从 ∞ 降至 18.3 秒,且最优解不变。

4.2 现象:导出的排料图中,两个零件看似不重叠,但 CNC 机床报错“路径冲突”

原因:模型只约束了零件矩形框不重叠,但未考虑锯路宽度。当两块零件水平相邻时,实际切割路径需留出 1.8mm 锯缝,若模型中它们 x 坐标紧贴(如零件 A 右边界 x=800,零件 B 左边界 x=800),CNC 会试图在同一位置下两刀,触发保护停机。
解决:在重叠约束中引入锯路间隙项。修改非重叠约束为:

# 零件j1右侧与j2左侧间距 ≥ saw_kerf(若水平排列) m += u[i,j1] + parts[j1].w + saw_kerf <= u[i,j2] + M * (1 - z1) m += u[i,j2] + parts[j2].w + saw_kerf <= u[i,j1] + M * (1 - z2)

此改动使模型变量数增加约 12%,但彻底消除 CNC 报错。

4.3 现象:同一订单,白天运行结果利用率 93.2%,晚上运行变成 91.7%,重启程序后恢复

原因:Python-MIP 默认使用 CBC 的随机种子(seed),用于分支定界中的节点选择策略。夜间服务器负载高,CBC 内存分配略有差异,导致随机序列偏移,影响求解路径。虽最终收敛到同一最优解,但设置了max_seconds=30时,可能在不同时间点截断于不同近似解。
解决:在创建模型时固定随机种子:

m = Model(solver_name="CBC") m.seed = 42 # 强制固定 m.max_seconds = 30

加这一行后,100 次重复运行结果完全一致。

4.4 现象:Qt 界面中排料图显示正常,但导出 PDF 后所有文字变模糊,且尺寸缩小 15%

原因:Qt 的QPrinter默认使用系统 DPI,而 Windows 10/11 启用“缩放与布局”(如 125%)时,QPrinter::logicalDpiX()返回值异常(如返回 120 而非 96),导致QPainter坐标换算错误。
解决:导出 PDF 时强制使用物理 DPI:

QPrinter printer; printer.setOutputFormat(QPrinter::PdfFormat); printer.setPaperSize(QSizeF(2440, 1220), QPrinter::Millimeter); printer.setResolution(150); // 强制设为 150dpi,忽略系统缩放 // ... 后续 render 逻辑不变

4.5 现象:客户要求“同订单零件必须用同一批次板材”,但模型分配后出现跨批次混用

原因:模型中只定义了y[i](是否启用第 i 块板),但未对“批次”建立分组约束。当原料库中有 A 批次 5 块、B 批次 7 块板时,算法可能为节省板材,从 A 批用 3 块、B 批用 2 块,违反工艺纪律。
解决:引入批次变量b[k] ∈ {0,1}表示是否启用第 k 批次,再添加批次绑定约束:

# 批次k包含板材集合 plates_in_batch[k] for k in range(num_batches): for i in plates_in_batch[k]: m += y[i] <= b[k] # 若启用板i,则批次k必须启用 # 强制同订单所有零件来自同一启用批次 for j in order_parts: m += xsum(x[i,j] for i in range(N)) <= xsum(b[k] for k in range(num_batches)) # 更关键:添加“至多启用一个批次”的约束(根据客户要求) m += xsum(b[k] for k in range(num_batches)) <= 1

此约束使模型规模增大,但保障了工艺合规性。


5. 余料智能复用:从“切完就扔”到“余料即原料”的闭环策略

5.1 余料建模:不是简单记录尺寸,而是构建“可复用余料库”

传统做法把余料当垃圾,顶多记个“600×400”存数据库。我们的做法是:每次排料完成后,自动扫描每块原料板的未使用区域,生成带坐标的余料多边形。关键不是矩形——因为实际切割后,余料常呈 L 形、T 形(如切掉一个 1800×600 零件后,剩余区域是 2440×1220 减去该矩形,形成两个矩形拼接)。我们用扫描线算法提取余料区域:

def extract_scrap_regions(cut_lines_x, cut_lines_y, plate_w, plate_h): """cut_lines_x/y: 排序后的垂直/水平切割坐标列表,含0和板边界""" regions = [] # 对每个由相邻x、y线围成的网格单元,检查是否全空 for i in range(len(cut_lines_x)-1): for j in range(len(cut_lines_y)-1): x0, x1 = cut_lines_x[i], cut_lines_x[i+1] y0, y1 = cut_lines_y[j], cut_lines_y[j+1] if not is_covered_by_part(x0,x1,y0,y1): # 此区域无零件覆盖 regions.append(ScrapRect(x0,y0,x1-x0,y1-y0)) return regions

每个ScrapRect记录其在原料板上的绝对坐标(如x=0,y=600,w=2440,h=620),并打上“来源批次”“纹理方向”“存放货架号”标签。这些余料自动进入scrap_db表,字段包括id,batch_id,x_mm,y_mm,w_mm,h_mm,grain_dir,shelf_no,created_at。

5.2 余料复用触发机制:三重优先级调度

新订单进来时,系统不直接调用全量原料板,而是按以下顺序匹配余料:

优先级匹配条件示例触发动作
Level 1:精准尺寸复用余料w≥part.w && h≥part.h且纹理方向兼容余料 800×600,零件 780×580(竖纹)直接分配,不进 ILP 求解
Level 2:旋转复用余料w≥part.h && h≥part.w且允许旋转余料 600×800,零件 780×580(无纹向要求)分配并标记“已旋转”
Level 3:拼接复用两块余料可沿长边拼接 ≥ 零件尺寸余料A 1200×400 + 余料B 1200×400 → 拼成 1200×800启动轻量拼接求解器(专用小模型)

Level 3 的拼接模型极简:仅 2 个变量(是否用 A、是否用 B)、1 个约束(w_A + w_B ≥ part.w),求解时间 <0.5 秒。实测某月数据:32% 的订单零件通过 Level 1/2 直接复用,11% 通过 Level 3 拼接复用,整体减少新板材消耗 18.7%。

5.3 余料库的“货架管理”:让仓库员一眼找到可用余料

光有数据库不够,必须解决“人找料”问题。我们在 Qt 界面中嵌入Scrap Warehouse View:

  • 左侧树形控件按shelf_no分组(如 A-01, A-02...);
  • 每个货架节点下,用QTableWidget列出该货架所有余料,列包括:尺寸(w×h)、纹理(↑/→/○)、来源批次、入库时间、可用状态图标(绿色√=可立即用,黄色△=需确认纹理,红色×=尺寸不足);
  • 点击任一余料行,右侧QGraphicsView动态渲染其形状(按比例缩放),并叠加半透明网格(100mm 间隔)辅助目视判断。

关键交互:按住Ctrl键拖拽余料行到订单零件列表,系统自动执行 Level 1 匹配校验,通过则高亮显示“已绑定”,并禁用该余料后续分配。这个设计让仓库员无需懂算法,30 秒内完成余料指派。

从那以后我每次部署新产线,都强制走一遍“余料库压力测试”:导入 500 条历史余料记录,用脚本模拟 100 个随机订单,监控 Level 1/2/3 复用率、平均响应时间、误匹配次数。只要 Level 1 复用率低于 25%,就回溯检查余料采集逻辑——因为这意味着切割坐标解析有偏差,或是锯路补偿没对齐。这套闭环策略让我们在三家不同家具厂落地时,余料复用率稳定在 41%~47%,远超行业平均的 12%。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进