违停检测模型改为yolov8s,检测模式修改为手动框选禁停区域
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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">支持 JPG、PNG、WEBP 格式</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>
|
||||
Reference in New Issue
Block a user