违停检测模型改为yolov8s,检测模式修改为手动框选禁停区域

This commit is contained in:
2026-06-15 09:22:07 +08:00
parent 18cfc9b16a
commit 4283fb1332
8 changed files with 492 additions and 358 deletions
+9 -2
View File
@@ -25,11 +25,18 @@ export const detectionApi = {
return api.get('/algorithms/config')
},
detectImage(formData, algorithmConfig = null) {
const params = {}
detectImage(formData, modelId, confidence, iou, algorithmConfig = null, regionPolygon = null) {
const params = {
model_id: modelId,
confidence,
iou
}
if (algorithmConfig) {
params.algorithm_config = JSON.stringify(algorithmConfig)
}
if (regionPolygon) {
params.region_polygon = JSON.stringify(regionPolygon)
}
return api.post('/detect/image', formData, {
headers: {
+273 -6
View File
@@ -50,10 +50,26 @@
</el-upload>
</div>
</template>
<div class="image-container">
<!-- 图片显示 -->
<div class="image-container" ref="imageContainer">
<!-- 违停模型原始图片供绘制区域 -->
<img
v-if="resultImage && !supportsVideoUpload"
v-if="isParkingDetection && originalImage && !resultImage"
:src="originalImage"
class="display-image"
ref="originalImg"
@load="onOriginalImageLoad"
alt="原始图片"
/>
<!-- 违停模型区域绘制画布 -->
<canvas
v-if="isParkingDetection && originalImage && !resultImage"
ref="regionCanvas"
class="region-canvas"
@click="onCanvasClick"
></canvas>
<!-- 检测结果图片 -->
<img
v-else-if="resultImage && !supportsVideoUpload"
:src="resultImage"
class="display-image"
alt="检测结果"
@@ -95,6 +111,23 @@
<p class="empty-hint">支持 JPGPNGWEBP 格式</p>
</div>
</div>
<!-- 违停区域绘制控制条移出 image-container避免遮挡 canvas -->
<div v-if="isParkingDetection && originalImage && !resultImage" class="region-controls">
<div class="region-info">
<el-tag type="info" size="small">已绘制 {{ regionPoints.length }} 个顶点</el-tag>
<span v-if="regionPoints.length > 0 && regionPoints.length < 3" class="region-hint">至少还需 {{ 3 - regionPoints.length }} 个点形成闭合区域</span>
<span v-else-if="regionPoints.length >= 3" class="region-hint ready">区域已闭合可以开始检测</span>
<span v-else class="region-hint">点击图片上的位置绘制禁停区域</span>
</div>
<div class="region-actions">
<el-button size="small" @click="clearRegion" :disabled="regionPoints.length === 0">
<el-icon><Delete /></el-icon> 清除
</el-button>
<el-button size="small" type="primary" @click="performDetection" :loading="isDetecting" :disabled="regionPoints.length < 3">
<el-icon><VideoPlay /></el-icon> 开始检测
</el-button>
</div>
</div>
</el-card>
</el-col>
@@ -294,7 +327,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
UploadFilled,
@@ -307,7 +340,9 @@ import {
Warning,
WarningFilled,
Camera,
Loading
Loading,
Delete,
VideoPlay
} from '@element-plus/icons-vue'
import { detectionApi } from '@/api/detection'
import DetectionConfig from './DetectionConfig.vue'
@@ -380,6 +415,15 @@ const fightVideoStats = ref(null)
const keyFrames = ref([])
const videoError = ref(false)
// 违停检测相关状态
const isParkingDetection = computed(() => config.value.model === 'illegal_parking_detection')
const regionPoints = ref([])
const currentFile = ref(null)
const originalImageSize = ref({ width: 0, height: 0 })
const imageContainer = ref(null)
const regionCanvas = ref(null)
const originalImg = ref(null)
const isActionDetection = computed(() => {
return config.value.model === 'action_detection'
})
@@ -427,7 +471,25 @@ const formattedJson = computed(() => {
const beforeUpload = async (file) => {
isDetecting.value = true
currentFile.value = file
// 违停检测模型:不自动上传,显示原始图片让用户绘制禁停区域
if (isParkingDetection.value) {
const isImage = file.type.startsWith('image/')
if (!isImage) {
ElMessage.error('违停检测只能上传图片文件')
isDetecting.value = false
return false
}
originalImage.value = URL.createObjectURL(file)
resultImage.value = ''
regionPoints.value = []
detections.value = []
stats.value = null
isDetecting.value = false
return false
}
if (isActionDetection.value) {
const isVideo = file.type.startsWith('video/')
if (!isVideo) {
@@ -523,6 +585,149 @@ const handleUploadSuccess = (response) => {
}
}
// ========== 违停区域绘制相关方法 ==========
const getImageDisplayRect = () => {
const container = imageContainer.value
const img = originalImg.value
if (!container || !img) return { scale: 1, offsetX: 0, offsetY: 0 }
const imgWidth = originalImageSize.value.width || img.naturalWidth || container.clientWidth
const imgHeight = originalImageSize.value.height || img.naturalHeight || container.clientHeight
const containerWidth = container.clientWidth
const containerHeight = container.clientHeight
const scale = Math.min(containerWidth / imgWidth, containerHeight / imgHeight)
const displayWidth = imgWidth * scale
const displayHeight = imgHeight * scale
const offsetX = (containerWidth - displayWidth) / 2
const offsetY = (containerHeight - displayHeight) / 2
return { scale, offsetX, offsetY, displayWidth, displayHeight, imgWidth, imgHeight }
}
const canvasToImageCoords = (cx, cy) => {
const { scale, offsetX, offsetY } = getImageDisplayRect()
return {
x: Math.round((cx - offsetX) / scale),
y: Math.round((cy - offsetY) / scale)
}
}
const onOriginalImageLoad = (e) => {
originalImageSize.value = {
width: e.target.naturalWidth,
height: e.target.naturalHeight
}
drawRegion()
}
const drawRegion = () => {
const canvas = regionCanvas.value
if (!canvas || !imageContainer.value) return
const container = imageContainer.value
canvas.width = container.clientWidth
canvas.height = container.clientHeight
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (regionPoints.value.length === 0) return
const { scale, offsetX, offsetY } = getImageDisplayRect()
ctx.beginPath()
regionPoints.value.forEach((point, i) => {
const cx = point.x * scale + offsetX
const cy = point.y * scale + offsetY
if (i === 0) ctx.moveTo(cx, cy)
else ctx.lineTo(cx, cy)
})
if (regionPoints.value.length >= 3) {
ctx.closePath()
ctx.fillStyle = 'rgba(239, 68, 68, 0.25)'
ctx.fill()
}
ctx.strokeStyle = '#EF4444'
ctx.lineWidth = 2
ctx.setLineDash([6, 4])
ctx.stroke()
ctx.setLineDash([])
// 绘制顶点
regionPoints.value.forEach((point, i) => {
const cx = point.x * scale + offsetX
const cy = point.y * scale + offsetY
ctx.beginPath()
ctx.arc(cx, cy, 5, 0, Math.PI * 2)
ctx.fillStyle = '#EF4444'
ctx.fill()
ctx.strokeStyle = '#fff'
ctx.lineWidth = 1.5
ctx.stroke()
// 顶点序号
ctx.fillStyle = '#fff'
ctx.font = 'bold 11px sans-serif'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(String(i + 1), cx, cy)
})
}
const onCanvasClick = (e) => {
const canvas = regionCanvas.value
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const cx = e.clientX - rect.left
const cy = e.clientY - rect.top
const coords = canvasToImageCoords(cx, cy)
const { imgWidth, imgHeight } = getImageDisplayRect()
// 限制在图片范围内
if (coords.x < 0 || coords.y < 0 || coords.x > imgWidth || coords.y > imgHeight) {
return
}
regionPoints.value.push({ x: coords.x, y: coords.y })
drawRegion()
}
const clearRegion = () => {
regionPoints.value = []
drawRegion()
}
const performDetection = async () => {
if (!currentFile.value) {
ElMessage.warning('请先上传图片')
return
}
isDetecting.value = true
const formData = new FormData()
formData.append('file', currentFile.value)
try {
const response = await detectionApi.detectImage(
formData,
config.value.model,
config.value.confidence,
config.value.iou,
config.value.algorithmConfig,
regionPoints.value.length >= 3 ? regionPoints.value : null
)
handleUploadSuccess(response.data)
} catch (error) {
handleUploadError(error)
}
}
const handleActionDetectionSuccess = (response) => {
isDetecting.value = false
console.log('Action detection response:', response)
@@ -626,6 +831,21 @@ const modelName = computed(() => {
const model = props.models.find(m => m.id === config.value.model)
return model ? model.name : config.value.model
})
// 窗口大小变化时重绘 canvas
const handleWindowResize = () => {
if (isParkingDetection.value && originalImage.value && !resultImage.value) {
drawRegion()
}
}
onMounted(() => {
window.addEventListener('resize', handleWindowResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleWindowResize)
})
</script>
<style scoped>
@@ -1376,4 +1596,51 @@ const modelName = computed(() => {
height: 90px;
}
}
/* ========== 违停区域绘制样式 ========== */
.region-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
cursor: crosshair;
}
.region-controls {
margin-top: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
background: rgba(15, 23, 42, 0.9);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 12px;
padding: 10px 16px;
backdrop-filter: blur(10px);
}
.region-info {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.region-hint {
font-size: 12px;
color: #94A3B8;
}
.region-hint.ready {
color: #22C55E;
font-weight: 500;
}
.region-actions {
display: flex;
gap: 8px;
}
</style>