中秋猜灯谜:基于华为云码道零依赖纯 Canvas 手写一个灯会小游戏

中秋猜灯谜:基于华为云码道零依赖纯 Canvas 手写一个灯会小游戏

项目地址:https://atomgit.com/sdf56g99988/mid-autumn-huadeng-riddle

运行方式:双击 index.html,离线即玩,无需安装、无需构建、无需联网。

中秋少不了猜灯谜。这篇文章记录一个「中秋灯会 · 猜灯谜」单页小游戏的完整实现------纯手写 HTML + CSS + JavaScript(Canvas 2D),零依赖、零构建、零后端、零外链 ,所有视觉(灯笼、夜空、明月、烟花)全部由代码绘制,不加载任何一张图片、一个字体文件、一个 CDN 资源。

先看成品的灯会现场:

夜空渐变底色、漫天闪烁繁星、右上角一轮带月晕的明月,下面是 24 盏红灯笼排成几排,在夜风里轻轻摆动、灯芯火光摇曳。每一盏灯笼背后都挂着一道灯谜。


一、玩法:一场能玩的灯会

整个游戏就是一个「逛灯会、猜灯谜」的闭环:

  1. 灯会场景:Canvas 绘制夜空 + 明月 + 繁星 + 24 盏悬挂灯笼,灯笼带摆动与火光摇曳动画。
  2. 点灯笼出题:点击任意一盏灯笼,对应的灯谜以「卷轴」形式从上方垂落展开(带展开动画)。
  3. 答题 :在卷轴输入框写下谜底,回车或点「提交」。
    • 答对:该灯笼「点亮变金」+ 金光晕 + 烟花粒子特效,显示解析(谜底的扣合逻辑),计入得分。
    • 答错:灯笼轻晃 + 提示「再想想」,不扣分,可重试。
  4. 提示:同一题连续答错 3 次后「提示」按钮解锁,每用一次消耗一次全局提示机会,按「由弱到强」渐进出示 1~3 条提示。
  5. 计分:每题满分 100,按用时阶梯给分(≤10s 满分 / ≤30s 八成 / ≤60s 六成 / >60s 四成),再乘以提示扣分(每用一次提示扣两成)。
  6. 通关:24 题全部答完弹出「灯会通关」结算面板------总分、用时、答对数、评级(灯谜状元 / 榜眼 / 探花 / 再接再厉)+ 星级。

点开灯笼,卷轴垂落展开的样子:

答对一题,灯笼点亮、烟花绽放、解析展示:

答对后回到灯会,那盏灯笼已经变成金色:


二、灯谜引擎:把「判定」做成纯函数

这个小游戏最值得讲的不是画面,而是引擎与界面彻底解耦的设计。整个游戏拆成三层:

复制代码
题库(riddles.js)   → 只提供数据,零逻辑
引擎(engine.js)    → 归一化 + 判定 + 状态演算 + 计分,纯函数,无 DOM
渲染(main.js)      → Canvas 渲染与交互,只读状态

题库和引擎都用 UMD 包装(兼容 CommonJS / AMD / 全局挂载),因此同一份 engine.js 既能在浏览器里 <script> 直接引入跑游戏,也能在 Node 下 require 跑单元测试------这是它能做到「file:// 双击即玩」又能「npm test 全绿」的关键。

1. 答案归一化:让玩家怎么答都不算「冤」

玩家输入谜底的方式千奇百怪:可能带空格、用全角、写繁体、大小写混着来。如果直接 === 比对,「月餅」和「月饼」就会判错,体验很差。引擎的 normalizeAnswer 做四步归一:

js 复制代码
function normalizeAnswer(input) {
  if (input === null || input === undefined) return '';
  var s = String(input);
  s = toHalfWidth(s);   // 全角 → 半角(含全角空格)
  s = trimAll(s);       // 去掉所有空白
  s = t2s(s);           // 常见繁体 → 简体(白名单)
  s = s.toLowerCase();  // 英文 / 拼音大小写容错
  return s;
}

其中「全角转半角」利用的是 Unicode 的编码规律:全角字符 !~~(U+FF01--U+FF5E)与对应半角字符的码点恰好相差 0xFEE0,所以一个 charCodeAt 偏移就能批量转;全角空格(U+3000)单独处理:

js 复制代码
function toHalfWidth(s) {
  return String(s)
    .replace(/[\uFF01-\uFF5E]/g, function (ch) {
      return String.fromCharCode(ch.charCodeAt(0) - 0xFEE0);
    })
    .replace(/\u3000/g, ' '); // 全角空格 → 半角空格
}

function trimAll(s) {
  return String(s).replace(/\s+/g, '');
}

繁简转换不引入任何库,用一张「常见字白名单」映射表逐字替换------只收录谜底里真正会出现的那批字(餅→饼、燈→灯、籠→笼、煙→烟、圓→圆、團→团......),够用就好,避免为了一张全量对照表拖进几百 KB 数据:

js 复制代码
var T2S = {
  '餅': '饼', '燈': '灯', '籠': '笼', '煙': '烟', '吳': '吴', '剛': '刚',
  '圓': '圆', '裡': '里', '個': '个', '們': '们', '這': '这', '對': '对',
  '為': '为', '題': '题', '節': '节', '樂': '乐', '團': '团', '滿': '满',
  // ...只收谜底相关字
};

function t2s(s) {
  return String(s).split('').map(function (c) {
    return T2S[c] || c;
  }).join('');
}

判定时先与标准答案精确比对,再遍历每题可配置的 aliases 同义/俗称白名单。关键在于:判定结果带着原因 返回 { correct, reason },reason ∈ exact | alias | wrong | empty,UI 可以据此给出不同的反馈文案(比如 alias 命中时提示「也算对,标准写法是......」):

js 复制代码
function checkAnswer(input, riddle) {
  var norm = normalizeAnswer(input);
  if (!norm) return { correct: false, reason: 'empty' };
  if (norm === normalizeAnswer(riddle.answer)) {
    return { correct: true, reason: 'exact' };
  }
  var aliases = riddle.aliases || [];
  for (var i = 0; i < aliases.length; i++) {
    if (norm === normalizeAnswer(aliases[i])) {
      return { correct: true, reason: 'alias' };
    }
  }
  return { correct: false, reason: 'wrong' };
}

注意 aliases 在比对前也要过一遍 normalizeAnswer------否则白名单里写「月饼」、玩家输入「月餅」时,归一化只对玩家输入生效,两边还是对不上。这是一个容易漏掉的细节。

2. 状态演算:不可变更新,渲染层只读

游戏状态(每盏灯笼是否已答对、尝试了几次、用了几条提示、总分、起止时间)全部放在一个纯数据对象里,所有变更都通过引擎的纯函数返回新状态 ,而不是原地修改。submitAnswer 是整个引擎的核心,看它一个函数就能看懂全部设计:

js 复制代码
function submitAnswer(state, riddle, input, now) {
  var next = cloneState(state);                 // ① 先克隆,不可变更新
  var l = findLantern(next, riddle.id);
  if (!l)      return { state: next, result: { correct: false, reason: 'notfound', /*...*/ } };
  if (l.solved) return { state: next, result: { correct: true,  reason: 'solved',   /*...*/ } };

  var t = typeof now === 'number' ? now : Date.now();  // ② 时间可注入,便于单测
  l.attempts += 1;
  var check = checkAnswer(input, riddle);
  if (check.correct) {
    l.solved = true;
    var timeMs = l.openTime != null ? Math.max(0, t - l.openTime) : 0;
    l.score = scoreAnswer(timeMs, l.hintsUsed);  // ③ 用时越短、提示越少,分越高
    next.totalScore += l.score;
    if (next.lanterns.every(function (x) { return x.solved; })) {  // ④ 全部答完→通关
      next.finished = true;
      next.endTime = t;
      next.currentId = null;
    }
  }
  return { state: next, result: { correct: check.correct, reason: check.reason,
           score: l.solved ? l.score : 0, attempts: l.attempts, finished: next.finished } };
}

几个设计点:

  1. 不可变更新 :先 cloneState 再改副本,渲染层拿到的 state 永远是只读快照,可预测、可回放、可单测。
  2. 时间可注入 :now 允许从外部传入,单测里可以精确控制「用时」,验证计分阶梯,而不必真的去 sleep。
  3. 重复提交防护 :已答对的题再提交直接返回 reason: 'solved',不会重复加分。
  4. 通关检测内聚 :every(solved) 就地在引擎里判断,UI 只读 finished 标志。

createGameState / openRiddle / submitAnswer / requestHint / summarize 这一组函数覆盖了从开局到结算的全部状态流转。

3. 计分与评级

计分公式 score = round(100 × 时长系数 × 提示系数)------答得越快、用提示越少,分越高。时长系数按阶梯取,提示每用一次扣两成、扣到零为止:

js 复制代码
function scoreAnswer(timeMs, hintsUsed) {
  var base = 100;
  var sec = (timeMs || 0) / 1000;
  var timeFactor;
  if (sec <= 10)      timeFactor = 1.0;
  else if (sec <= 30) timeFactor = 0.8;
  else if (sec <= 60) timeFactor = 0.6;
  else                timeFactor = 0.4;
  var hintFactor = Math.max(0, 1 - (hintsUsed || 0) * 0.2);
  return Math.round(base * timeFactor * hintFactor);
}

评级看「答对比例」和「得分比例」两个维度:全对且得分 ≥90% 才够格「灯谜状元」(3 星),光蒙对但拖太久、提示用光也拿不到满分评级:

js 复制代码
function gradeResult(totalScore, maxScore, correctCount, totalCount) {
  var ratio = totalCount > 0 ? correctCount / totalCount : 0;
  var scoreRatio = maxScore > 0 ? totalScore / maxScore : 0;
  if (ratio >= 1 && scoreRatio >= 0.9) return { title: '灯谜状元', stars: 3 };
  if (ratio >= 0.8) return { title: '灯谜榜眼', stars: 2 };
  if (ratio >= 0.6) return { title: '灯谜探花', stars: 1 };
  return { title: '再接再厉', stars: 0 };
}

4. 提示门槛:把「解锁条件」收进引擎

提示不是想用就用------要连续答错 3 次才解锁,且消耗全局提示机会。这个门槛逻辑也放在引擎里,用 canRequestHint 单独判断,UI 只负责拿结果去 disable 按钮:

js 复制代码
function canRequestHint(state, riddle) {
  var l = findLantern(state, riddle.id);
  if (!l || l.solved) return false;         // 已答对不需提示
  if (l.attempts < 3) return false;         // 连错 3 次才解锁
  if (state.hintsRemaining <= 0) return false; // 全局机会耗尽
  var hints = riddle.hints || [];
  if (l.hintsUsed >= hints.length) return false; // 该题提示已用完
  return true;
}

把「为什么不能用提示」拆成四个明确的分支,单测可以逐条断言;requestHint 里再返回 reason ∈ need-more-attempts | no-hint-left | no-more-hint | ok,UI 据此给出精准提示文案,而不是一句笼统的「暂不可用」。

5. 题库:数据驱动,扩充零成本

内置 24 道原创灯谜(字谜 9 / 成语谜 6 / 中秋意象谜 9),每题是一个结构化对象:

js 复制代码
{
  id: 'q01',
  category: '字谜',           // 谜面分类
  answerType: '字',           // 谜底类型:字 / 成语 / 事物
  question: '一月一日非今天',  // 谜面
  answer: '明',               // 谜底
  aliases: [],                // 同义/俗称白名单
  analysis: '"日"与"月"相合为"明"......',  // 解析(扣合逻辑)
  hints: ['...', '...', '...']       // 1~3 条渐进提示,由弱到强
}

想加题?往 riddles 数组里追加一个同结构对象即可,引擎和 UI 一行都不用改。


三、Canvas 渲染:手写每一帧

画面没有任何素材,全是 Canvas 2D 一笔一画画出来的:

  • 夜空:纵向渐变底色 + 随机分布、各自以不同频率闪烁的繁星。
  • 明月:月晕(径向渐变光晕)+ 月体 + 暗斑。
  • 灯笼:悬挂线、顶盖、椭圆主体、竖纹、灯芯火光(摇曳)、流苏、谜字标记;每盏灯有独立的摆动相位与速度,答对后叠加金光晕。
  • 烟花:答对瞬间从灯笼处炸开 72 个粒子,带重力下落与淡出。
  • 卷轴 :木质轴头 + 纸身,展开用 scale + translateY 动画。
  • 高 DPI 适配 :按 devicePixelRatio 缩放画布,Retina 屏不模糊;布局随窗口响应式重排,也支持触摸点灯笼。

主循环是标准的 requestAnimationFrame 驱动,update(dt) 推进物理(摆动、火光、烟花粒子),render(t) 重绘每一帧。dt 做了封顶(Math.min(60, ...)),防止切后台再切回来时物理量暴跳:

js 复制代码
function loop(t) {
  if (!lastFrame) lastFrame = t;
  var dt = Math.min(60, t - lastFrame);  // 封顶,避免切后台后 dt 过大
  lastFrame = t;
  update(dt, t);
  render(t);
  requestAnimationFrame(loop);
}

1. 高 DPI 适配:一个 setTransform 解决模糊

Retina 屏上 Canvas 默认会糊。解法是把画布像素尺寸放大 devicePixelRatio 倍,再用 setTransform 把坐标系缩回去------之后所有绘制代码照常按 CSS 像素写,不用管物理像素:

js 复制代码
function resize() {
  dpr = window.devicePixelRatio || 1;
  W = window.innerWidth;
  H = window.innerHeight;
  canvas.width  = Math.floor(W * dpr);   // 物理像素放大
  canvas.height = Math.floor(H * dpr);
  canvas.style.width  = W + 'px';        // CSS 尺寸不变
  canvas.style.height = H + 'px';
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 坐标系缩放,之后按 CSS 像素绘制
  layoutStars();
  layoutMoon();
  if (state) layoutLanterns();
}

2. 画一盏灯笼:摆动 + 摇曳 + 点亮

一盏灯笼由悬挂线、顶盖、椭圆灯身、竖纹、灯芯火光、流苏、谜字拼成,全部即时绘制。核心是两个随时间变化的量------摆动 (正弦)和火光摇曳(正弦扰动半径)。答对后灯身从红色渐变切换为金色渐变:

js 复制代码
function drawLantern(l, t) {
  var ls = findLanternState(l.riddleId);
  var solved = ls && ls.solved;
  var swingX = Math.sin(t * 0.001 * l.swingSpeed + l.swing) * 5;   // 夜风摆动
  var shakeX = l.shake > 0 ? Math.sin(t * 0.05) * l.shake * 9 : 0; // 答错时的抖动
  var x = l.x + swingX + shakeX, y = l.y;
  var w = LANTERN_W, h = LANTERN_H;

  // 灯身:横向线性渐变,答对切金色
  var grad = ctx.createLinearGradient(x - w / 2, y, x + w / 2, y);
  if (solved) {
    grad.addColorStop(0, '#9c6f12');
    grad.addColorStop(0.5, '#ffd76a');
    grad.addColorStop(1, '#9c6f12');
  } else {
    grad.addColorStop(0, '#7a1f15');
    grad.addColorStop(0.5, '#e74c3c');
    grad.addColorStop(1, '#7a1f15');
  }
  ctx.fillStyle = grad;
  ctx.beginPath();
  ctx.ellipse(x, y, w / 2, h / 2, 0, 0, Math.PI * 2);
  ctx.fill();

  // 灯芯火光:半径随正弦摇曳,未答对才画
  if (!solved) {
    var flicker = 0.82 + 0.18 * Math.sin(t * 0.02 + l.swing);
    var fireR = w * 0.20 * flicker;
    var fg = ctx.createRadialGradient(x, y, 0, x, y, fireR * 2.1);
    fg.addColorStop(0, 'rgba(255,242,190,0.9)');
    fg.addColorStop(0.5, 'rgba(255,180,80,0.45)');
    fg.addColorStop(1, 'rgba(255,120,40,0)');
    ctx.fillStyle = fg;
    ctx.beginPath();
    ctx.arc(x, y, fireR * 2.1, 0, Math.PI * 2);
    ctx.fill();
  }
  // ...顶盖 / 竖纹 / 流苏 / 谜字略
}

每盏灯用 Math.random() 生成独立的 swing(相位)与 swingSpeed(速度),所以 24 盏灯不会整齐划一地摆,而是错落有致------这是让画面「活」起来的关键。答错时的抖动复用了同一个 x 偏移:给 l.shake 置 1,再在 update 里随时间衰减回 0,就得到了一次「晃一下就停」的反馈。

3. 烟花:一个极简粒子系统

答对瞬间从灯笼处炸开 72 个粒子,每个粒子有初速度、重力加速度、空气阻尼和生命值。没有引入任何物理引擎,就是几条运动学公式:

js 复制代码
function spawnFireworks(x, y) {
  var colors = ['#FFD700', '#FF8C00', '#FF4500', '#FFB347', '#FFF8DC', '#FF6B6B', '#FFE066'];
  for (var i = 0; i < 72; i++) {
    var a = Math.random() * Math.PI * 2;          // 随机方向
    var sp = 1 + Math.random() * 4.6;             // 随机速率
    fireworks.push({
      x: x, y: y,
      vx: Math.cos(a) * sp,
      vy: Math.sin(a) * sp - 1.6,                 // 整体向上偏一点
      life: 1,
      color: colors[Math.floor(Math.random() * colors.length)]
    });
  }
}

function updateFireworks() {
  for (var i = fireworks.length - 1; i >= 0; i--) {
    var p = fireworks[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.08;     // 重力
    p.vx *= 0.99;     // 阻尼
    p.life -= 0.012;  // 生命值衰减
    if (p.life <= 0) fireworks.splice(i, 1);      // 死了就移除
  }
}

function drawFireworks() {
  for (var i = 0; i < fireworks.length; i++) {
    var p = fireworks[i];
    ctx.globalAlpha = Math.max(0, p.life);        // 用生命值做透明度 → 淡出
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x, p.y, 2.2, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
}

vy 初始减 1.6 让烟花先向上蹿,再被重力 vy += 0.08 拉回来,形成抛物线;透明度直接绑在 life 上,粒子越接近消亡越淡。整套系统三个函数、不到 40 行,就是一朵像样的烟花。


四、工程化:5 个 commit,全程可验证

整个项目严格按语义化分 5 个 commit 推进,每个 commit 都能独立检出运行:

# Commit 内容 验证
1 chore 仓库初始化(.gitignore / package.json / README / 目录骨架) ---
2 feat(domain) 题库 riddles.js + 判定引擎 engine.js(UMD 纯函数) Node 验证通过
3 test 引擎单测 test/engine.test.js(node:test + assert,零依赖) npm test 50/50 全绿
4 feat(ui) Canvas 灯会三件套 index.html + style.css + main.js file:// 实测 8/8,控制台零报错
5 docs 完善 README(玩法 / 操作 / 题库 / 引擎 / 技术要点 / 运行 / 测试) ---

单测:50 个用例全绿

测试用 Node 内置的 node:test + node:assert,不引入任何测试框架,覆盖:题库完整性(题数 / 字段 / 类型 / id 唯一 / 分类齐全)、归一化(空白 / 全角 / 繁简 / 大小写 / 空值)、判定(精确 / 别名 / 繁体 / 答错 / 空答案)、计分阶梯、评级、渐进提示、洗牌可复现、状态演算(不可变 / 重复答 / 重复开题)、提示阶梯与机会耗尽、通关结算。

随机洗牌是单测的老大难------带 Math.random 的函数没法断言确定结果。引擎的解法是允许注入随机源 :shuffle(arr, random) 接受一个可选的 random 函数,游戏运行时默认用 Math.random,测试时传入一个固定序列,就能断言「同样的输入必然得到同样的打乱结果」:

js 复制代码
function shuffle(arr, random) {
  var rnd = typeof random === 'function' ? random : Math.random; // 可注入
  var a = arr.slice();                          // 不改原数组
  for (var i = a.length - 1; i > 0; i--) {
    var j = Math.floor(rnd() * (i + 1));        // Fisher-Yates
    var tmp = a[i]; a[i] = a[j]; a[j] = tmp;
  }
  return a;
}

于是测试可以这样写:注入一个永远返回 0 的「随机」源,Fisher-Yates 的行为就完全确定,断言它既不打乱元素集合、又确实改变了顺序:

js 复制代码
const test = require('node:test');
const assert = require('node:assert');
const E = require('../src/engine.js');

test('shuffle 可复现:注入固定随机源', () => {
  const input = ['a', 'b', 'c', 'd', 'e'];
  const out = E.shuffle(input, () => 0);        // 注入确定性随机源
  assert.deepStrictEqual([...out].sort(), [...input].sort()); // 元素不重不漏
  assert.notDeepStrictEqual(out, input);        // 顺序确实变了
  assert.deepStrictEqual(input, ['a', 'b', 'c', 'd', 'e']);   // 原数组未被修改
});

test('答对计分:越快越高,提示扣分', () => {
  assert.strictEqual(E.scoreAnswer(5 * 1000, 0), 100);  // 5 秒内、无提示 → 满分
  assert.strictEqual(E.scoreAnswer(20 * 1000, 0), 80);  // 30 秒内 → 八成
  assert.strictEqual(E.scoreAnswer(5 * 1000, 2), 60);   // 满分但用 2 次提示 → 六成
});

本地克隆下来跑一遍:

复制代码
ℹ tests 50
ℹ pass 50
ℹ fail 0
ℹ duration_ms 401.31

file:// 实测

file:// 协议下没有模块系统,所以领域脚本全部用 UMD 挂到全局(window.MidAutumnRiddles / window.MidAutumnEngine),main.js 直接用全局引用。端到端实测 8/8 步骤全绿:渲染、点灯笼开题、答错反馈、请求提示、答对特效、重新开局,控制台零报错、零 pageerror。


五、过程剪影

这次开发由 AI 编程助手在云端沙箱里完成,从理解需求到推送上线一气呵成。几个关键节点:

任务下发后,助手先复述确认理解,并就「远端仓库推送方式」「仓库可见性」做了澄清:

设计题库时逐题校验扣合逻辑(比如「一月一日非今天 → 明」「半部春秋 → 秦」),确保每道题都经得起推敲:

五个 commit 全部完成、本地验证通过后,汇总了完整进度:

推送阶段还踩了两个有意思的坑:

坑一:lantern 是平台敏感词。 原计划仓库名叫 mid-autumn-lantern-riddle,创建远端时被拒("项目名称存在违规内容")。助手做了对照实验,确认 autumn、mid 都能正常创建、唯独 lantern 触发拦截,最后改用 huadeng(花灯)------语义上反而更贴切,中秋赏的本就是花灯。

坑二:云端沙箱的环境重置。 推送时发现沙箱在工具调用之间会重置 .git 的 refs,把 main 分支打回只有 1 个 commit。解法是先把 5 个 commit 的引用固定下来,再用一条原子命令 git reset --hard <完整commit> && git push origin main 在同一个 shell 调用内完成「恢复 + 推送」,避免中途再被重置:

最终 git push 输出 a16f4ed..5602b82 main -> main,远端 main 指向包含全部 5 个 commit 的最新节点,仓库公开可访问:

六、结语

这个项目没有用到任何框架和构建工具,却把「可玩性」和「工程质量」都做了出来:

  • 对玩家,它是一场能点亮、会放烟花、有评级的中秋灯会;
  • 对开发者,它是一次「纯函数引擎 + 数据驱动 + UMD 模块 + 零依赖单测」的练手------证明了不依赖任何第三方库,也能写出结构清晰、可测试、可扩展的前端代码。

中秋将至,祝你猜谜连中,个个状元。

相关推荐
yunlaodacom10 小时前
华为云国际站服务代理商:BMS数据库延迟突然升高?磁盘I/O、网络与CPU排查方法
网络·数据库·华为云
GitCode官方12 小时前
直播预告:华为云码道实战|Agentic DevOps 与昇腾 CANN 算子 AI 开发
人工智能·华为云·devops·atomgit
晨间晓2 天前
图片里的高频和低频是什么?用华为云码道开发 OpenCV 傅里叶频谱分析工具
opencv·计算机视觉·华为云
晨间晓2 天前
图片里的阴影能不能减弱?用华为云码道开发 OpenCV 光照校正工具
人工智能·opencv·华为云
熊猫钓鱼>_>2 天前
Kotlin Multiplatform for OpenHarmony 实战:kotlinx-serialization JSON 适配
开发语言·华为·kotlin·华为云·json·harmonyos·code
晨间晓2 天前
图片里的细小噪点怎么平滑?用华为云码道开发 OpenCV 图像滤波对比工具
人工智能·opencv·华为云
偷吃米饭2 天前
图片特征怎么描述?用华为云码道开发 OpenCV ORB 特征提取工具
人工智能·opencv·华为云
晨间晓3 天前
图片里的关键点怎么找?用华为云码道开发 OpenCV FAST 特征点检测工具
人工智能·opencv·华为云
晨间晓3 天前
二值图里的噪点怎么去掉?用华为云码道开发 OpenCV 形态学操作工具
人工智能·opencv·华为云
晨间晓3 天前
图片里的角点在哪里?用华为云码道开发 OpenCV Harris 角点检测工具
人工智能·opencv·华为云