仅在于其形式转载商用
利用AI模型识别您绘制的形状,并将其修正为标准的几何图形。
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>手画图形自动修正 | AI 识别</title>
<style>
* {
box-sizing: border-box;
user-select: none;
}
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(145deg, #f5f7fa 0%, #e9edf5 100%);
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', Roboto, system-ui, -apple-system, sans-serif;
padding: 16px;
}
.card {
max-width: 700px;
width: 100%;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-radius: 48px 48px 32px 32px;
box-shadow: 0 20px 40px rgba(0, 20, 40, 0.2), 0 8px 16px rgba(0, 0, 0, 0.08);
padding: 24px 24px 32px;
transition: all 0.2s;
border: 1px solid rgba(255, 255, 255, 0.5);
}
h1 {
font-weight: 500;
font-size: 1.8rem;
letter-spacing: -0.5px;
color: #1a2639;
margin: 0 0 6px 0;
display: flex;
align-items: center;
gap: 8px;
}
h1 small {
font-size: 1rem;
font-weight: 400;
color: #4a5b6e;
margin-left: 8px;
opacity: 0.8;
}
.sub {
color: #3a4a5e;
margin-top: -4px;
margin-bottom: 20px;
font-weight: 300;
font-size: 0.95rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.canvas-wrapper {
position: relative;
background: #ffffff;
border-radius: 28px;
box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.02);
padding: 8px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.8);
}
#drawCanvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 1 / 1;
background: #ffffff;
border-radius: 24px;
cursor: crosshair;
touch-action: none; /* 避免触摸滚动 */
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 12px 16px;
margin: 16px 0 10px;
}
.btn-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
.btn {
background: #ffffffdd;
border: none;
padding: 10px 22px;
border-radius: 60px;
font-size: 0.95rem;
font-weight: 500;
color: #1e2b3a;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.02);
backdrop-filter: blur(4px);
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255,255,255,0.6);
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
letter-spacing: 0.3px;
}
.btn-primary {
background: #2c3e50;
color: white;
border: 1px solid #1f2e3d;
box-shadow: 0 8px 16px -6px rgba(28, 52, 84, 0.2);
}
.btn-primary:hover {
background: #1f3347;
transform: scale(0.97);
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.btn-primary:active {
transform: scale(0.95);
}
.btn-outline {
background: transparent;
border: 1px solid #b0c0d0;
color: #2c3e50;
}
.btn-outline:hover {
background: #eef3fa;
border-color: #8a9db0;
}
.btn:disabled {
opacity: 0.5;
pointer-events: none;
filter: grayscale(0.3);
}
.badge {
background: #d0dae8;
padding: 4px 14px;
border-radius: 40px;
font-size: 0.8rem;
color: #1a2a3a;
font-weight: 500;
}
.status-area {
background: #f0f4fc;
border-radius: 40px;
padding: 8px 20px;
display: inline-block;
font-size: 0.9rem;
color: #1f3349;
border: 1px solid #dce3ef;
backdrop-filter: blur(2px);
margin-top: 4px;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
margin-top: 12px;
padding: 0 4px;
}
.hint {
color: #4a5f78;
font-size: 0.85rem;
background: #eef3fa;
padding: 6px 16px;
border-radius: 30px;
border: 1px solid #d3deec;
}
@media (max-width: 480px) {
.card { padding: 16px; }
.btn { padding: 8px 16px; font-size: 0.85rem; }
.toolbar { gap: 8px; }
}
</style>
</head>
<body>
<div class="card">
<h1>
✦ 图形修正
<small>手画 → 标准</small>
</h1>
<div class="sub">
<span>🖌️ 绘制任意形状 · 点击修正</span>
<span class="badge" id="shapeLabel">⚡ 等待识别</span>
</div>
<div class="canvas-wrapper">
<canvas id="drawCanvas" width="600" height="600"></canvas>
</div>
<div class="toolbar">
<div class="btn-group">
<button class="btn btn-primary" id="correctBtn">✨ 修正图形</button>
<button class="btn btn-outline" id="clearBtn">🗑️ 清空</button>
</div>
<button class="btn btn-outline" id="undoBtn" style="padding:10px 18px;">↩ 撤销</button>
</div>
<div class="footer">
<span class="status-area" id="statusMessage">✏️ 画一个圆、矩形或三角形</span>
<span class="hint">⚡ 自动识别并修正</span>
</div>
</div>
<script>
(function(){
// ----- 画布设置 -----
const canvas = document.getElementById('drawCanvas');
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
const statusMsg = document.getElementById('statusMessage');
const shapeLabel = document.getElementById('shapeLabel');
// 尺寸 (画布逻辑 600x600)
const W = 600, H = 600;
canvas.width = W;
canvas.height = H;
// ----- 状态 -----
let isDrawing = false;
let points = []; // 当前笔画点集 {x, y}
let strokes = []; // 历史笔画 (用于撤销)
let correctedShape = null; // 修正后的图形数据 {type, points, ...}
// 绘图样式
const STROKE_COLOR = '#1f2e4a';
const STROKE_WIDTH = 3.5;
const CORRECT_COLOR = '#d43f34';
const CORRECT_WIDTH = 3.2;
// ----- 辅助函数 -----
function getCanvasCoords(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width; // 通常 canvas 逻辑尺寸与 CSS 尺寸比例
const scaleY = canvas.height / rect.height;
let clientX, clientY;
if (e.touches) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
e.preventDefault();
} else {
clientX = e.clientX;
clientY = e.clientY;
}
const x = (clientX - rect.left) * scaleX;
const y = (clientY - rect.top) * scaleY;
return { x: Math.min(W, Math.max(0, x)), y: Math.min(H, Math.max(0, y)) };
}
// 重绘全部 (包含修正图形)
function redraw() {
ctx.clearRect(0, 0, W, H);
// 绘制所有保存的笔画 (strokes)
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = STROKE_COLOR;
ctx.lineWidth = STROKE_WIDTH;
for (let stroke of strokes) {
if (stroke.length < 1) continue;
ctx.beginPath();
ctx.moveTo(stroke[0].x, stroke[0].y);
for (let i = 1; i < stroke.length; i++) {
ctx.lineTo(stroke[i].x, stroke[i].y);
}
ctx.stroke();
}
// 绘制当前正在画的点 (如果 isDrawing 但 points 可能为空)
if (points.length > 1 && !isDrawing) {
// 如果不在绘制状态但有点,用于显示最后一段(但通常不会)
}
// 如果有修正图形,绘制修正结果 (覆盖)
if (correctedShape) {
ctx.save();
ctx.strokeStyle = CORRECT_COLOR;
ctx.lineWidth = CORRECT_WIDTH;
ctx.setLineDash([6, 5]);
ctx.shadowColor = 'rgba(200, 60, 40, 0.2)';
ctx.shadowBlur = 10;
const pts = correctedShape.points;
if (pts && pts.length > 0) {
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length; i++) {
ctx.lineTo(pts[i].x, pts[i].y);
}
// 如果是封闭图形 (矩形/三角形/圆用 closePath)
if (correctedShape.type === 'rectangle' || correctedShape.type === 'triangle' || correctedShape.type === 'circle') {
ctx.closePath();
}
ctx.stroke();
}
ctx.restore();
// 更新标签
let label = '🔷 ' + correctedShape.type.charAt(0).toUpperCase() + correctedShape.type.slice(1);
if (correctedShape.type === 'circle') label = '⭕ 圆';
else if (correctedShape.type === 'rectangle') label = '▭ 矩形';
else if (correctedShape.type === 'triangle') label = '△ 三角形';
else if (correctedShape.type === 'line') label = '━ 线段';
shapeLabel.innerText = label;
statusMsg.innerText = '✅ 已修正为 ' + correctedShape.type;
} else {
// 没有修正图形,显示提示
if (strokes.length === 0 && points.length === 0) {
shapeLabel.innerText = '⚡ 等待识别';
statusMsg.innerText = '✏️ 画一个圆、矩形或三角形';
} else {
// 有笔画但无修正
shapeLabel.innerText = '📐 未修正';
statusMsg.innerText = '✏️ 点击 "修正图形" 识别';
}
}
}
// ----- 核心: 识别并修正图形 (基于简化点集) -----
function recognizeAndCorrect() {
// 合并所有笔画为一个点集 (用于识别)
let allPoints = [];
for (let stroke of strokes) {
allPoints = allPoints.concat(stroke);
}
// 加上当前未保存的 points (如果正在画但没保存, 一般不会)
if (points.length > 0) {
allPoints = allPoints.concat(points);
}
// 如果点太少,无法识别
if (allPoints.length < 6) {
statusMsg.innerText = '⚠️ 点太少,请画一个完整形状';
correctedShape = null;
redraw();
return;
}
// 采样降噪:均匀采样最多 80 个点 (提高性能)
let sampled = [];
const step = Math.max(1, Math.floor(allPoints.length / 80));
for (let i = 0; i < allPoints.length; i += step) {
sampled.push(allPoints[i]);
}
if (sampled.length < 4) sampled = allPoints.slice(0, 20);
// 1. 判断形状: 基于凸包 + 角度/距离特征 (简易)
// 使用凸包算法 (Andrew monotone chain) 获取轮廓
function convexHull(points) {
if (points.length < 3) return points.slice();
const sorted = points.slice().sort((a, b) => a.x - b.x || a.y - b.y);
const lower = [];
for (let p of sorted) {
while (lower.length >= 2 && cross(lower[lower.length-2], lower[lower.length-1], p) <= 0) lower.pop();
lower.push(p);
}
const upper = [];
for (let p of sorted.reverse()) {
while (upper.length >= 2 && cross(upper[upper.length-2], upper[upper.length-1], p) <= 0) upper.pop();
upper.push(p);
}
lower.pop();
upper.pop();
return lower.concat(upper);
}
function cross(o, a, b) {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
const hull = convexHull(sampled);
if (hull.length < 3) {
// 如果点太少或退化成线,当作线段
const first = sampled[0], last = sampled[sampled.length-1];
correctedShape = {
type: 'line',
points: [first, last]
};
redraw();
statusMsg.innerText = '📏 识别为线段';
return;
}
// 计算多边形面积 (用于判断)
function polyArea(pts) {
let area = 0;
const n = pts.length;
for (let i = 0; i < n; i++) {
const j = (i+1) % n;
area += pts[i].x * pts[j].y;
area -= pts[j].x * pts[i].y;
}
return Math.abs(area) / 2;
}
const area = polyArea(hull);
// 计算周长
function perimeter(pts) {
let p = 0;
for (let i = 0; i < pts.length; i++) {
const j = (i+1) % pts.length;
p += Math.hypot(pts[i].x - pts[j].x, pts[i].y - pts[j].y);
}
return p;
}
const perim = perimeter(hull);
// 圆形度: 4pi*area / perim^2
const circularity = (4 * Math.PI * area) / (perim * perim + 0.001);
// 判断形状类型
let type = 'line';
let pts = [];
// 如果近似圆 (circularity > 0.75 且 点数>5)
if (circularity > 0.70 && hull.length >= 5) {
type = 'circle';
// 计算圆心和半径 (最小二乘)
let cx = 0, cy = 0;
for (let p of hull) { cx += p.x; cy += p.y; }
cx /= hull.length; cy /= hull.length;
let r = 0;
for (let p of hull) {
r += Math.hypot(p.x - cx, p.y - cy);
}
r /= hull.length;
// 生成圆 (32个点)
const segs = 32;
pts = [];
for (let i = 0; i <= segs; i++) {
const theta = (i / segs) * 2 * Math.PI;
pts.push({ x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) });
}
statusMsg.innerText = '⭕ 识别为圆形';
}
else {
// 判断矩形 / 三角形 : 基于凸包顶点数量 + 角度
const hullLen = hull.length;
// 计算角度 (近似)
function angleAt(p, prev, next) {
const v1 = { x: prev.x - p.x, y: prev.y - p.y };
const v2 = { x: next.x - p.x, y: next.y - p.y };
const dot = v1.x * v2.x + v1.y * v2.y;
const cross = v1.x * v2.y - v1.y * v2.x;
let ang = Math.atan2(cross, dot);
if (ang < 0) ang += Math.PI;
return ang;
}
// 如果 hull 顶点数为 3~5 且 近似直角或锐角
if (hullLen >= 3 && hullLen <= 5) {
// 针对三角形 (3个顶点) 或 矩形 (4个顶点)
// 计算角度
let angles = [];
for (let i = 0; i < hullLen; i++) {
const prev = hull[(i-1+hullLen) % hullLen];
const curr = hull[i];
const next = hull[(i+1) % hullLen];
angles.push(angleAt(curr, prev, next));
}
const avgAngle = angles.reduce((a,b) => a+b, 0) / angles.length;
const minAngle = Math.min(...angles);
const maxAngle = Math.max(...angles);
// 判断三角形:角度范围大,且接近60度
const angleRange = maxAngle - minAngle;
if (hullLen === 3 || (hullLen <= 4 && angleRange > 0.8 && avgAngle > 0.9 && avgAngle < 1.8)) {
type = 'triangle';
// 生成三角形: 取凸包 3 个顶点 (或近似)
if (hullLen === 3) {
pts = hull.slice();
pts.push(hull[0]); // 闭合
} else {
// 如果多于3个点,取三个角度最大的顶点 (最简单)
let sorted = hull.map((p, idx) => ({ p, angle: angles[idx], idx }));
sorted.sort((a,b) => b.angle - a.angle);
const top3 = sorted.slice(0, 3).map(item => item.p);
pts = top3.concat(top3[0]);
}
statusMsg.innerText = '△ 识别为三角形';
}
else if (hullLen >= 4 && hullLen <= 6) {
// 尝试矩形: 角度接近 90° (1.57 rad)
const rightAngleCount = angles.filter(a => a > 1.2 && a < 1.9).length;
if (rightAngleCount >= 2) {
type = 'rectangle';
// 取凸包 4 个顶点,排序 (按角度)
let sorted = hull.map((p, idx) => ({ p, angle: angles[idx], idx }));
sorted.sort((a,b) => a.angle - b.angle);
// 取4个极值点 (实际上取最远点)
const rectPts = [];
// 使用极值: 左右上下
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
for (let p of hull) {
if (p.x < minX) minX = p.x;
if (p.x > maxX) maxX = p.x;
if (p.y < minY) minY = p.y;
if (p.y > maxY) maxY = p.y;
}
const corners = [
{ x: minX, y: minY },
{ x: maxX, y: minY },
{ x: maxX, y: maxY },
{ x: minX, y: maxY }
];
pts = corners.concat(corners[0]);
type = 'rectangle';
statusMsg.innerText = '▭ 识别为矩形';
} else {
// 默认当作线段
const first = sampled[0], last = sampled[sampled.length-1];
pts = [first, last];
type = 'line';
statusMsg.innerText = '📏 识别为线段';
}
} else {
// 线段
const first = sampled[0], last = sampled[sampled.length-1];
pts = [first, last];
type = 'line';
statusMsg.innerText = '📏 识别为线段';
}
} else {
// 默认线
const first = sampled[0], last = sampled[sampled.length-1];
pts = [first, last];
type = 'line';
statusMsg.innerText = '📏 识别为线段';
}