Parcourir la source

添加检验任务拍照点拍照图片清晰度检测

ZhangLeo il y a 1 semaine
Parent
commit
a159eefc88

BIN
nativeplugins/opencv/android/libs/opencv.jar


+ 388 - 0
nativeplugins/opencv/android/uts/BitmapUtils.uts

@@ -0,0 +1,388 @@
+// nativeplugins/opencv/android/uts/BitmapUtils.uts
+
+import Bitmap from 'android.graphics.Bitmap'
+import Base64 from 'android.util.Base64'
+import BitmapFactory from 'android.graphics.BitmapFactory'
+
+// 定义类型接口
+type ImageClarityScore = {
+    score: number
+    level: string
+}
+
+type ImageClarityResult = {
+    score: number
+    level: string
+    isClear: boolean
+}
+
+/**
+ * 图像清晰度检测工具类
+ */
+export class BitmapUtils {
+    
+    /**
+     * 计算图像的亮度方差作为清晰度指标
+     */
+    static calculateBrightnessVariance(bitmap: Bitmap): number {
+        try {
+            const width = bitmap.getWidth()
+            const height = bitmap.getHeight()
+            
+            // 根据原始尺寸动态计算采样尺寸
+            // 目标采样区域的总像素数,控制在合理范围内
+            const targetPixelCount = 2500 // 50x50 = 2500像素
+            
+            // 计算合适的采样尺寸,保持宽高比
+            let sampleWidth: number
+            let sampleHeight: number
+            
+            if (width > height) {
+                // 宽图
+                sampleWidth = Math.min(width, Math.sqrt(targetPixelCount * width / height).toInt())
+                sampleHeight = Math.min(height, (sampleWidth * height / width).toInt())
+            } else {
+                // 高图或方图
+                sampleHeight = Math.min(height, Math.sqrt(targetPixelCount * height / width).toInt())
+                sampleWidth = Math.min(width, (sampleHeight * width / height).toInt())
+            }
+            
+            // 确保最小尺寸
+            sampleWidth = Math.max(sampleWidth, 32)
+            sampleHeight = Math.max(sampleHeight, 32)
+            
+            const scaledBitmap = Bitmap.createScaledBitmap(
+                bitmap, 
+                sampleWidth.toInt(), 
+                sampleHeight.toInt(), 
+                false
+            )
+            
+            let totalBrightness = 0.0
+            let brightnessSquares = 0.0
+            const pixelCount = sampleWidth * sampleHeight
+            
+            // 计算平均亮度
+            for (let i: number = 0; i < sampleWidth; i++) {
+                for (let j: number = 0; j < sampleHeight; j++) {
+                    const pixel = scaledBitmap.getPixel(i.toInt(), j.toInt())
+                    const brightness = this.getPixelBrightness(pixel)
+                    totalBrightness += brightness
+                }
+            }
+            
+            const meanBrightness = totalBrightness / pixelCount
+            
+            // 计算亮度方差
+            for (let i: number = 0; i < sampleWidth; i++) {
+                for (let j: number = 0; j < sampleHeight; j++) {
+                    const pixel = scaledBitmap.getPixel(i.toInt(), j.toInt())
+                    const brightness = this.getPixelBrightness(pixel)
+                    const diff = brightness - meanBrightness
+                    brightnessSquares += diff * diff
+                }
+            }
+            
+            const variance = brightnessSquares / pixelCount
+            
+            // 回收临时Bitmap
+            if (!scaledBitmap.isRecycled()) {
+                scaledBitmap.recycle()
+            }
+            
+            return variance
+            
+        } catch (error) {
+            console.error('清晰度计算失败:', error)
+            return -1
+        }
+    }
+	
+	// 在calculateBrightnessVariance方法中使用多尺度采样
+	static calculateBrightnessVariance2(bitmap: Bitmap): number {
+	    try {
+	        const width = bitmap.getWidth()
+	        const height = bitmap.getHeight()
+	        
+	        // 对2048×1536的图片,使用多个采样尺度
+	        const scales = [
+	            { width: 200, height: 150 },   // 约10%尺度
+	            { width: 100, height: 75 },    // 约5%尺度  
+	            { width: 50, height: 38 }      // 约2.5%尺度
+	        ]
+	        
+	        let totalVariance = 0.0
+	        let scaleCount = 0
+	        
+	        for (const scale of scales) {
+	            const sampleWidth = Math.min(width, scale.width as number)
+	            const sampleHeight = Math.min(height, scale.height as number)
+	            
+	            if (sampleWidth < 32 || sampleHeight < 32) continue
+	            
+	            const scaledBitmap = Bitmap.createScaledBitmap(
+	                bitmap, 
+	                sampleWidth.toInt(), 
+	                sampleHeight.toInt(), 
+	                false
+	            )
+	            
+	            const variance = this.calculateVarianceForBitmap(scaledBitmap)
+	            totalVariance += variance
+	            scaleCount++
+	            
+	            if (!scaledBitmap.isRecycled()) {
+	                scaledBitmap.recycle()
+	            }
+	        }
+	        
+	        return scaleCount > 0 ? totalVariance / scaleCount : -1
+	        
+	    } catch (error) {
+	        console.error('多尺度清晰度计算失败:', error)
+	        return -1
+	    }
+	}
+	
+	// 提取方差计算为独立方法
+	private static calculateVarianceForBitmap(bitmap: Bitmap): number {
+	    const width = bitmap.getWidth()
+	    const height = bitmap.getHeight()
+	    
+	    let totalBrightness = 0.0
+	    let brightnessSquares = 0.0
+	    const pixelCount = width * height
+	    
+	    // 计算平均亮度
+	    for (let i: number = 0; i < width; i++) {
+	        for (let j: number = 0; j < height; j++) {
+	            const pixel = bitmap.getPixel(i.toInt(), j.toInt())
+	            const brightness = this.getPixelBrightness(pixel)
+	            totalBrightness += brightness
+	        }
+	    }
+	    
+	    const meanBrightness = totalBrightness / pixelCount
+	    
+	    // 计算亮度方差
+	    for (let i: number = 0; i < width; i++) {
+	        for (let j: number = 0; j < height; j++) {
+	            const pixel = bitmap.getPixel(i.toInt(), j.toInt())
+	            const brightness = this.getPixelBrightness(pixel)
+	            const diff = brightness - meanBrightness
+	            brightnessSquares += diff * diff
+	        }
+	    }
+	    
+	    return brightnessSquares / pixelCount
+	}
+	
+	/**
+	 * 使用边缘密度作为清晰度指标(更适合大图)
+	 */
+	static calculateEdgeDensity(bitmap: Bitmap): number {
+	    try {
+	        const width = bitmap.getWidth()
+	        const height = bitmap.getHeight()
+	        
+	        // 对于大图,使用适当的采样尺寸
+	        const sampleWidth = Math.min(width, 300)
+	        const sampleHeight = Math.min(height, (300 * height / width).toInt())
+	        
+	        const scaledBitmap = Bitmap.createScaledBitmap(
+	            bitmap, 
+	            sampleWidth.toInt(), 
+	            sampleHeight.toInt(), 
+	            false
+	        )
+	        
+	        let edgeCount = 0
+	        const totalPixels = sampleWidth * sampleHeight
+	        
+	        // 简单的边缘检测
+	        for (let i: number = 1; i < sampleWidth - 1; i++) {
+	            for (let j: number = 1; j < sampleHeight - 1; j++) {
+	                const center = this.getPixelBrightness(scaledBitmap.getPixel(i.toInt(), j.toInt()))
+	                const right = this.getPixelBrightness(scaledBitmap.getPixel((i + 1).toInt(), j.toInt()))
+	                const bottom = this.getPixelBrightness(scaledBitmap.getPixel(i.toInt(), (j + 1).toInt()))
+	                
+	                // 如果相邻像素亮度差异大,认为是边缘
+	                if (Math.abs(center - right) > 20 || Math.abs(center - bottom) > 20) {
+	                    edgeCount++
+	                }
+	            }
+	        }
+	        
+	        const edgeDensity = (edgeCount / totalPixels) * 1000
+	        
+	        if (!scaledBitmap.isRecycled()) {
+	            scaledBitmap.recycle()
+	        }
+	        
+	        return edgeDensity
+	        
+	    } catch (error) {
+	        console.error('边缘密度计算失败:', error)
+	        return -1
+	    }
+	}
+    
+    /**
+     * 获取像素亮度
+     */
+    private static getPixelBrightness(pixel: number): number {
+        const r = (pixel >> 16) & 0xff
+        const g = (pixel >> 8) & 0xff
+        const b = pixel & 0xff
+        return (r + g + b) / 3.0
+    }
+    
+    /**
+     * 判断图像是否清晰
+     */
+    static isImageClear(bitmap: Bitmap, threshold: number = 500): boolean {
+        const variance = this.calculateBrightnessVariance2(bitmap)
+        return variance > threshold
+    }
+    
+    /**
+     * 获取图像清晰度评分
+     */
+    static getImageClarityScore(bitmap: Bitmap): ImageClarityScore {
+        const variance = this.calculateBrightnessVariance2(bitmap)
+        
+        let level: string
+        if (variance < 0) {
+            level = "处理失败"
+        } else if (variance < 500) {
+            level = "非常模糊"
+        } else if (variance < 2000) {
+            level = "模糊"
+        } else if (variance < 3200) {
+            level = "一般"
+        } else if (variance < 3500) {
+            level = "清晰"
+        } else {
+            level = "非常清晰"
+        }
+        
+        return {
+            score: Math.round(variance * 100) / 100,
+            level: level
+        }
+    }
+    
+    /**
+     * 从Base64数据检测清晰度
+     */
+    static checkImageClarityFromBase64(base64Data: string): ImageClarityResult {
+        try {
+            const bitmap = this.base64ToBitmap(base64Data)
+            if (bitmap == null) {
+                return { score: -1, level: "加载失败", isClear: false }
+            }
+            
+            const clarityInfo = this.getImageClarityScore(bitmap)
+            const isClear = this.isImageClear(bitmap)
+            
+            if (!bitmap.isRecycled()) {
+                bitmap.recycle()
+            }
+            
+            return {
+                score: clarityInfo.score,
+                level: clarityInfo.level,
+                isClear: isClear
+            }
+            
+        } catch (error) {
+            console.error('图像清晰度检测失败:', error)
+            return { score: -1, level: "检测失败", isClear: false }
+        }
+    }
+    
+    /**
+     * 从文件路径检测清晰度
+     */
+    static checkImageClarityFromPath(imagePath: string): ImageClarityResult {
+        try {
+            const bitmap = BitmapFactory.decodeFile(imagePath)
+            if (bitmap == null) {
+                return { score: -1, level: "加载失败", isClear: false }
+            }
+            
+            const clarityInfo = this.getImageClarityScore(bitmap)
+            const isClear = this.isImageClear(bitmap)
+            
+            if (!bitmap.isRecycled()) {
+                bitmap.recycle()
+            }
+            
+            return {
+                score: clarityInfo.score,
+                level: clarityInfo.level,
+                isClear: isClear
+            }
+            
+        } catch (error) {
+            console.error('图像清晰度检测失败:', error)
+            return { score: -1, level: "检测失败", isClear: false }
+        }
+    }
+    
+    /**
+     * 简化的Base64转Bitmap方法
+     */
+    private static base64ToBitmap(base64Data: string): Bitmap | null {
+        try {
+            // 移除Base64前缀
+            let pureBase64 = base64Data
+            if (base64Data.includes(',')) {
+                pureBase64 = base64Data.split(',')[1]
+            }
+            
+            // 解码Base64
+            const decodedBytes = Base64.decode(pureBase64, Base64.DEFAULT)
+            
+            // 获取字节数组长度
+            const bitmap = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
+            
+            return bitmap
+            
+        } catch (error) {
+            console.error('Base64转Bitmap失败:', error)
+            return null
+        }
+    }
+    
+    /**
+     * 备用方法:直接返回清晰度分数(避免复杂对象类型)
+     */
+    static getSimpleClarityScore(bitmap: Bitmap): number {
+        return this.calculateBrightnessVariance(bitmap)
+    }
+    
+    /**
+     * 从Base64获取简单清晰度分数
+     */
+    static getSimpleClarityScoreFromBase64(base64Data: string): number {
+        try {
+            const bitmap = this.base64ToBitmap(base64Data)
+            if (bitmap == null) {
+                return -1
+            }
+            
+            const score = this.calculateBrightnessVariance(bitmap)
+            
+            if (!bitmap.isRecycled()) {
+                bitmap.recycle()
+            }
+            
+            return score
+            
+        } catch (error) {
+            console.error('清晰度检测失败:', error)
+            return -1
+        }
+    }
+}

+ 20 - 0
nativeplugins/opencv/package.json

@@ -0,0 +1,20 @@
+{
+  "name": "opencv",
+  "version": "1.0.0",
+  "description": "OpenCV + Bitmap 处理插件",
+  "nativePlugins": {
+    "opencv": {
+      "platforms": {
+        "android": {
+          "integrateType": "jar",
+          "dependencies": {
+            "libs": ["opencv.jar"]
+          },
+          "uts": {
+            "src": "android/uts"
+          }
+        }
+      }
+    }
+  }
+}

+ 160 - 72
pages/work/download/TaskCamera-scan-code.uvue

@@ -16,13 +16,6 @@
 				</view>
 			</swiper-item>
 		</swiper>
-	
-
-		<view class="camera-scan-code-back-wrap" v-show="false">
-			<button class="btn block bg-blue lg round" @click="chooseImage">
-				浏览相册
-			</button>
-		</view>
 
 		<view class="camera-scan-code-table">
 			<view class="camera-scan-code-table-pair" v-if="joinRecord.imgname!=''">
@@ -56,7 +49,9 @@
 		    <button type="default" class="btn bg-blue round" @click="navigateExit">退出拍照</button>
 		    
 		</view>
-    </view>	
+    </view>
+    <!-- 隐藏的canvas用于清晰度检测 -->
+	<canvas id="canvas" style="position: absolute; left: -9999px; top: -9999px; width: 300px; height: 300px;"></canvas>
 	<!-- #ifdef APP -->
 	</scroll-view>
 	<!-- #endif -->
@@ -73,6 +68,9 @@
 		checked : boolean
 	}
 	import { getJoinList, TaskJoinRecord, updateData } from '@/api/work';
+	import { BitmapUtils } from '@/nativeplugins/opencv/android/uts/BitmapUtils.uts'
+	
+
 	export default {
 		data() {
 			return {
@@ -107,7 +105,10 @@
 					image: '/static/images/banner/banner01.png',
 					name: 'banner01.png',
 					checked: true
-				}] as ImageItem[]
+				}] as ImageItem[],
+				canvas: null as UniCanvasElement | null,
+				canvasContext: null as CanvasContext | null,
+				renderingContext: null as CanvasRenderingContext2D | null,
 			}
 		},
 		onBackPress() {
@@ -163,6 +164,22 @@
 			if (maxcount != '') {
 				this.maxcount = parseInt(maxcount)
 			}
+			
+			// 异步调用方式, 跨平台写法
+			uni.createCanvasContextAsync({
+			  id: 'canvas',
+			  component: this,
+			  success: (context : CanvasContext) => {
+			    this.canvasContext = context;
+			    this.renderingContext = context.getContext('2d')!;
+			    this.canvas = this.renderingContext!.canvas;
+			  }
+			})
+		},
+		onReady() {
+		// 确认canvas元素可正常使用
+		// UTS中不支持uni.createCanvasContext,改用其他方式验证
+		console.log('Canvas元素已准备就绪');
 		},
 		methods: {
 			navigateBack() {
@@ -194,7 +211,75 @@
 					}
 				});
 			},
-			saveImage() {
+			async saveImage() {
+				// 保存图片到数据库的函数
+				const saveImageToDatabase = () => {
+					let updateImgs = ''
+					let updateNames = ''
+
+					// 动态生成最小的可用图片编号
+					const getMinAvailableNumber = () => {
+						if (this.imgArrLen === 0) return 1;
+
+						// 从已有的图片名称中提取编号
+						const existingNumbers = new Set<number>();
+						const imgNames = this.joinRecord.imgname.split(',');
+
+						imgNames.forEach((name : string) => {
+							if (name != '') {
+								// 提取图片名称中的数字部分
+								const match = name.match(/-(\d+)\.jpg$/);
+								if (match != null && match[1] != null) {
+									const numStr = match[1] as string;
+									existingNumbers.add(parseInt(numStr));
+								}
+							}
+						});
+
+						// 查找1到所需图片数量之间最小的未使用编号
+						for (let i = 1; i <= 10; i++) {
+							if (!existingNumbers.has(i)) {
+								return i;
+							}
+						}
+
+						// 如果1到所需数量都被使用了,则使用当前数量+1
+						return this.imgArrLen + 1;
+					};
+
+					const minAvailableNumber = getMinAvailableNumber();
+					const newImgName = this.dyImgName + "-" + minAvailableNumber + ".jpg";				
+					const relativePath = `/uploadImgs/${newImgName}`
+
+					if (this.imgArrLen == 0) {
+						updateImgs = relativePath
+						updateNames = newImgName;
+					} else {
+						updateImgs = this.joinRecord.photourl + "," + relativePath
+						updateNames = this.joinRecord.imgname + "," + newImgName;
+					}
+
+					let updatedData = "imgname='" + updateNames + "',photourl='" + updateImgs + "'"
+					
+					updateData('app_task_photo', updatedData, 'sxid', this.recordId.toString()).then((res : UTSJSONObject) => {
+						let data = res?.['data'] as boolean ?? false
+						if (data != null && data== true) {
+							uni.showToast({
+								title: "保存成功!",
+							});
+							const newPath = `${uni.env.USER_DATA_PATH}/uploadImgs/${newImgName}`
+							this.renameFile(this.path, newPath)
+
+							uni.navigateTo({
+								url: `/pages/work/download/PhotoRecord?pdid=${this.joinRecord.pdid}&senum=${this.num}`,
+								// 修改动画方向为从左到右退回
+								animationType: 'slide-in-left', // 使用从左到右滑出的动画效果
+								animationDuration: 300 // 动画持续时间,单位ms
+							})
+						}
+					});
+				}
+
 				//保存图片进入相册文件
 				if (this.path == '') {
 					uni.showToast({
@@ -204,70 +289,73 @@
 					});
 					return;
 				}
-				let updateImgs = ''
-				let updateNames = ''
-
-				// 动态生成最小的可用图片编号
-				const getMinAvailableNumber = () => {
-					if (this.imgArrLen === 0) return 1;
-
-					// 从已有的图片名称中提取编号
-					const existingNumbers = new Set<number>();
-					const imgNames = this.joinRecord.imgname.split(',');
-
-					imgNames.forEach((name : string) => {
-						if (name != '') {
-							// 提取图片名称中的数字部分
-							const match = name.match(/-(\d+)\.jpg$/);
-							if (match != null && match[1] != null) {
-								const numStr = match[1] as string;
-								existingNumbers.add(parseInt(numStr));
-							}
-						}
-					});
-
-					// 查找1到所需图片数量之间最小的未使用编号
-					for (let i = 1; i <= 10; i++) {
-						if (!existingNumbers.has(i)) {
-							return i;
-						}
-					}
-
-					// 如果1到所需数量都被使用了,则使用当前数量+1
-					return this.imgArrLen + 1;
-				};
-
-				const minAvailableNumber = getMinAvailableNumber();
-				const newImgName = this.dyImgName + "-" + minAvailableNumber + ".jpg";				
-				const relativePath = `/uploadImgs/${newImgName}`
-
-				if (this.imgArrLen == 0) {
-					updateImgs = relativePath
-					updateNames = newImgName;
-				} else {
-					updateImgs = this.joinRecord.photourl + "," + relativePath
-					updateNames = this.joinRecord.imgname + "," + newImgName;
-				}
-
-				let updatedData = "imgname='" + updateNames + "',photourl='" + updateImgs + "'"
 				
-				updateData('app_task_photo', updatedData, 'sxid', this.recordId.toString()).then((res : UTSJSONObject) => {
-					let data = res?.['data'] as boolean ?? false
-					if (data != null && data== true) {
-						uni.showToast({
-							title: "保存成功!",
-						});
-						const newPath = `${uni.env.USER_DATA_PATH}/uploadImgs/${newImgName}`
-						this.renameFile(this.path, newPath)
-
-						uni.navigateTo({
-							url: `/pages/work/download/PhotoRecord?pdid=${this.joinRecord.pdid}&senum=${this.num}`,
-							// 修改动画方向为从左到右退回
-							animationType: 'slide-in-left', // 使用从左到右滑出的动画效果
-							animationDuration: 300 // 动画持续时间,单位ms
-						})
+				// 检测图片清晰度
+				try {
+					uni.showLoading({ title: '检测图片清晰度...' });
+					this.renderingContext!.clearRect(0, 0, 1000, 1000)
+					let image = this.canvasContext!.createImage();
+					image.src = this.path
+					let score = 0
+					let level = ""
+					image.onload = () => {
+					  
+					  this.renderingContext?.drawImage(image, 0, 0, image.width, image.height);
+					  console.log(image.width)
+					  console.log(image.height)
+					  setTimeout(() => {
+					    try {
+						  // 将canvas内容转换为base64格式
+						  const base64Data = this.canvasContext!.toDataURL();
+						  const result = BitmapUtils.checkImageClarityFromBase64(base64Data)
+						  console.log(result)
+						  console.log("清晰度检测结果得分:", result?.['score']);
+						  score = result?.['score'] as number
+						  level = result?.['level'] as string
+					      uni.hideLoading();
+						  const customThreshold = 3000;
+						  const isClearWithCustomThreshold = score > customThreshold;
+						  if (!isClearWithCustomThreshold) {
+						  	// 不清晰的图片,提示用户
+						  	uni.showModal({
+						  		title: '提示',
+						  		content: `图片清晰度检测结果:${level}\n图片可能不够清晰,是否继续保存?`,
+						  		success: (res) => {
+						  			if (!res.confirm) {
+						  				// 用户取消保存
+						  				return;
+						  			}
+						  			// 用户确认后执行保存流程
+						  			saveImageToDatabase();
+						  		},
+						  		fail: () => {
+						  			// 模态框失败时继续保存
+						  			saveImageToDatabase();
+						  		}
+						  	});
+						  } else {
+						  	// 图片清晰,直接保存
+						  	saveImageToDatabase();
+						  }
+						  
+					    } catch (error) {
+					      console.error("转换图片为Base64失败:", error);
+					    }
+					  }, 100); 
 					}
-				});
+				
+				} catch (error) {
+					uni.hideLoading();
+					console.error('图片清晰度检测失败:', error);
+					// 检测失败时继续保存流程
+					uni.showToast({
+						title: '清晰度检测失败,继续保存',
+						icon: 'none',
+						duration: 2000
+					});
+					saveImageToDatabase();
+				}
+				return;
 			},
 			previewSingleImage(imageUrl : string) {
 				uni.previewImage({