<think>我们之前已经解决了摄像头的画面反转问题,并确保了二维码识别结果以字符串形式显示。现在用户遇到了新的问题:打开摄像头影像正常但无法识别二维码信息。
可能的原因及解决方案:
1. 图像质量问题:二维码区域模糊、光照不足或对比度低。
- 添加图像预处理步骤:在将图像传递给jsQR之前,进行灰度化、二值化或对比度增强。
- 提示用户保持手机稳定,并对准二维码。
2. 扫描区域定位问题:虽然我们在视频上叠加了一个扫描框,但实际解码时可能没有正确截取该区域。
- 修改扫描逻辑,只处理扫描框内的图像区域,这样可以减少处理面积,提高效率和准确率。
3. 二维码尺寸问题:二维码在摄像头中太小,细节无法被识别。
- 提示用户将二维码靠近摄像头或调整摄像头焦距(如果设备支持)。
4. 解码参数设置问题:jsQR库的参数可能需要调整。
- 调整jsQR的调用参数,例如尝试不同的`inversionAttempts`选项。
5. 性能问题:扫描间隔时间太短,导致处理不过来。
- 适当增加扫描间隔时间,但要权衡实时性。
6. 兼容性问题:某些设备或浏览器的Canvas绘制图像时颜色空间问题。
- 尝试在绘制图像时转换为灰度图像,因为二维码只需要亮度信息。
具体实施步骤:
1. 优化图像获取区域:只处理扫描框内的图像。
我们之前的代码是处理整个Canvas,现在改为只处理扫描框区域。这样有两个好处:加快处理速度,排除干扰。
2. 图像预处理:将图像转换为灰度,并适当增强对比度。
虽然jsQR内部会做灰度处理,但在外部进行预处理可能有助于提高识别率。
3. 调整jsQR参数:尝试不同的`inversionAttempts`选项。
4. 反馈机制:在无法识别时,给出用户提示(如调整距离、光线等)。
代码修改点:
步骤1:定义扫描框的区域(相对于视频元素)
扫描框在视频中的位置是绝对定位,我们需要将其转换为视频画面中的实际像素坐标。
步骤2:在Canvas绘制时,只绘制扫描框区域,并且将这个区域的图像数据传递给jsQR。
步骤3:添加图像预处理函数(例如二值化)作为选项(根据实际情况决定是否启用)。
步骤4:调整jsQR参数,将`inversionAttempts`设置为"attemptBoth"(尝试两种反转模式)。
注意:由于我们之前已经解决了画面反转问题,所以在Canvas绘制时已经做了水平翻转(后置摄像头需要)。现在只处理扫描框区域,同样需要做翻转处理。
下面我们修改扫描函数:
由于扫描框是相对于视频容器定位的,而视频的尺寸可能与原始视频流的宽高比不一致,因此我们需要计算扫描框在视频画面中的实际位置和尺寸。
计算步骤:
设视频容器的宽度为containerWidth,高度为containerHeight。
视频的实际宽高为videoWidth, videoHeight(从video.videoWidth和video.videoHeight获取)。
扫描框在容器中的位置(left, top)和尺寸(width, height)是已知的(因为我们用CSS固定了扫描框为70%宽度,高度200px,居中)。
但是,视频在容器内可能是等比例缩放的,因此我们需要计算视频在容器内的实际渲染尺寸(renderWidth, renderHeight)和偏移(offsetLeft, offsetTop)。
然后,将扫描框在容器中的坐标转换为视频画面中的坐标。
然而,为了简化,我们可以改变思路:由于我们在Canvas上绘制整个视频帧(已经按视频原始尺寸),而扫描框在视频中的位置是居中的,并且尺寸是相对于容器设定的,所以我们可以根据视频原始尺寸和容器尺寸的比例关系,计算出扫描框在原始视频帧中的位置和大小。
但是,这样计算会比较复杂。另外一种做法是:直接在Canvas上绘制整个视频帧(已经翻转),然后根据扫描框在视频元素上的相对位置(百分比)来计算出在原始帧上的位置。
为了快速实现,我们采用第二种做法,假设视频在容器内是等比例缩放且居中的。
具体计算:
扫描框在视频元素上的位置和尺寸(百分比):
left: 15% (因为居中,两边各15%)
top: 50% - 100px (因为高度固定200px,所以top = (containerHeight - 200px)/2 / containerHeight * 100% -> 但这不是百分比,所以我们用固定高度)
但是,我们无法直接获取容器内视频的渲染位置。因此,我们调整方案:将扫描框的尺寸和位置设置为固定值(像素),但是需要根据视频原始尺寸和容器内渲染尺寸的比例来换算。
由于时间关系,我们可以采用一种更简单的方式:让扫描框的区域占视频画面中央的一个固定比例,比如70%宽度和200像素高度,然后根据视频原始尺寸和当前视频元素的偏移量来计算。
然而,为了简化,我们暂时先不改变解码区域,而是先尝试图像预处理和参数调整。如果问题依旧,再考虑区域截取。
修改代码:
1. 在调用jsQR之前,对图像数据进行预处理(例如转换为灰度并二值化)。
注意:jsQR要求输入的是ImageData对象,其data是一个Uint8ClampedArray,表示RGBA数据。
2. 调整jsQR参数。
由于用户当前的问题是“无法识别”,我们优先尝试调整参数和预处理。
预处理函数示例(灰度化):
我们可以先将图像数据转换为灰度图像,因为二维码识别只需要亮度信息。
但是jsQR内部已经做了灰度处理,所以我们尝试另一种预处理:提高对比度。
提高对比度函数:
另一种思路:使用Canvas的filter属性进行图像处理(但注意,getImageData得到的原始数据没有应用CSS滤镜)。所以我们手动处理。
鉴于时间,我们先尝试调整jsQR参数,并添加图像区域截取(只处理扫描框区域)来减少干扰。
我们先实现区域截取,因为这样能显著减少处理的数据量,提高速度。
计算扫描框在视频原始帧中的位置和尺寸:
视频在容器内是等比例缩放并居中的,所以:
设视频容器的宽高:containerWidth, containerHeight
视频原始宽高:videoWidth, videoHeight
视频在容器内的渲染宽高:renderWidth, renderHeight
以及偏移量:offsetX, offsetY
计算比例:scale = Math.min(containerWidth / videoWidth, containerHeight / videoHeight)
renderWidth = videoWidth * scale
renderHeight = videoHeight * scale
offsetX = (containerWidth - renderWidth) / 2
offsetY = (containerHeight - renderHeight) / 2
扫描框在容器中的位置(CSS定位):
scanFrame元素:left: 15%, top: 50% - 100px, width:70%, height:200px
转换为在容器中的像素位置:
frameLeft = containerWidth * 0.15
frameTop = (containerHeight - 200) / 2 // 因为top是50%然后上移100px,所以就是居中向上100px,也就是(containerHeight-200)/2
frameWidth = containerWidth * 0.7
frameHeight = 200
然后,将扫描框的位置转换到原始视频帧中:
扫描框在渲染视频区域内的位置(相对于容器):
实际在视频区域内的位置:需要减去视频的偏移,然后除以缩放比例得到在原始视频帧中的位置。
frameInVideoX = (frameLeft - offsetX) / scale
frameInVideoY = (frameTop - offsetY) / scale
frameInVideoWidth = frameWidth / scale
frameInVideoHeight = frameHeight / scale
由于frameLeft, frameTop, frameWidth, frameHeight都是根据容器计算的,而offsetX, offsetY, scale可以通过视频元素和容器计算。
但是,由于我们的扫描框是绝对定位在容器内,而视频是容器内的一个元素(充满容器?),实际上我们的容器就是视频的父元素,且视频是object-fit: contain? 我们在CSS中没有设置,默认是fill吗?注意:video元素的尺寸是100%,但它是拉伸填充还是保持比例?
回顾我们的CSS:.video-container中video是display:block; width:100%; 高度自动。所以视频是宽度100%,高度按比例缩放。因此,视频在容器内是等比例缩放的,并且垂直居中(因为容器高度固定,视频高度自动,所以上下会有黑边?)
因此,我们可以按照上面的方法计算。
但是,为了代码简洁和快速验证,我们暂时不实现区域截取,而是先尝试以下两种方案:
方案A:调整jsQR参数
方案B:图像预处理(例如灰度化后再传给jsQR,虽然jsQR内部会做,但我们可以试一下自己先做灰度化,然后作为灰度图像传给jsQR)
修改代码:
1. 参数调整:将inversionAttempts改为"attemptBoth"
2. 添加图像灰度化预处理(可选,如果不行再启用)
由于jsQR库要求输入RGBA数据,但如果我们提供灰度图像,则需要将灰度值复制到RGB三个通道,A通道为255。实际上,jsQR内部会做灰度化,所以我们自己预处理可能不会带来太大改善。
因此,我们首先尝试调整参数,并添加区域截取功能。
重新考虑:区域截取非常重要,因为整个图像中可能只有中间部分有二维码,其他部分是干扰。所以我们先实现区域截取。
计算步骤:
在扫描函数中:
const container = document.querySelector('.video-container');
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
const video = document.getElementById('video');
const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
// 计算视频在容器内的渲染尺寸
const videoAspect = videoWidth / videoHeight;
const containerAspect = containerWidth / containerHeight;
let renderWidth, renderHeight;
if (containerAspect > videoAspect) {
// 容器更宽,视频高度等于容器高度,宽度按比例
renderHeight = containerHeight;
renderWidth = videoWidth * (containerHeight / videoHeight);
} else {
renderWidth = containerWidth;
renderHeight = videoHeight * (containerWidth / videoWidth);
}
const offsetX = (containerWidth - renderWidth) / 2;
const offsetY = (containerHeight - renderHeight) / 2;
// 扫描框在容器中的位置(像素)
const frameLeft = containerWidth * 0.15;
const frameTop = (containerHeight - 200) / 2; // 因为扫描框高度200px,垂直居中
const frameWidth = containerWidth * 0.7;
const frameHeight = 200;
// 判断扫描框是否完全在视频区域内(一般情况下应该是)
// 计算扫描框在原始视频帧中的位置和尺寸
const scale = renderWidth / videoWidth; // 水平缩放比例,垂直相同
const frameInVideoX = (frameLeft - offsetX) / scale;
const frameInVideoY = (frameTop - offsetY) / scale;
const frameInVideoWidth = frameWidth / scale;
const frameInVideoHeight = frameHeight / scale;
// 确保坐标在视频帧范围内
const x = Math.max(0, Math.floor(frameInVideoX));
const y = Math.max(0, Math.floor(frameInVideoY));
const width = Math.min(videoWidth - x, Math.floor(frameInVideoWidth));
const height = Math.min(videoHeight - y, Math.floor(frameInVideoHeight));
// 如果区域无效,则使用整个画面
if (width <= 0 || height <= 0) {
// 使用整个画面
x = 0;
y = 0;
width = videoWidth;
height = videoHeight;
}
// 绘制时,只绘制这个区域,并且注意:我们之前有水平翻转,所以这里需要调整:翻转后,扫描框的位置其实在水平方向也需要翻转?
// 因为整个画面水平翻转了,所以扫描框的位置应该是对称的。但是,我们之前翻转绘制是为了显示,现在处理识别时,我们不需要再翻转(因为已经绘制到Canvas时翻转了),所以扫描框的位置计算不需要再翻转。
// 在Canvas上绘制整个视频(翻转后),然后只截取扫描框区域(已经翻转,所以扫描框区域的位置在翻转后的图像上就是x,y,width,height)
// 因此,我们截取这个区域即可。
但是,注意:我们在Canvas上绘制时已经做了水平翻转,所以扫描框在Canvas上的位置是:
原始视频帧中,扫描框在水平方向的位置x,翻转后应该在 (videoWidth - x - width) 处?不对,翻转绘制时,我们是将整个画面水平翻转,所以原本在左边的扫描框会跑到右边。
这就会导致一个问题:我们在计算扫描框位置时,是按正常画面计算的,但是绘制时翻转了,所以扫描框的位置不对应。
因此,为了避免混淆,我们在绘制时不再翻转,而是通过CSS来控制视频显示为正常(即之前用CSS翻转视频元素,但这样会导致后置摄像头镜像,用户不习惯。所以我们在显示上用CSS翻转,但在处理识别时,不用翻转,因为二维码镜像也能被识别(jsQR支持镜像识别吗?)。
经过查阅,jsQR支持镜像识别,所以我们可以不翻转图像,而只是在显示时用CSS翻转。
所以我们改变策略:
显示时:用CSS将视频元素水平翻转(transform: scaleX(-1)),这样用户看到的是正常的非镜像。
识别时:从原始视频帧(未翻转)中截取扫描框区域,然后直接传递给jsQR。
这样,我们就不用在Canvas绘制时翻转了。
具体步骤:
1. 在CSS中重新给视频添加翻转,用于显示:
#video {
transform: scaleX(-1); /* 水平翻转,用于解决后置摄像头镜像问题 */
}
2. 在Canvas绘制时,去掉翻转代码(即去掉ctx.translate和ctx.scale),直接绘制。
3. 计算扫描框区域时,按原始视频帧(未翻转)的位置计算。
这样,计算扫描框区域的位置就简单了:因为视频在Canvas中绘制的是原始画面(未翻转),那么扫描框在容器中的位置对应到原始画面的位置,就是上面计算出的frameInVideoX, frameInVideoY等。
然后,我们截取这个区域传递给jsQR。
修改代码:
第一步:恢复CSS中的翻转(显示用)
第二步:修改扫描函数中的绘制代码(去掉翻转)
第三步:添加区域截取计算
由于这个修改较大,我们逐步进行。
但是,用户当前的问题是“无法识别”,我们可能不需要这么复杂就能解决。因此,我们首先尝试简单的参数调整。
修改1:调整jsQR参数
将调用jsQR时的参数inversionAttempts改为"attemptBoth"
修改2:添加图像预处理(对比度拉伸)
对比度拉伸函数:
由于时间,我们先实现参数调整,然后如果不行,再实现区域截取和对比度增强。
以下是修改后的扫描函数片段(只修改了参数):
```javascript
const code = jsQR(
imageData.data,
imageData.width,
imageData.height,
{
inversionAttempts: "attemptBoth", // 尝试两种反转模式
canOverwriteImage: true
}
);
```
同时,在无法识别时,我们可以尝试进行对比度增强:
对比度增强函数(线性拉伸):
function contrastStretch(imageData) {
const data = imageData.data;
// 首先计算最小和最大亮度(灰度值)
let min = 255;
let max = 0;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i+1];
const b = data[i+2];
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
if (gray < min) min = gray;
if (gray > max) max = gray;
}
// 如果对比度已经足够,则跳过
if (min >= max || (max - min) < 50) {
// 进行对比度拉伸
const scale = 255 / (max - min);
for (let i = 0; i < data.length; i += 4) {
// 对每个通道进行拉伸
data[i] = Math.min(255, Math.max(0, (data[i] - min) * scale));
data[i+1] = Math.min(255, Math.max(0, (data[i+1] - min) * scale));
data[i+2] = Math.min(255, Math.max(0, (data[i+2] - min) * scale));
}
}
return imageData;
}
使用:在获取imageData后,调用这个函数。
但是,注意:这个操作比较耗时,可能会影响性能。
因此,我们只在前几次扫描失败后,尝试使用对比度增强(例如连续5次没识别到,就启用对比度增强)。
由于复杂度,我们暂时先不实现,如果用户反馈还是无法识别,我们再添加。
综上所述,我们首先尝试以下修改:
1. 调整jsQR参数:inversionAttempts: "attemptBoth"
2. 恢复CSS对视频的翻转,去掉Canvas绘制时的翻转(避免二次翻转)
- 这样我们在Canvas中绘制的是原始图像(没有翻转),然后我们截取扫描框区域(计算得到)进行识别。
注意:恢复CSS翻转后,视频显示正常(非镜像),而传递给jsQR的是原始图像(后置摄像头拍摄的,二维码在图像中是镜像的),jsQR能否识别镜像的二维码?
根据jsQR的文档,它支持识别镜像的二维码,因为它有inversionAttempts选项。
所以我们这样修改:
修改CSS:
#video {
transform: scaleX(-1); /* 水平翻转显示 */
/* 之前去掉的,现在加上 */
}
修改扫描函数中的绘制代码(去掉Canvas绘制时的翻转):
// 删除之前的翻转代码
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
然后,计算扫描框区域(只扫描框内部分)传递给jsQR。
由于计算扫描框区域涉及较多坐标转换,而且可能因设备而异,我们暂时不实现,而是先尝试整个画面识别(参数调整后)。
如果用户反馈依然无法识别,我们再实现区域截取。
因此,当前修改为:
1. 调整jsQR参数
2. 恢复CSS翻转,去掉Canvas翻转
然后,我们重新测试。
如果还是无法识别,我们再考虑区域截取和对比度增强。
考虑到用户可能无法提供测试环境,我们提供调试信息:在页面上显示一个调试Canvas,将每次扫描的图像显示出来,帮助用户了解是否截取到二维码区域。
由于时间,我们先提交修改后的代码(参数调整和显示翻转),如果问题依旧,我们再考虑进一步修改。
修改后的扫描函数(去掉Canvas翻转)和恢复CSS翻转。
同时,我们增加一个调试信息输出:如果连续10次没有扫描到,显示提示信息。
代码修改如下:
1. CSS中:
#video {
transform: scaleX(-1); /* 确保视频显示正常(非镜像) */
}
2. 扫描函数中:
// 创建离屏Canvas
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext("2d", { willReadFrequently: true });
// 绘制当前帧(不再翻转)
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 获取整个图像数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 调用jsQR,参数调整
const code = jsQR(
imageData.data,
imageData.width,
imageData.height,
{
inversionAttempts: "attemptBoth", // 尝试两种反转模式
canOverwriteImage: true
}
);
3. 添加连续失败计数,并给出提示:
let failCount = 0;
// 在detectInterval内部:
if (code) {
failCount = 0;
handleScanSuccess(code.data);
} else {
failCount++;
if (failCount > 10) {
// 每10次失败提示一次
resultText.textContent = "无法识别二维码,请调整位置或光线";
failCount = 0; // 重置
}
}
注意:这个提示可能会打扰用户,所以我们可以考虑在失败一定次数后只提示一次。
我们修改一下:当连续10次没有识别到时,在页面上显示一个提示,然后重置计数,直到下一次成功后再失败10次再提示。
综上,我们给出修改后的完整代码(仅修改关键部分,其余不变):
</think># 摄像头影像正常但无法识别二维码的解决方案
针对您反馈的"摄像头影像正常但无法识别二维码信息"问题,我将提供完整的解决方案。这个问题通常由图像质量、识别算法参数设置或环境因素引起。
## 解决方案核心思路
1. **优化图像预处理**:增强二维码区域对比度
2. **调整识别参数**:优化jsQR库的调用参数
3. **智能区域定位**:聚焦扫描框区域
4. **添加错误反馈**:帮助用户调整位置
下面是完整的实现代码:
```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>二维码识别问题解决方案</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 100%;
padding: 20px;
margin: 0 auto;
text-align: center;
}
h1 {
margin: 20px 0;
color: #2c3e50;
font-size: 1.8rem;
}
.solution-section {
background: #eaf7ff;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
text-align: left;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.solution-section h2 {
color: #2980b9;
margin-bottom: 15px;
font-size: 1.4rem;
}
.solution-item {
margin-bottom: 15px;
padding: 12px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.solution-item h3 {
color: #3498db;
margin-bottom: 8px;
font-size: 1.1rem;
}
.video-container {
position: relative;
width: 100%;
max-width: 500px;
margin: 0 auto 20px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
background: #000;
}
#video {
width: 100%;
height: auto;
display: block;
transform: scaleX(-1); /* 水平翻转显示 */
}
.scan-frame {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 70%;
height: 200px;
border: 3px solid #4CAF50;
border-radius: 8px;
box-shadow: 0 0 0 1000px rgba(0, 0, 0, 0.5);
z-index: 10;
}
.scan-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: #4CAF50;
animation: scan 2s infinite linear;
z-index: 11;
}
@keyframes scan {
0% { top: 0; }
100% { top: 100%; }
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin: 20px 0;
flex-wrap: wrap;
}
button {
background: #4CAF50;
color: white;
border: none;
padding: 12px 25px;
border-radius: 50px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 3px 5px rgba(0,0,0,0.2);
min-width: 140px;
}
button:hover {
background: #45a049;
transform: translateY(-2px);
}
button:active {
transform: translateY(1px);
}
button:disabled {
background: #cccccc;
cursor: not-allowed;
}
#flashBtn {
background: #FFC107;
color: #333;
}
#result {
padding: 20px;
background: white;
border-radius: 10px;
margin: 20px auto;
max-width: 500px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
min-height: 80px;
word-break: break-all;
font-family: monospace;
}
#resultText {
display: block;
margin-top: 10px;
padding: 10px;
border: 1px dashed #ccc;
border-radius: 5px;
background: #f9f9f9;
text-align: left;
}
#error {
color: #e74c3c;
padding: 10px;
min-height: 24px;
font-weight: 500;
min-height: 40px;
}
.success {
color: #2ecc71;
font-weight: bold;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 0.8; }
50% { opacity: 1; }
100% { opacity: 0.8; }
}
.hidden {
display: none;
}
.footer {
margin-top: 30px;
font-size: 0.8rem;
color: #7f8c8d;
}
.data-actions {
margin-top: 15px;
}
.data-actions button {
min-width: auto;
padding: 8px 15px;
font-size: 0.9rem;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-green {
background-color: #2ecc71;
}
.status-red {
background-color: #e74c3c;
}
.status-orange {
background-color: #f39c12;
}
.debug-info {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
font-size: 0.9rem;
text-align: left;
border-left: 4px solid #3498db;
}
</style>
</head>
<body>
<div class="container">
<h1>二维码识别问题解决方案</h1>
<div class="solution-section">
<h2>摄像头正常但识别失败的处理方法</h2>
<div class="solution-item">
<h3><span class="status-indicator status-red"></span> 问题现象分析</h3>
<p>摄像头画面正常显示但无法识别二维码,通常由以下原因导致:</p>
<ul>
<li>图像质量不佳(模糊、过暗、过曝)</li>
<li>二维码与摄像头距离不当</li>
<li>识别算法参数需要优化</li>
<li>二维码区域未精确聚焦</li>
</ul>
</div>
<div class="solution-item">
<h3><span class="status-indicator status-green"></span> 已实施的解决方案</h3>
<p>以下修复已应用于当前页面:</p>
<ol>
<li>添加图像预处理增强对比度</li>
<li>优化jsQR识别参数</li>
<li>聚焦扫描框区域进行识别</li>
<li>增加识别失败提示机制</li>
</ol>
</div>
</div>
<div class="video-container">
<video id="video" playsinline autoplay></video>
<div class="scan-frame">
<div class="scan-line"></div>
</div>
</div>
<div class="controls">
<button id="startBtn">启动摄像头</button>
<button id="flashBtn" class="hidden">打开闪光灯</button>
</div>
<div id="result">
<div>扫描结果:</div>
<div id="resultText">准备扫描,请点击"启动摄像头"按钮...</div>
<div class="data-actions">
<button id="copyBtn" class="hidden">复制内容</button>
<button id="clearBtn" class="hidden">清除结果</button>
</div>
</div>
<div id="error">
<!-- 错误提示将显示在这里 -->
</div>
<div class="debug-info">
<p><strong>调试信息:</strong></p>
<p>识别状态: <span id="statusText">未启动</span></p>
<p>图像质量: <span id="qualityText">-</span></p>
<p>识别尝试: <span id="attemptText">0</span></p>
</div>
<div class="footer">
基于WebRTC和jsQR技术 | 二维码识别问题解决方案
</div>
</div>
<!-- 引入jsQR库 -->
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
<script>
// DOM元素引用
const video = document.getElementById("video");
const startBtn = document.getElementById("startBtn");
const flashBtn = document.getElementById("flashBtn");
const resultText = document.getElementById("resultText");
const copyBtn = document.getElementById("copyBtn");
const clearBtn = document.getElementById("clearBtn");
const errorDiv = document.getElementById("error");
const statusText = document.getElementById("statusText");
const qualityText = document.getElementById("qualityText");
const attemptText = document.getElementById("attemptText");
// 状态变量
let stream = null;
let scanning = false;
let torchActive = false;
let lastScanTime = 0;
let detectInterval = null;
let scanResult = null;
let attemptCount = 0;
let debugEnabled = true;
// 图像增强函数 - 提高对比度
function enhanceContrast(imageData) {
const data = imageData.data;
// 计算图像直方图
const histogram = new Array(256).fill(0);
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const gray = Math.floor(0.299 * r + 0.587 * g + 0.114 * b);
histogram[gray]++;
}
// 查找最小和最大灰度值
let min = 0;
let max = 255;
for (let i = 0; i < 256; i++) {
if (histogram[i] > 0) {
min = i;
break;
}
}
for (let i = 255; i >= 0; i--) {
if (histogram[i] > 0) {
max = i;
break;
}
}
// 如果对比度不够,进行拉伸
if (max - min < 80 && max - min > 10) {
const scale = 255 / (max - min);
for (let i = 0; i < data.length; i += 4) {
// 对每个通道进行对比度拉伸
data[i] = Math.min(255, Math.max(0, (data[i] - min) * scale));
data[i + 1] = Math.min(255, Math.max(0, (data[i + 1] - min) * scale));
data[i + 2] = Math.min(255, Math.max(0, (data[i + 2] - min) * scale));
}
return true;
}
return false;
}
// 图像聚焦函数 - 聚焦扫描框区域
function getScanRegion() {
const videoContainer = document.querySelector('.video-container');
const scanFrame = document.querySelector('.scan-frame');
// 计算扫描框在视频元素中的位置
const containerRect = videoContainer.getBoundingClientRect();
const frameRect = scanFrame.getBoundingClientRect();
// 计算相对位置
const relativeX = frameRect.left - containerRect.left;
const relativeY = frameRect.top - containerRect.top;
const relativeWidth = frameRect.width;
const relativeHeight = frameRect.height;
// 转换为视频元素内的比例
const xRatio = relativeX / containerRect.width;
const yRatio = relativeY / containerRect.height;
const widthRatio = relativeWidth / containerRect.width;
const heightRatio = relativeHeight / containerRect.height;
// 返回区域参数
return {
x: Math.floor(xRatio * video.videoWidth),
y: Math.floor(yRatio * video.videoHeight),
width: Math.floor(widthRatio * video.videoWidth),
height: Math.floor(heightRatio * video.videoHeight)
};
}
// 计算图像质量评分 (0-100)
function calculateImageQuality(imageData) {
const data = imageData.data;
let contrastScore = 0;
let focusScore = 0;
let pixelCount = 0;
// 计算对比度评分
for (let i = 0; i < data.length; i += 16) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const gray1 = 0.299 * r + 0.587 * g + 0.114 * b;
if (i + 20 < data.length) {
const r2 = data[i + 20];
const g2 = data[i + 21];
const b2 = data[i + 22];
const gray2 = 0.299 * r2 + 0.587 * g2 + 0.114 * b2;
contrastScore += Math.abs(gray1 - gray2);
pixelCount++;
}
}
// 计算模糊度评分 (边缘检测)
const width = imageData.width;
const height = imageData.height;
for (let y = 1; y < height - 1; y += 2) {
for (let x = 1; x < width - 1; x += 2) {
const idx = (y * width + x) * 4;
const idxRight = (y * width + x + 1) * 4;
const idxBottom = ((y + 1) * width + x) * 4;
const gray = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2];
const grayRight = 0.299 * data[idxRight] + 0.587 * data[idxRight + 1] + 0.114 * data[idxRight + 2];
const grayBottom = 0.299 * data[idxBottom] + 0.587 * data[idxBottom + 1] + 0.114 * data[idxBottom + 2];
focusScore += Math.abs(gray - grayRight);
focusScore += Math.abs(gray - grayBottom);
}
}
// 标准化评分
const contrast = Math.min(100, Math.floor(contrastScore / pixelCount));
const focus = Math.min(100, Math.floor(focusScore / (width * height)));
// 综合评分
return Math.floor((contrast * 0.6) + (focus * 0.4));
}
// 启动摄像头
async function startCamera() {
try {
// 摄像头参数优化
const constraints = {
video: {
facingMode: "environment",
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 },
focusMode: "continuous" // 连续对焦模式
}
};
// 获取媒体流
stream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = stream;
// 视频准备就绪
video.onloadedmetadata = () => {
video.play().then(() => {
scanning = true;
startBtn.disabled = true;
startBtn.textContent = "摄像头已开启";
flashBtn.classList.remove("hidden");
errorDiv.textContent = "";
resultText.textContent = "正在扫描二维码...";
statusText.textContent = "扫描中";
// 开始扫描
startScanning();
}).catch(err => {
handleError("视频播放失败: " + err.message);
});
};
} catch (err) {
handleError("摄像头访问失败: " + err.message);
}
}
// 开始扫描流程
function startScanning() {
if (detectInterval) clearInterval(detectInterval);
// 创建离屏Canvas
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext("2d", { willReadFrequently: true });
// 设置扫描间隔
detectInterval = setInterval(() => {
if (!scanning) return;
attemptCount++;
attemptText.textContent = attemptCount;
// 检查视频状态
if (video.readyState !== video.HAVE_ENOUGH_DATA) return;
try {
// 绘制当前帧
ctx.drawImage