QT中QMessagebox弹出的窗口背景变黑色怎么变成白色

时间: 2025-08-30 22:16:55 AIGC 浏览: 9
<think>好的,用户想修改QMessageBox弹出窗口的背景颜色从黑色变为白色。我需要根据提供的引用资料来找到解决方法。 首先,看到引用[2]提到可以通过QSS样式表设置窗体与按钮的外观。比如使用setStyleSheet方法。这可能适用于QMessageBox,但需要确认。 引用[1]中的例子是通过QLabel的样式表设置背景色为白色,这说明直接对控件应用样式表是可行的。但QMessageBox是标准对话框,可能需要特殊处理。 引用[4]提到在Android下QMessageBox背景黑色,可能涉及到主题设置。但用户可能是在桌面环境下使用,所以可能需要不同的方法。 接下来,我需要考虑如何应用样式表到QMessageBox。因为QMessageBox是系统提供的对话框,直接设置全局样式表可能会影响其他控件。或者,可能需要针对QMessageBox的特定类名设置样式。 根据引用[2]的例子,全局设置QSS可能有效。例如,设置QMessageBox的背景颜色为白色,可能需要这样的代码: qApp->setStyleSheet("QMessageBox { background-color: white; }"); 但需要注意子控件的颜色,如文本和按钮,可能需要单独设置,否则文字可能不可见。比如设置QLabel和QPushButton的样式,如引用[1]和[2]中的做法。 另外,引用[3]中的自定义控件通过重写paintEvent来实现样式,但对于QMessageBox这种标准对话框,可能不太适用,除非子类化,但比较复杂。 所以,综合这些信息,步骤应该是: 1. 使用样式表设置QMessageBox的背景为白色。 2. 同时设置相关子控件的颜色,确保文字可见。 3. 测试是否生效,可能需要调整其他属性。 需要注意的是,不同平台或主题下可能有不同的表现,如引用[4]提到的主题问题,可能需要更详细的样式设置。</think>要修改QMessageBox弹出窗口的背景颜色为白色,可以通过以下三种方法实现: ### 方法1:全局样式表设置 通过QSS样式表直接定义QMessageBox的样式: ```cpp qApp->setStyleSheet( "QMessageBox { background-color: white; }" //设置对话框背景色 "QLabel { color: black; }" //设置文字颜色 "QPushButton { color: black; }" //设置按钮文字颜色 ); ``` *优点*:全局生效,代码简洁 *缺点*:影响所有QMessageBox实例[^2] ### 方法2:单实例样式设置 针对特定QMessageBox单独设置样式: ```cpp QMessageBox msgBox; msgBox.setStyleSheet( "QMessageBox { background-color: white; }" "QLabel { color: black; }" "QPushButton { background: white; color: black; }" ); msgBox.exec(); ``` *优点*:仅影响当前对话框 *注意*:必须重新定义子控件样式[^1] ### 方法3:主题样式覆盖(Android特殊处理) 对于Android平台需添加窗口标志: ```cpp msgBox.setWindowFlags(msgBox.windowFlags() | Qt::FramelessWindowHint); msgBox.setAttribute(Qt::WA_TranslucentBackground); //禁用系统默认背景 msgBox.setStyleSheet("background: white;"); //必须设置半透明属性才能生效 ``` 该方案适用于移动端特殊主题适配[^4] 相关问题
阅读全文

相关推荐

以下是chuanganqi_1.cpp文件的内容 #include "chuanganqi_1.h" #include "ui_chuanganqi_1.h" #include "mainwindow.h" #include "chuanganqi_2.h" #include <QWidget> #include <QPushButton> #include <QStatusBar> #include <QLabel> #include <QTextEdit> #include <QSplitter> #include <QTimer> #include <QTime> #include <cmath> #include <QDateTime> #include <QRandomGenerator> #include <QFile> #include <QTextStream> #include "qcustomplot.h" #include <QString> #include <QMouseEvent> #include <QMessageBox> Chuanganqi_1::Chuanganqi_1(QWidget *parent) : QMainWindow(parent), ui(new Ui::Chuanganqi_1) { ui->setupUi(this); //设置窗口标题 this->setWindowTitle("IMU"); this->setFixedSize(1600,900); ui->widget_22->setStyleSheet("#widget_22{background: transparent;}"); // 设置背景图 ui->widget_23->setStyleSheet("#widget_23{" "border-image: url(:/image/beijing4.jpg);" "background-repeat: no-repeat; " "background-position: center;}"); //设置最小尺寸 ui->widget1->setFixedSize(1600,50); // 左侧栏设置按钮属性 ui->toolButton_2->setFixedSize(100,50); ui->toolButton_2->move(0,0); ui->toolButton_2->setStyleSheet("QToolButton{ font-size: 24px;font-weight:bold}"); ui->toolButton_3->setFixedSize(100,50); ui->toolButton_3->move(0,70); ui->toolButton_3->setStyleSheet("QToolButton{ font-size: 24px;font-weight:bold;}"); ui->toolButton_4->setFixedSize(150,50); ui->toolButton_4->move(0,140); ui->toolButton_4->setStyleSheet("QToolButton{ font-size: 24px;font-weight:bold}"); ui->toolButton_5->setFixedSize(150,50); ui->toolButton_5->move(0,210); ui->toolButton_5->setStyleSheet("QToolButton{ font-size: 24px;font-weight:bold}"); ui->widget_4->setFixedWidth(500); ui->widget_5->setFixedHeight(50); ui->lineEdit->setFixedWidth(120); ui->lineEdit_2->setFixedWidth(120); ui->lineEdit_3->setFixedWidth(120); ui->widget_23->setFixedSize(1600,900); ui->widget_22->setMinimumSize(790,700); //初始化角度图表 ui->CustomPlot->setBackground(QColor(224,224,224,120));// 设置整个绘图区域背景为浅灰色(RGB 224)半透明(alpha=150) ui->CustomPlot->axisRect()->setBackground(QColor(240, 240, 240, 100));// 设置坐标轴区域背景为更浅的灰色(RGB 240)高透明度(alpha=100) // ui->CustomPlot->setBackground(Qt::transparent);//背景透明 ui->CustomPlot->legend->setBrush(Qt::transparent);//图例背景透明 ui->CustomPlot->legend->setBorderPen(QPen(Qt::white)); // 可选,去掉边框 // 设置坐标轴线颜色为白色 ui->CustomPlot->xAxis->setBasePen(QPen(Qt::white)); // x轴轴线 ui->CustomPlot->yAxis->setBasePen(QPen(Qt::white)); // y轴轴线 // 设置X轴 QPen axisPen(Qt::white); // 轴线颜色 axisPen.setWidth(2); ui->CustomPlot->xAxis->setBasePen(axisPen); // 设置主刻度线:白色,宽度2 ui->CustomPlot->xAxis->setTickLength(8, 0); // 内侧0,外侧8 ui->CustomPlot->xAxis->setTickPen(QPen(Qt::white, 2)); // 设置次刻度线:白色,宽度1 ui->CustomPlot->xAxis->setSubTickLength(4, 0); // 内侧0,外侧8 ui->CustomPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); // 设置y轴 QPen ayisPen(Qt::white); // 轴线颜色 ayisPen.setWidth(2); ui->CustomPlot->yAxis->setBasePen(axisPen); // 设置主刻度线:白色,宽度2 ui->CustomPlot->yAxis->setTickLength(8, 0); // 内侧0,外侧8 ui->CustomPlot->yAxis->setTickPen(QPen(Qt::white, 2)); // 设置次刻度线:白色,宽度1 ui->CustomPlot->yAxis->setSubTickLength(4, 0); // 内侧0,外侧4 ui->CustomPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); // 设置刻度标签颜色为白色 ui->CustomPlot->xAxis->setTickLabelColor(Qt::white); // x轴刻度文字 ui->CustomPlot->yAxis->setTickLabelColor(Qt::white); // y轴刻度文字 // 设置坐标轴标题(轴标签)颜色为白色 ui->CustomPlot->xAxis->setLabelColor(Qt::white); // x轴标题 ui->CustomPlot->yAxis->setLabelColor(Qt::white); // y轴标题 // 设置图例文字颜色为白色 ui->CustomPlot->legend->setTextColor(Qt::white); // 应用设置 ui->CustomPlot->replot(); ui->CustomPlot->addGraph();//添加一个图表曲线 ui->CustomPlot->graph(0)->setPen(QPen(Qt::yellow));//设置曲线颜色为蓝色 ui->CustomPlot->graph(0)->setName("X 角度 (°)"); ui->CustomPlot->addGraph(); ui->CustomPlot->graph(1)->setPen(QPen(Qt::red)); ui->CustomPlot->graph(1)->setName("Y 角度 (°)"); ui->CustomPlot->addGraph(); ui->CustomPlot->graph(2)->setPen(QPen(Qt::green)); ui->CustomPlot->graph(2)->setName("Z 角度 (°)"); //设置X轴为时间轴 Y轴为角度轴 ui->CustomPlot->xAxis->setLabel("时间 (秒)"); ui->CustomPlot->yAxis->setLabel("角度 (°)"); //设置Y轴范围(根据角度传感器的正常范围设置) ui->CustomPlot->yAxis->setRange(-360, 360); //添加图例,显示曲线名称 ui->CustomPlot->legend->setVisible(true); //启用鼠标交互:拖拽和缩放 ui->CustomPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); //初始化数据存储 currentTime = 0.0;//从零秒开始 //点击首页 返回首页 connect(ui->toolButton_2,&QToolButton::clicked,[=](){ //告诉主场景 我返回了 emit this->chuanganqBack_1(); }); c_2 = new Chuanganqi_2; // 连接"传感器2"按钮到信号 connect(ui->toolButton_4, &QToolButton::clicked, [=]() { emit switchToChild2(); // 发送切换请求 this->hide(); // 隐藏当前窗口 }); //监听C_2的返回C_1按钮的信号 connect(c_2,&Chuanganqi_2::chuanganqBack_C_1,[=](){ c_2->hide();//将选择关卡场景 隐藏掉 this->show();//重新显示主场景 }); // 连接信号:串口响应 -> 显示函数 connect(this, &Chuanganqi_1::serialResponseReceived, this, &Chuanganqi_1::displayResponse); //角度更新定时器相关初始化 angleUpdateTimer = new QTimer(this); connect(angleUpdateTimer, &QTimer::timeout, this, &Chuanganqi_1::startAngleUpdate); // 连接鼠标点击信号 connect(ui->CustomPlot, &QCustomPlot::mousePress, this, &Chuanganqi_1::onCustomPlotClick); // 初始化标记 clickMarker = new QCPItemText(ui->CustomPlot); clickMarker->setVisible(false); clickMarker->setColor(Qt::white); // 设置文本颜色为白色,适应深色背景 clickMarker->setFont(QFont("SimHei", 10)); // 设置字体,确保中文显示 // 初始化圆点 clickDot = new QCPItemEllipse(ui->CustomPlot); clickDot->setVisible(false); clickDot->setPen(QPen(Qt::red,2)); // 设置圆点的颜色 clickDot->setBrush(QBrush(Qt::red)); // 设置圆点的填充颜色 // 设置椭圆相对中心的偏移(形成5x5的圆点) clickDot->topLeft->setCoords(-2.5, -2.5); clickDot->bottomRight->setCoords(2.5, 2.5); // clickDot->topLeft->setCoords(0, 0); // clickDot->bottomRight->setCoords(0, 0); // 初始化标志位 isButtonClicked = false; isFirstDataDisplayed = false; firstSecondTimer = new QTimer(this); connect(firstSecondTimer, &QTimer::timeout, this, &Chuanganqi_1::resetFirstDataFlag); // 连接鼠标点击和释放信号(新增部分) connect(ui->CustomPlot, &QCustomPlot::mousePress, this, &Chuanganqi_1::onCustomPlotClick); connect(ui->CustomPlot, &QCustomPlot::mouseRelease, this, &Chuanganqi_1::onCustomPlotRelease); } Chuanganqi_1::~Chuanganqi_1() { delete ui; } // 清空数据并重新开始的实现 void Chuanganqi_1::clearDataAndRestart() { // 清空角度数据 timeData.clear(); angleXData.clear(); angleYData.clear(); angleZData.clear(); allAngleData.clear(); // 重置当前时间 currentTime = 0.0; // 清空图表 ui->CustomPlot->graph(0)->setData(QVector<double>(), QVector<double>()); ui->CustomPlot->graph(1)->setData(QVector<double>(), QVector<double>()); ui->CustomPlot->graph(2)->setData(QVector<double>(), QVector<double>()); ui->CustomPlot->replot(); // 清空文本显示 ui->textEdit->clear(); ui->lineEdit->clear(); ui->lineEdit_2->clear(); ui->lineEdit_3->clear(); } void Chuanganqi_1::on_pushButton_clicked() { clearDataAndRestart(); // 调用清空数据并重新开始的函数 angleUpdateTimer->start(); // 重新开始角度更新 emit startDataTransmission(); // 发射开始数据传输信号 // 设置标志位为 true isButtonClicked = true; isFirstDataDisplayed = false; firstSecondTimer->start(1000); // 启动计时器,计时一秒 } //接收数据的槽函数 void Chuanganqi_1::displayResponse(const QString &response) { // 如果按钮未点击,忽略接收到的数据 if (!isButtonClicked) { return; } // 如果在点击按钮后的第一秒内已经显示了一个数据,忽略后续数据 if (firstSecondTimer->isActive() && isFirstDataDisplayed) { return; } qDebug() << "子窗口收到响应:" << response; // 获取当前时间 QDateTime currentDateTime = QDateTime::currentDateTime(); QString timeString = currentDateTime.toString("yyyy-MM-dd HH:mm:ss"); // 提取角度值并显示 QRegularExpression re("X:([+-]?\\d+\\.?\\d*) Y:([+-]?\\d+\\.?\\d*) Z:([+-]?\\d+\\.?\\d*)"); QRegularExpressionMatch match = re.match(response); if (match.hasMatch()) { bool okX, okY, okZ; double angleX = match.captured(1).toDouble(&okX); double angleY = match.captured(2).toDouble(&okY); double angleZ = match.captured(3).toDouble(&okZ); if (okX && okY && okZ) { // 先存储数据到列表中 AngleData data; data.timestamp = currentDateTime; data.angleX = angleX; data.angleY = angleY; data.angleZ = angleZ; allAngleData.append(data); // 显示数据到lineEdit ui->lineEdit->setText(QString::number(angleX)); ui->lineEdit_2->setText(QString::number(angleY)); ui->lineEdit_3->setText(QString::number(angleZ)); // 更新图表 updateAnglePlot(); // 将XYZ数据和时间显示在textEdit中 QString line = QString("%1 : X: %2°, Y: %3°, Z: %4° \n").arg(timeString).arg(angleX).arg(angleY).arg(angleZ); ui->textEdit->append(line); // 设置标志位为 true,表示已经显示了一个数据 isFirstDataDisplayed = true; } else { qDebug() << "角度转换失败,提取值:" << match.captured(1) << match.captured(2) << match.captured(3); } } else { qDebug() << "未找到角度值,原始响应:" << response; } } // 用于重置标志位 void Chuanganqi_1::resetFirstDataFlag() { isFirstDataDisplayed = false; } // 更新角度波形图 void Chuanganqi_1::updateAnglePlot() { //确保timeData和angleData与allAngleData保持同步 currentTime = allAngleData.size(); // 使用数据点数量作为时间戳 //清空并重新填充波形数据,确保与allAngleData一致 timeData.clear(); angleXData.clear(); angleYData.clear(); angleZData.clear(); for (int i = 0; i < allAngleData.size(); ++i) { timeData.append(i); angleXData.append(allAngleData[i].angleX); angleYData.append(allAngleData[i].angleY); angleZData.append(allAngleData[i].angleZ); } // 更新图表数据 ui->CustomPlot->graph(0)->setData(timeData, angleXData); ui->CustomPlot->graph(1)->setData(timeData, angleYData); ui->CustomPlot->graph(2)->setData(timeData, angleZData); // 调整X轴范围 if (timeData.size() <= 30) { ui->CustomPlot->xAxis->setRange(0, 30); } else { ui->CustomPlot->xAxis->setRange(currentTime, 30, Qt::AlignRight); } ui->CustomPlot->replot(); // 确保标记在数据更新时保持可见(如果之前已显示) if (clickDot->visible() || clickMarker->visible()) { ui->CustomPlot->replot(); } } //开始角度更新 void Chuanganqi_1::startAngleUpdate() { angleUpdateTimer->start(1000); // 每秒更新一次 } //处理鼠标点击事件 void Chuanganqi_1::onCustomPlotClick(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { // 如果按下Ctrl键,才进行标点(避免与拖动冲突) if (event->modifiers() & Qt::ControlModifier) { double clickX = ui->CustomPlot->xAxis->pixelToCoord(event->pos().x()); double clickY = ui->CustomPlot->yAxis->pixelToCoord(event->pos().y()); // 找到最接近的点 int closestIndex = 0; double minDistance = std::numeric_limits<double>::max(); for (int i = 0; i < timeData.size(); ++i) { // 计算三维距离(同时考虑X、Y、Z角度) double distance = std::sqrt( std::pow(timeData[i] - clickX, 2) + std::pow(angleXData[i] - clickY, 2) ); if (distance < minDistance) { minDistance = distance; closestIndex = i; } } // 更新标记位置和文本 double closestX = timeData[closestIndex]; double closestY = angleXData[closestIndex]; QDateTime pointTime = allAngleData[closestIndex].timestamp; clickDot->setVisible(true); clickMarker->setVisible(true); clickMarker->position->setCoords(closestX, closestY); clickMarker->setText(QString("时间: %1\nX: %2°\nY: %3°\nZ: %4°") .arg(pointTime.toString("HH:mm:ss")) .arg(angleXData[closestIndex], 0, 'f', 2) .arg(angleYData[closestIndex], 0, 'f', 2) .arg(angleZData[closestIndex], 0, 'f', 2)); ui->CustomPlot->replot(); } } } // 新增:处理鼠标释放事件(用于拖动结束后重绘) void Chuanganqi_1::onCustomPlotRelease(QMouseEvent *event) { // 拖动或缩放后重绘标记,确保位置正确 if (clickDot->visible() || clickMarker->visible()) { ui->CustomPlot->replot(); } } // 导出数据 void Chuanganqi_1::on_pushButton_2_clicked() { emit stopDataTransmission(); // 发射停止数据传输信号 // 弹出文件保存对话框,让用户选择保存位置和文件名 QString filePath = QFileDialog::getSaveFileName( this, tr("保存角度数据"), QDir::homePath() + "/角度数据_" + QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss") + ".csv", tr("CSV文件 (*.csv);;所有文件 (*)")); if (filePath.isEmpty()) { return; } QFile file(filePath); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { // 使用标准C++文件流确保编码一致性 FILE *fp = fopen(filePath.toLocal8Bit().constData(), "wb"); // 写入UTF-8 BOM标记,确保Excel正确识别编码 const char *bom = "\xEF\xBB\xBF"; fwrite(bom, 1, 3, fp); // 写入表头 const char *header = "时间,X角度(°),Y角度(°),Z角度(°)\n"; fwrite(header, 1, strlen(header), fp); // 写入数据行 for (const auto &data : allAngleData) { QString line = QString("%1,%2,%3,%4\n") .arg(data.timestamp.toString("HH:mm:ss")) // 完整时间格式 .arg(data.angleX, 0, 'f', 2) // 保留两位小数 .arg(data.angleY, 0, 'f', 2) .arg(data.angleZ, 0, 'f', 2); // 使用fwrite避免Qt内部编码转换 QByteArray utf8Line = line.toUtf8(); fwrite(utf8Line.constData(), 1, utf8Line.length(), fp); } fclose(fp); file.close(); qDebug() << "角度数据已保存到" << filePath; // 显示保存成功消息 QMessageBox::information(this, tr("保存成功"), tr("角度数据已成功保存到:\n%1").arg(filePath)); } } 以下是chuanganqi_1.h文件的内容 #ifndef CHUANGANQI_1_H #define CHUANGANQI_1_H #include <QMainWindow> #include <QTimer> #include "qcustomplot.h" #include <QDateTime> #include <QFileDialog> class Chuanganqi_2; // 定义结构体用于存储角度数据和时间戳 struct AngleData { QDateTime timestamp; double angleX; double angleY; double angleZ; }; QT_BEGIN_NAMESPACE namespace Ui { class Chuanganqi_1; } QT_END_NAMESPACE class Chuanganqi_1 : public QMainWindow { Q_OBJECT public: explicit Chuanganqi_1(QWidget *parent = nullptr); ~Chuanganqi_1(); Chuanganqi_2 *c_2; private: Ui::Chuanganqi_1 *ui; // 数据更新定时器 QTimer *dataTimer; // 时间和角度数据存储 QVector<double> timeData; QVector<double> angleXData; QVector<double> angleYData; QVector<double> angleZData; // 当前时间(秒) double currentTime; QTimer *debounceTimer; QString pendingCommand; // 角度更新定时器 QTimer *angleUpdateTimer; // 点击标记 QCPItemText * clickMarker;// 文本标记 // 点击位置的圆点 QCPItemEllipse * clickDot;// 圆点标记 bool isDraggingMarker; // 标记是否正在被拖动(用于拖动功能) // 用于存储所有角度数据和时间戳的列表 QList<AngleData> allAngleData; // 标志位,pushButton是否被点击 bool isButtonClicked; // 标志位,用于记录是否已经显示了一个数据 bool isFirstDataDisplayed; // 计时器,用于记录点击按钮后的第一秒 QTimer *firstSecondTimer; signals: // 写一个自定义信号 告诉主场景点击了返回 void chuanganqBack_1(); void chuanganqBack_C_2(); // 请求切换到子窗口2 void switchToChild2(); // 子窗口发送指令 void sendCommand(const QString &command1); void serialResponseReceived(const QString &response); // 主界面开始和停止传输数据的函数 void startDataTransmission(); void stopDataTransmission(); private slots: // 鼠标事件处理槽函数(新增 void onCustomPlotRelease(QMouseEvent *event); void on_pushButton_clicked(); //接收数据 void displayResponse(const QString &response); // 定时更新角度波形的槽函数 void updateAnglePlot(); // 开始角度更新的槽函数 void startAngleUpdate(); // 处理鼠标点击事件 void onCustomPlotClick(QMouseEvent * event); void on_pushButton_2_clicked(); // 用于清空数据并重新开始 void clearDataAndRestart(); // 用于重置标志位 void resetFirstDataFlag(); }; #endif // CHUANGANQI_1_H 修改chuanganqi_1.cpp和chuanganqi_1.h文件中的代码,实现鼠标在CustomPlot部件中鼠标点击标识数据,缩放、拖拽功能,不管customplot内是否有曲线都可以点击标识,但是需要按住ctrl键才可点击标识

# -*- coding: utf-8 -*- import sys import os import cv2 import numpy as np from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QMessageBox, QLabel, QFileDialog, QToolBar, QComboBox, QStatusBar, QGroupBox, QSlider, QDockWidget, QProgressDialog) from PyQt5.QtCore import QRect, Qt, QSettings, QThread, pyqtSignal from CamOperation_class import CameraOperation sys.path.append("D:\\海康\\MVS\\Development\\Samples\\Python\\BasicDemo") from MvCameraControl_class import * from MvErrorDefine_const import * from CameraParams_header import * from PyUICBasicDemo import Ui_MainWindow import ctypes from datetime import datetime import logging import platform # 配置日志系统 logging.basicConfig( level=logging.DEBUG, # 设置为DEBUG级别获取更多信息 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("cloth_inspection_debug.log"), logging.StreamHandler() ] ) logging.info("布料印花检测系统启动") # 全局变量 current_sample_path = "" # 当前使用的样本路径 detection_history = [] # 检测历史记录 # 帧监控线程 class FrameMonitorThread(QThread): frame_status = pyqtSignal(str) def __init__(self, cam_operation): super().__init__() self.cam_operation = cam_operation self.running = True def run(self): while self.running: if self.cam_operation: status = self.cam_operation.get_frame_status() frame_text = "有帧" if status.get('current_frame', False) else "无帧" self.frame_status.emit(f"帧状态: {frame_text}") QThread.msleep(500) def stop(self): self.running = False # 布料印花检测函数(修复版) def check_print_quality(sample_image_path, test_image, threshold=0.05): """ 检测布料印花是否合格,直接使用内存中的测试图像 :param sample_image_path: 合格样本图像路径 :param test_image: 内存中的测试图像 (numpy数组) :param threshold: 差异阈值 :return: 是否合格,差异值,标记图像 """ # 读取样本图像 try: sample_img_data = np.fromfile(sample_image_path, dtype=np.uint8) sample_image = cv2.imdecode(sample_img_data, cv2.IMREAD_GRAYSCALE) if sample_image is None: logging.error(f"无法解码样本图像: {sample_image_path}") return None, None, None except Exception as e: logging.exception(f"样本图像读取异常: {str(e)}") return None, None, None # 确保测试图像是灰度图 if len(test_image.shape) == 3: # 如果是彩色图像 test_image = cv2.cvtColor(test_image, cv2.COLOR_BGR2GRAY) # 确保两个图像大小一致 try: test_image = cv2.resize(test_image, (sample_image.shape[1], sample_image.shape[0])) except Exception as e: logging.error(f"图像调整大小失败: {str(e)}") return None, None, None # 计算差异 diff = cv2.absdiff(sample_image, test_image) # 二值化差异 _, thresholded = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) # 计算差异比例 diff_pixels = np.count_nonzero(thresholded) total_pixels = sample_image.size diff_ratio = diff_pixels / total_pixels # 判断是否合格 is_qualified = diff_ratio <= threshold # 创建标记图像(红色标记差异区域) marked_image = cv2.cvtColor(test_image, cv2.COLOR_GRAY2BGR) marked_image[thresholded == 255] = [0, 0, 255] # 红色标记 return is_qualified, diff_ratio, marked_image # 更新检测结果显示 def update_diff_display(diff_ratio, is_qualified): """ 更新差异度显示控件 """ # 更新当前差异度显示 ui.lblCurrentDiff.setText(f"当前差异度: {diff_ratio*100:.2f}%") # 根据合格状态设置颜色 if is_qualified: ui.lblDiffStatus.setText("状态: 合格") ui.lblDiffStatus.setStyleSheet("color: green; font-size: 12px;") else: ui.lblDiffStatus.setText("状态: 不合格") ui.lblDiffStatus.setStyleSheet("color: red; font-size: 12px;") # 更新差异度阈值显示 def update_diff_threshold(value): """ 当滑块值改变时更新阈值显示 """ ui.lblDiffValue.setText(f"{value}%") # 布料印花检测功能(修复版) def check_print(): global isGrabbing, obj_cam_operation, current_sample_path, detection_history logging.info("检测印花质量按钮按下") # 1. 检查相机状态 if not isGrabbing: logging.warning("相机未取流") QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 2. 检查相机操作对象 if not obj_cam_operation: logging.error("相机操作对象未初始化") QMessageBox.warning(mainWindow, "错误", "相机未正确初始化!", QMessageBox.Ok) return # 3. 检查样本路径 if not current_sample_path or not os.path.exists(current_sample_path): logging.warning(f"无效样本路径: {current_sample_path}") QMessageBox.warning(mainWindow, "错误", "请先设置有效的标准样本图像!", QMessageBox.Ok) return # 使用进度对话框防止UI阻塞 progress = QProgressDialog("正在检测...", "取消", 0, 100, mainWindow) progress.setWindowModality(Qt.WindowModal) progress.setValue(10) try: # 4. 获取当前帧 logging.info("尝试获取当前帧") test_image = obj_cam_operation.get_current_frame() progress.setValue(30) if test_image is None: logging.warning("获取当前帧失败") QMessageBox.warning(mainWindow, "错误", "无法获取当前帧图像!", QMessageBox.Ok) return # 5. 获取差异度阈值 diff_threshold = ui.sliderDiffThreshold.value() / 100.0 logging.info(f"使用差异度阈值: {diff_threshold}") progress.setValue(50) # 6. 执行检测 is_qualified, diff_ratio, marked_image = check_print_quality( current_sample_path, test_image, threshold=diff_threshold ) progress.setValue(70) # 检查返回结果是否有效 if is_qualified is None: logging.error("检测函数返回无效结果") QMessageBox.critical(mainWindow, "检测错误", "检测失败,请检查日志", QMessageBox.Ok) return logging.info(f"检测结果: 合格={is_qualified}, 差异={diff_ratio}") progress.setValue(90) # 7. 更新UI update_diff_display(diff_ratio, is_qualified) result_text = f"印花是否合格: {'合格' if is_qualified else '不合格'}\n差异占比: {diff_ratio*100:.2f}%\n阈值: {diff_threshold*100:.2f}%" QMessageBox.information(mainWindow, "检测结果", result_text, QMessageBox.Ok) if marked_image is not None: cv2.imshow("缺陷标记结果", marked_image) cv2.waitKey(0) cv2.destroyAllWindows() else: logging.warning("标记图像为空") # 8. 记录检测结果 detection_result = { 'timestamp': datetime.now(), 'qualified': is_qualified, 'diff_ratio': diff_ratio, 'threshold': diff_threshold } detection_history.append(detection_result) update_history_display() progress.setValue(100) except Exception as e: logging.exception("印花检测失败") QMessageBox.critical(mainWindow, "检测错误", f"检测过程中发生错误: {str(e)}", QMessageBox.Ok) finally: progress.close() # 保存标准样本函数 def save_sample_image(): global isGrabbing, obj_cam_operation, current_sample_path if not isGrabbing: QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 检查是否有有效图像 if not obj_cam_operation.is_frame_available(): QMessageBox.warning(mainWindow, "无有效图像", "未捕获到有效图像,请检查相机状态!", QMessageBox.Ok) return # 读取上次使用的路径 settings = QSettings("ClothInspection", "CameraApp") last_dir = settings.value("last_save_dir", os.path.join(os.getcwd(), "captures")) # 创建默认文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") default_filename = f"sample_{timestamp}" # 弹出文件保存对话框 file_path, selected_filter = QFileDialog.getSaveFileName( mainWindow, "保存标准样本图像", os.path.join(last_dir, default_filename), "BMP Files (*.bmp);;PNG Files (*.png);;JPEG Files (*.jpg);;所有文件 (*)", options=QFileDialog.DontUseNativeDialog ) if not file_path: logging.info("用户取消了图像保存操作") return # 用户取消保存 # 处理文件扩展名 file_extension = os.path.splitext(file_path)[1].lower() if not file_extension: # 根据选择的过滤器添加扩展名 if "BMP" in selected_filter: file_path += ".bmp" elif "PNG" in selected_filter: file_path += ".png" elif "JPEG" in selected_filter or "JPG" in selected_filter: file_path += ".jpg" else: # 默认使用BMP格式 file_path += ".bmp" file_extension = os.path.splitext(file_path)[1].lower() # 根据扩展名设置保存格式 format_mapping = { ".bmp": "bmp", ".png": "png", ".jpg": "jpg", ".jpeg": "jpg" } save_format = format_mapping.get(file_extension) if not save_format: QMessageBox.warning(mainWindow, "错误", "不支持的文件格式!", QMessageBox.Ok) return # 确保目录存在 directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): try: os.makedirs(directory, exist_ok=True) logging.info(f"创建目录: {directory}") except OSError as e: error_msg = f"无法创建目录 {directory}: {str(e)}" QMessageBox.critical(mainWindow, "目录创建错误", error_msg, QMessageBox.Ok) return # 保存当前帧作为标准样本 try: ret = obj_cam_operation.save_image(file_path, save_format) if ret != MV_OK: strError = f"保存样本图像失败: {hex(ret)}" QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) else: success_msg = f"标准样本已保存至:\n{file_path}" QMessageBox.information(mainWindow, "成功", success_msg, QMessageBox.Ok) # 更新当前样本路径 current_sample_path = file_path update_sample_display() # 保存当前目录 settings.setValue("last_save_dir", os.path.dirname(file_path)) except Exception as e: error_msg = f"保存图像时发生错误: {str(e)}" QMessageBox.critical(mainWindow, "异常错误", error_msg, QMessageBox.Ok) logging.exception("保存样本图像时发生异常") # 预览当前样本 def preview_sample(): global current_sample_path if not current_sample_path or not os.path.exists(current_sample_path): QMessageBox.warning(mainWindow, "错误", "请先设置有效的标准样本图像!", QMessageBox.Ok) return try: # 使用安全方法读取图像 img_data = np.fromfile(current_sample_path, dtype=np.uint8) sample_img = cv2.imdecode(img_data, cv2.IMREAD_COLOR) if sample_img is None: raise Exception("无法加载图像") cv2.imshow("标准样本预览", sample_img) cv2.waitKey(0) cv2.destroyAllWindows() except Exception as e: QMessageBox.warning(mainWindow, "错误", f"预览样本失败: {str(e)}", QMessageBox.Ok) # 更新样本路径显示 def update_sample_display(): global current_sample_path if current_sample_path: ui.lblSamplePath.setText(f"当前样本: {os.path.basename(current_sample_path)}") ui.lblSamplePath.setToolTip(current_sample_path) ui.bnPreviewSample.setEnabled(True) else: ui.lblSamplePath.setText("当前样本: 未设置样本") ui.bnPreviewSample.setEnabled(False) # 更新历史记录显示 def update_history_display(): global detection_history ui.cbHistory.clear() for i, result in enumerate(detection_history[-10:]): # 显示最近10条记录 timestamp = result['timestamp'].strftime("%H:%M:%S") status = "合格" if result['qualified'] else "不合格" ratio = f"{result['diff_ratio']*100:.2f}%" ui.cbHistory.addItem(f"[{timestamp}] {status} - 差异: {ratio}") # 获取选取设备信息的索引,通过[]之间的字符去解析 def TxtWrapBy(start_str, end, all): start = all.find(start_str) if start >= 0: start += len(start_str) end = all.find(end, start) if end >= 0: return all[start:end].strip() # 将返回的错误码转换为十六进制显示 def ToHexStr(num): """将错误码转换为十六进制字符串""" # 处理非整数输入 if not isinstance(num, int): try: # 尝试转换为整数 num = int(num) except: # 无法转换时返回类型信息 return f"<非整数:{type(num)}>" chaDic = {10: 'a', 11: 'b', 12: 'c', 13: 'd', 14: 'e', 15: 'f'} hexStr = "" # 处理负数 if num < 0: num = num + 2 ** 32 # 转换为十六进制 while num >= 16: digit = num % 16 hexStr = chaDic.get(digit, str(digit)) + hexStr num //= 16 hexStr = chaDic.get(num, str(num)) + hexStr return "0x" + hexStr # ch:初始化SDK | en: initialize SDK MvCamera.MV_CC_Initialize() global deviceList deviceList = MV_CC_DEVICE_INFO_LIST() global cam cam = MvCamera() global nSelCamIndex nSelCamIndex = 0 global obj_cam_operation obj_cam_operation = 0 global isOpen isOpen = False global isGrabbing isGrabbing = False global isCalibMode # 是否是标定模式(获取原始图像) isCalibMode = True global frame_monitor_thread # 绑定下拉列表至设备信息索引 def xFunc(event): global nSelCamIndex nSelCamIndex = TxtWrapBy("[", "]", ui.ComboDevices.get()) # Decoding Characters def decoding_char(c_ubyte_value): c_char_p_value = ctypes.cast(c_ubyte_value, ctypes.c_char_p) try: decode_str = c_char_p_value.value.decode('gbk') # Chinese characters except UnicodeDecodeError: decode_str = str(c_char_p_value.value) return decode_str # ch:枚举相机 | en:enum devices def enum_devices(): global deviceList global obj_cam_operation deviceList = MV_CC_DEVICE_INFO_LIST() n_layer_type = (MV_GIGE_DEVICE | MV_USB_DEVICE | MV_GENTL_CAMERALINK_DEVICE | MV_GENTL_CXP_DEVICE | MV_GENTL_XOF_DEVICE) ret = MvCamera.MV_CC_EnumDevices(n_layer_type, deviceList) if ret != 0: strError = "Enum devices fail! ret = :" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) return ret if deviceList.nDeviceNum == 0: QMessageBox.warning(mainWindow, "Info", "Find no device", QMessageBox.Ok) return ret print("Find %d devices!" % deviceList.nDeviceNum) devList = [] for i in range(0, deviceList.nDeviceNum): mvcc_dev_info = cast(deviceList.pDeviceInfo[i], POINTER(MV_CC_DEVICE_INFO)).contents if mvcc_dev_info.nTLayerType == MV_GIGE_DEVICE or mvcc_dev_info.nTLayerType == MV_GENTL_GIGE_DEVICE: print("\ngige device: [%d]" % i) user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stGigEInfo.chUserDefinedName) model_name = decoding_char(mvcc_dev_info.SpecialInfo.stGigEInfo.chModelName) print("device user define name: " + user_defined_name) print("device model name: " + model_name) nip1 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0xff000000) >> 24) nip2 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x00ff0000) >> 16) nip3 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x0000ff00) >> 8) nip4 = (mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x000000ff) print("current ip: %d.%d.%d.%d " % (nip1, nip2, nip3, nip4)) devList.append( "[" + str(i) + "]GigE: " + user_defined_name + " " + model_name + "(" + str(nip1) + "." + str( nip2) + "." + str(nip3) + "." + str(nip4) + ")") elif mvcc_dev_info.nTLayerType == MV_USB_DEVICE: print("\nu3v device: [%d]" % i) user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stUsb3VInfo.chUser极DefinedName) model_name = decoding_char(mvcc_dev_info.SpecialInfo.stUsb3VInfo.chModelName) print("device user define name: " + user_defined_name) print("device model name: " + model_name) strSerialNumber = "" for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chSerialNumber: if per == 0: break strSerialNumber = strSerialNumber + chr(per) print("user serial number: " + strSerialNumber) devList.append("[" + str(i) + "]USB: " + user_defined_name + " " + model_name + "(" + str(strSerialNumber) + ")") elif mvcc_dev_info.nTLayerType == MV_GENTL_CAMERALINK_DEVICE: print("\nCML device: [%d]" % i) user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stCMLInfo.chUserDefinedName) model_name = decoding_char(mvcc_dev_info.SpecialInfo.stCMLInfo.chModelName) print("device user define name: " + user_defined_name) print("device model name: " + model_name) strSerialNumber = "" for per in mvcc_dev_info.SpecialInfo.stCMLInfo.chSerialNumber: if per == 0: break strSerialNumber = strSerialNumber + chr(per) print("user serial number: " + strSerialNumber) devList.append("[" + str(i) + "]CML: " + user_defined_name + " " + model_name + "(" + str(strSerialNumber) + ")") elif mvcc_dev_info.nTLayerType == MV_GENTL_CXP_DEVICE: print("\nCXP device: [%d]" % i) user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stCXPInfo.chUserDefinedName) model_name = decoding_char(mvcc_dev_info.SpecialInfo.stCXPInfo.chModelName) print("device user define name: " + user_defined_name) print("device model name: " + model_name) strSerialNumber = "" for per in mvcc_dev_info.SpecialInfo.stCXPInfo.chSerialNumber: if per == 0: break strSerialNumber = strSerialNumber + chr(per) print("user serial number: "+strSerialNumber) devList.append("[" + str(i) + "]CXP: " + user_defined_name + " " + model_name + "(" + str(strSerialNumber) + ")") elif mvcc_dev_info.nTLayerType == MV_GENTL_XOF_DEVICE: print("\nXoF device: [%d]" % i) user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stXoFInfo.chUserDefinedName) model_name = decoding_char(mvcc_dev_info.SpecialInfo.stXoFInfo.chModelName) print("device user define name: " + user_defined_name) print("device model name: " + model_name) strSerialNumber = "" for per in mvcc_dev_info.SpecialInfo.stXoFInfo.chSerialNumber: if per == 0: break strSerialNumber = strSerialNumber + chr(per) print("user serial number: " + strSerialNumber) devList.append("[" + str(i) + "]XoF: " + user_defined_name + " " + model_name + "(" + str(strSerialNumber) + ")") ui.ComboDevices.clear() ui.ComboDevices.addItems(devList) ui.ComboDevices.setCurrentIndex(0) # ch:打开相机 | en:open device def open_device(): global deviceList global nSelCamIndex global obj_cam_operation global isOpen global frame_monitor_thread if isOpen: QMessageBox.warning(mainWindow, "Error", 'Camera is Running!', QMessageBox.Ok) return MV_E_CALLORDER nSelCamIndex = ui.ComboDevices.currentIndex() if nSelCamIndex < 0: QMessageBox.warning(mainWindow, "Error", 'Please select a camera!', QMessageBox.Ok) return MV_E_CALLORDER obj_cam_operation = CameraOperation(cam, deviceList, nSelCamIndex) ret = obj_cam_operation.open_device() if 0 != ret: strError = "Open device failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) isOpen = False else: set_continue_mode() get_param() isOpen = True enable_controls() # 启动帧监控线程 frame_monitor_thread = FrameMonitorThread(obj_cam_operation) frame_monitor_thread.frame_status.connect(ui.statusBar.showMessage) frame_monitor_thread.start() # ch:开始取流 | en:Start grab image def start_grabbing(): global obj_cam_operation global isGrabbing ret = obj_cam_operation.start_grabbing(ui.widgetDisplay.winId()) if ret != 0: strError = "Start grabbing failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: isGrabbing = True enable_controls() # ch:停止取流 | en:Stop grab image def stop_grabbing(): global obj_cam_operation global isGrabbing ret = obj_cam_operation.Stop_grabbing() if ret != 0: strError = "Stop grabbing failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: isGrabbing = False enable_controls() # ch:关闭设备 | Close device def close_device(): global isOpen global isGrabbing global obj_cam_operation global frame_monitor_thread # 停止帧监控线程 if frame_monitor_thread and frame_monitor_thread.isRunning(): frame_monitor_thread.stop() frame_monitor_thread.wait(2000) if isOpen: obj_cam_operation.close_device() isOpen = False isGrabbing = False enable_controls() # ch:设置触发模式 | en:set trigger mode def set_continue_mode(): ret = obj_cam_operation.set_trigger_mode(False) if ret != 0: strError = "Set continue mode failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: ui.radioContinueMode.setChecked(True) ui.radioTriggerMode.setChecked(False) ui.bnSoftwareTrigger.setEnabled(False) # ch:设置软触发模式 | en:set software trigger mode def set_software_trigger_mode(): ret = obj_cam_operation.set_trigger_mode(True) if ret != 0: strError = "Set trigger mode failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: ui.radioContinueMode.setChecked(False) ui.radioTriggerMode.setChecked(True) ui.bnSoftwareTrigger.setEnabled(isGrabbing) # ch:设置触发命令 | en:set trigger software def trigger_once(): ret = obj_cam_operation.trigger_once() if ret != 0: strError = "TriggerSoftware failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) # 保存图像对话框 def save_image_dialog(): """ 打开保存图像对话框并保存当前帧 """ global isGrabbing, obj_cam_operation # 检查相机状态 if not isGrabbing: QMessageBox.warning(mainWindow, "相机未就绪", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 检查是否有有效图像 if not obj_cam_operation.is_frame_available(): QMessageBox.warning(mainWindow, "无有效图像", "未捕获到有效图像,请检查相机状态!", QMessageBox.Ok) return # 读取上次使用的路径 settings = QSettings("ClothInspection", "CameraApp") last_dir = settings.value("last_save_dir", os.path.join(os.getcwd(), "captures")) # 创建默认文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") default_filename = f"capture_{timestamp}" # 弹出文件保存对话框 file_path, selected_filter = QFileDialog.getSaveFileName( mainWindow, "保存图像", os.path.join(last_dir, default_filename), # 初始路径 "BMP 图像 (*.bmp);;JPEG 图像 (*.jpg);;PNG 图像 (*.png);;TIFF 图像 (*.tiff);;所有文件 (*)", options=QFileDialog.DontUseNativeDialog ) # 用户取消操作 if not file_path: logging.info("用户取消了图像保存操作") return # 处理文件扩展名 file_extension = os.path.splitext(file_path)[1].lower() if not file_extension: # 根据选择的过滤器添加扩展名 if "BMP" in selected_filter: file_path += ".bmp" elif "JPEG" in selected_filter or "JPG" in selected_filter: file_path += ".jpg" elif "PNG" in selected_filter: file_path += ".png" elif "TIFF" in selected_filter: file_path += ".tiff" else: # 默认使用BMP格式 file_path += ".bmp" # 确定保存格式 format_mapping = { ".bmp": "bmp", ".jpg": "jpg", ".jpeg": "jpg", ".png": "png", ".tiff": "tiff", ".tif": "tiff" } file_extension = os.path.splitext(file_path)[1].lower() save_format = format_mapping.get(file_extension, "bmp") # 确保目录存在 directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): try: os.makedirs(directory, exist_ok=True) except OSError as e: QMessageBox.critical(mainWindow, "目录错误", f"无法创建目录:\n{str(e)}", QMessageBox.Ok) return # 保存图像 try: ret = obj_cam_operation.save_image(file_path, save_format) if ret == MV_OK: QMessageBox.information(mainWindow, "保存成功", f"图像已保存至:\n{file_path}", QMessageBox.Ok) logging.info(f"图像保存成功: {file_path}") # 保存当前目录 settings.setValue("last_save_dir", os.path.dirname(file_path)) else: error_msg = f"保存失败! 错误代码: {hex(ret)}" QMessageBox.warning(mainWindow, "保存失败", error_msg, QMessageBox.Ok) logging.error(f"图像保存失败: {file_path}, 错误代码: {hex(ret)}") except Exception as e: QMessageBox.critical(mainWindow, "保存错误", f"保存图像时发生错误:\n{str(e)}", QMessageBox.Ok) logging.exception(f"保存图像时发生异常: {file_path}") def is_float(str): try: float(str) return True except ValueError: return False # ch: 获取参数 | en:get param def get_param(): try: # 调用方法获取参数 ret = obj_cam_operation.get_parameters() # 记录调用结果(调试用) logging.debug(f"get_param() 返回: {ret} (类型: {type(ret)})") # 处理错误码 if ret != MV_OK: strError = "获取参数失败,错误码: " + ToHexStr(ret) QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) else: # 成功获取参数后更新UI ui.edtExposureTime.setText("{0:.2f}".format(obj_cam_operation.exposure_time)) ui.edtGain.setText("{0:.2f}".format(obj_cam_operation.gain)) ui.edtFrameRate.setText("{0:.2f}".format(obj_cam_operation.frame_rate)) # 记录成功信息 logging.info("成功获取相机参数") except Exception as e: # 处理所有异常 error_msg = f"获取参数时发生错误: {str(e)}" logging.error(error_msg) QMessageBox.critical(mainWindow, "严重错误", error_msg, QMessageBox.Ok) # ch: 设置参数 | en:set param def set_param(): frame_rate = ui.edtFrameRate.text() exposure = ui.edtExposureTime.text() gain = ui.edtGain.text() if not (is_float(frame_rate) and is_float(exposure) and is_float(gain)): strError = "设置参数失败: 参数必须是有效的浮点数" QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) return MV_E_PARAMETER try: # 使用正确的参数顺序和关键字 ret = obj_cam_operation.set_param( frame_rate=float(frame_rate), exposure_time=float(exposure), gain=float(gain) ) if ret != MV_OK: strError = "设置参数失败,错误码: " + ToHexStr(ret) QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) else: logging.info("参数设置成功") return MV_OK except Exception as e: error_msg = f"设置参数时发生错误: {str(e)}" logging.error(error_msg) QMessageBox.critical(mainWindow, "严重错误", error_msg, QMessageBox.Ok) return MV_E_STATE # ch: 设置控件状态 | en:set enable status def enable_controls(): global isGrabbing global isOpen # 先设置group的状态,再单独设置各控件状态 ui.groupGrab.setEnabled(isOpen) ui.groupParam.setEnabled(isOpen) ui.bnOpen.setEnabled(not isOpen) ui.bnClose.setEnabled(isOpen) ui.bnStart.setEnabled(isOpen and (not isGrabbing)) ui.bnStop.setEnabled(isOpen and isGrabbing) ui.bnSoftwareTrigger.setEnabled(isGrabbing and ui.radioTriggerMode.isChecked()) ui.bnSaveImage.setEnabled(isOpen and isGrabbing) # 添加检测按钮控制 ui.bnCheckPrint.setEnabled(isOpen and isGrabbing) ui.bnSaveSample.setEnabled(isOpen and isGrabbing) ui.bnPreviewSample.setEnabled(bool(current_sample_path)) if __name__ == "__main__": # ch:初始化SDK | en: initialize SDK MvCamera.MV_CC_Initialize() deviceList = MV_CC_DEVICE_INFO_LIST() cam = MvCamera() nSelCamIndex = 0 obj_cam_operation = 0 isOpen = False isGrabbing = False isCalibMode = True # 是否是标定模式(获取原始图像) frame_monitor_thread = None # 初始化UI app = QApplication(sys.argv) mainWindow = QMainWindow() ui = Ui_MainWindow() ui.setupUi(mainWindow) # 扩大主窗口尺寸 mainWindow.resize(1200, 800) # 宽度1200,高度800 # 创建工具栏 toolbar = mainWindow.addToolBar("检测工具") # 添加检测按钮 ui.bnCheckPrint = QPushButton("检测印花质量") toolbar.addWidget(ui.bnCheckPrint) # 添加保存样本按钮 ui.bnSaveSample = QPushButton("保存标准样本") toolbar.addWidget(ui.bnSaveSample) # 添加预览样本按钮 ui.bnPreviewSample = QPushButton("预览样本") toolbar.addWidget(ui.bnPreviewSample) # 添加历史记录下拉框 ui.cbHistory = QComboBox() ui.cbHistory.setMinimumWidth(300) toolbar.addWidget(QLabel("历史记录:")) toolbar.addWidget(ui.cbHistory) # 添加当前样本显示标签 ui.lblSamplePath = QLabel("当前样本: 未设置样本") status_bar = mainWindow.statusBar() status_bar.addPermanentWidget(ui.lblSamplePath) # === 新增差异度调整控件 === # 创建右侧面板容器 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(10, 10, 10, 10) # 创建差异度调整组 diff_group = QGroupBox("差异度调整") diff_layout = QVBoxLayout(diff_group) # 差异度阈值控制 ui.lblDiffThreshold = QLabel("差异度阈值 (0-100%):") ui.sliderDiffThreshold = QSlider(Qt.Horizontal) ui.sliderDiffThreshold.setRange(0, 100) # 0-100% ui.sliderDiffThreshold.setValue(5) # 默认5% ui.lblDiffValue = QLabel("5%") # 当前差异度显示 ui.lblCurrentDiff = QLabel("当前差异度: -") ui.lblCurrentDiff.setStyleSheet("font-size: 14px; font-weight: bold;") # 差异度状态指示器 ui.lblDiffStatus = QLabel("状态: 未检测") ui.lblDiffStatus.setStyleSheet("font-size: 12px;") # 布局控件 diff_layout.addWidget(ui.lblDiffThreshold) diff_layout.addWidget(ui.sliderDiffThreshold) diff_layout.addWidget(ui.lblDiffValue) diff_layout.addWidget(ui.lblCurrentDiff) diff_layout.addWidget(ui.lblDiffStatus) # 添加差异度组到右侧布局 right_layout.addWidget(diff_group) # 添加拉伸项使控件靠上 right_layout.addStretch(1) # 创建停靠窗口 dock = QDockWidget("检测控制面板", mainWindow) dock.setWidget(right_panel) dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) mainWindow.addDockWidget(Qt.RightDockWidgetArea, dock) # === 差异度调整功能实现 === # 更新差异度阈值显示 def update_diff_threshold(value): ui.lblDiffValue.setText(f"{value}%") # 连接滑块信号 ui.sliderDiffThreshold.valueChanged.connect(update_diff_threshold) # 更新检测结果显示 def update_diff_display(diff_ratio, is_qualified): # 更新当前差异度显示 ui.lblCurrentDiff.setText(f"当前差异度: {diff_ratio*100:.2f}%") # 根据合格状态设置颜色 if is_qualified: ui.lblDiffStatus.setText("状态: 合格") ui.lblDiffStatus.setStyleSheet("color: green; font-size: 12px;") else: ui.lblDiffStatus.setText("状态: 不合格") ui.lblDiffStatus.setStyleSheet("color: red; font-size: 12px;") # 绑定按钮事件 ui.bnCheckPrint.clicked.connect(check_print) ui.bnSaveSample.clicked.connect(save_sample_image) ui.bnPreviewSample.clicked.connect(preview_sample) # 绑定其他按钮事件 ui.bnEnum.clicked.connect(enum_devices) ui.bnOpen.clicked.connect(open_device) ui.bnClose.clicked.connect(close_device) ui.bnStart.clicked.connect(start_grabbing) ui.bnStop.clicked.connect(stop_grabbing) ui.bnSoftwareTrigger.clicked.connect(trigger_once) ui.radioTriggerMode.clicked.connect(set_software_trigger_mode) ui.radioContinueMode.clicked.connect(set_continue_mode) ui.bnGetParam.clicked.connect(get_param) ui.bnSetParam.clicked.connect(set_param) # 修改保存图像按钮连接 ui.bnSaveImage.clicked.connect(save_image_dialog) # 显示主窗口 mainWindow.show() # 执行应用 app.exec_() # 关闭设备 close_device() # ch:反初始化SDK | en: finalize SDK MvCamera.MV_CC_Finalize() sys.exit() 我感觉这个代码对于印花图案缺陷检测能力还是不够精准能否更新算法让它缺陷检测更加敏锐

# -*- coding: utf-8 -*- import sys import os import cv2 import numpy as np import json import time import logging import platform from datetime import datetime from scipy import ndimage from scipy.spatial import distance # PyQt5 导入 from PyQt5.QtWidgets import ( QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QMessageBox, QLabel, QFileDialog, QToolBar, QComboBox, QStatusBar, QGroupBox, QSlider, QDockWidget, QProgressDialog, QCheckBox # 添加 QCheckBox ) from PyQt5.QtCore import QRect, Qt, QSettings, QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap # 可能需要这些用于图像显示 # 海康 SDK 导入 sys.path.append("D:\\海康\\MVS\\Development\\Samples\\Python\\BasicDemo") from MvCameraControl_class import * from MvErrorDefine_const import * from CameraParams_header import * # 自定义模块导入 from CamOperation_class import CameraOperation from PyUICBasicDemo import Ui_MainWindow # 如果使用 UI 文件生成 # 图像处理库导入 try: from skimage.feature import local_binary_pattern from skimage import exposure except ImportError: # 提供替代方案或错误处理 pass # 配置日志系统 logging.basicConfig( level=logging.DEBUG, # 设置为DEBUG级别获取更多信息 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("cloth_inspection_debug.log"), logging.StreamHandler() ] ) logging.info("布料印花检测系统启动 - 增强版") # 全局变量 current_sample_path = "" # 当前使用的样本路径 detection_history = [] # 检测历史记录 sample_features = {} # 存储样本特征数据 config_file = "detection_config.json" # 配置文件路径 # 加载配置文件 def load_config(): global config_file if os.path.exists(config_file): try: with open(config_file, 'r') as f: return json.load(f) except Exception as e: logging.error(f"加载配置文件失败: {str(e)}") return {} return {} # 保存配置文件 def save_config(config): global config_file try: with open(config_file, 'w') as f: json.dump(config, f, indent=4) logging.info("配置文件已保存") except Exception as e: logging.error(f"保存配置文件失败: {str(e)}") # 初始化配置 default_config = { "threshold": 0.05, "min_defect_area": 50, "max_defect_area": 5000, "texture_analysis": True, "color_sensitivity": 0.8, "edge_detection": True, "adaptive_threshold": True, "multi_scale_levels": 3, "feature_extraction": "histogram", # histogram, lbp, sift, orb "defect_classification": True } app_config = {**default_config, **load_config()} # 帧监控线程 class FrameMonitorThread(QThread): frame_status = pyqtSignal(str) def __init__(self, cam_operation): super().__init__() self.cam_operation = cam_operation self.running = True def run(self): while self.running: if self.cam_operation: status = self.cam_operation.get_frame_status() frame_text = "有帧" if status.get('current_frame', False) else "无帧" self.frame_status.emit(f"帧状态: {frame_text}") QThread.msleep(500) def stop(self): self.running = False # ======================== 增强版布料印花检测算法 ======================== def extract_features(image): """ 提取图像的多尺度特征 :param image: 输入图像 (灰度或彩色) :return: 特征字典 """ features = {} # 确保图像是灰度图 if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image # 1. 直方图特征 hist = cv2.calcHist([gray], [0], None, [256], [0, 256]) hist = cv2.normalize(hist, hist).flatten() features['histogram'] = hist.tolist() # 2. LBP纹理特征 (局部二值模式) radius = 3 n_points = 8 * radius lbp = local_binary_pattern(gray, n_points, radius, method='uniform') lbp_hist, _ = np.histogram(lbp.ravel(), bins=np.arange(0, n_points + 3), range=(0, n_points + 2)) lbp_hist = lbp_hist.astype("float") lbp_hist /= (lbp_hist.sum() + 1e-7) # 归一化 features['lbp'] = lbp_hist.tolist() # 3. 颜色特征 (如果是彩色图像) if len(image.shape) == 3: # 在HSV空间中计算颜色直方图 hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) h_hist = cv2.calcHist([hsv], [0], None, [180], [0, 180]) s_hist = cv2.calcHist([hsv], [1], None, [256], [0, 256]) v_hist = cv2.calcHist([hsv], [2], None, [256], [0, 256]) h_hist = cv2.normalize(h_hist, h_hist).flatten() s_hist = cv2.normalize(s_hist, s_hist).flatten() v_hist = cv2.normalize(v_hist, v_hist).flatten() features['h_hist'] = h_hist.tolist() features['s_hist'] = s_hist.tolist() features['v_hist'] = v_hist.tolist() # 4. 边缘特征 edges = cv2.Canny(gray, 100, 200) edge_density = np.sum(edges > 0) / (edges.shape[0] * edges.shape[1]) features['edge_density'] = edge_density # 5. 多尺度分析 scales = [1.0, 0.5, 0.25] scale_features = [] for scale in scales: scaled = cv2.resize(gray, None, fx=scale, fy=scale) # 计算小波变换 (近似) coeffs = cv2.dct(np.float32(scaled)/255.0) coeffs_flat = coeffs[:10, :10].flatten() scale_features.extend(coeffs_flat.tolist()) features['multi_scale'] = scale_features return features def compute_feature_distance(features1, features2): """ 计算两个特征集之间的距离 :param features1: 特征集1 :param features2: 特征集2 :return: 距离分数 (0-1, 0表示完全相同) """ # 直方图距离 (卡方距离) hist_dist = distance.chisquare(features1['histogram'], features2['histogram'])[0] / 1000 # LBP距离 (巴氏距离) lbp_dist = distance.bhattacharyya(features1['lbp'], features2['lbp']) # 颜色距离 (如果存在) color_dist = 0 if 'h_hist' in features1 and 'h_hist' in features2: h_dist = distance.chisquare(features1['h_hist'], features2['h_hist'])[0] / 1000 s_dist = distance.chisquare(features1['s_hist'], features2['s_hist'])[0] / 1000 v_dist = distance.chisquare(features1['v_hist'], features2['v_hist'])[0] / 1000 color_dist = (h_dist + s_dist + v_dist) / 3 # 边缘密度差异 edge_dist = abs(features1['edge_density'] - features2['edge_density']) # 多尺度特征差异 (欧氏距离) scale_dist = distance.euclidean(features1['multi_scale'], features2['multi_scale']) / 100 # 加权组合距离 weights = { 'hist': 0.3, 'lbp': 0.3, 'color': 0.2, 'edge': 0.1, 'scale': 0.1 } total_dist = ( weights['hist'] * hist_dist + weights['lbp'] * lbp_dist + weights['color'] * color_dist + weights['edge'] * edge_dist + weights['scale'] * scale_dist ) # 归一化到0-1范围 total_dist = min(1.0, max(0.0, total_dist)) return total_dist def adaptive_threshold_diff(sample_gray, test_gray): """ 自适应阈值差异检测 :param sample_gray: 样本灰度图像 :param test_gray: 测试灰度图像 :return: 二值差异图像 """ # 计算绝对差异 diff = cv2.absdiff(sample_gray, test_gray) # 自适应阈值 block_size = max(11, min(sample_gray.shape[0] // 20, 51)) adaptive_thresh = cv2.adaptiveThreshold( diff, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, block_size, 5 ) # 形态学操作增强缺陷区域 kernel = np.ones((3, 3), np.uint8) enhanced = cv2.morphologyEx(adaptive_thresh, cv2.MORPH_CLOSE, kernel) enhanced = cv2.morphologyEx(enhanced, cv2.MORPH_OPEN, kernel) return enhanced def detect_defects(sample_gray, test_gray, diff_map): """ 检测并分类缺陷 :param sample_gray: 样本灰度图像 :param test_gray: 测试灰度图像 :param diff_map: 差异图 :return: 缺陷列表, 标记图像 """ # 查找连通区域 num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(diff_map, connectivity=8) defects = [] marked_image = cv2.cvtColor(test_gray, cv2.COLOR_GRAY2BGR) # 最小和最大缺陷面积 min_area = app_config.get("min_defect_area", 50) max_area = app_config.get("max_defect_area", 5000) for i in range(1, num_labels): # 跳过背景 area = stats[i, cv2.CC_STAT_AREA] # 过滤太小或太大的区域 if area < min_area or area > max_area: continue # 获取缺陷区域 x, y, w, h = stats[i, cv2.CC_STAT_LEFT], stats[i, cv2.CC_STAT_TOP], stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT] defect_roi = diff_map[y:y+h, x:x+w] # 计算缺陷特征 sample_roi = sample_gray[y:y+h, x:x+w] test_roi = test_gray[y:y+h, x:x+w] # 缺陷类型分类 defect_type = classify_defect(sample_roi, test_roi, defect_roi) # 保存缺陷信息 defects.append({ "type": defect_type, "area": area, "location": (x, y, w, h), "centroid": (int(centroids[i][0]), int(centroids[i][1])) }) # 在标记图像上绘制 color = (0, 0, 255) # 默认红色 if defect_type == "color": color = (0, 165, 255) # 橙色 elif defect_type == "texture": color = (0, 255, 255) # 黄色 elif defect_type == "missing": color = (255, 0, 0) # 蓝色 elif defect_type == "stain": color = (0, 255, 0) # 绿色 cv2.rectangle(marked_image, (x, y), (x+w, y+h), color, 2) cv2.putText(marked_image, f"{defect_type}", (x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) return defects, marked_image def classify_defect(sample_roi, test_roi, defect_mask): """ 分类缺陷类型 :param sample_roi: 样本区域 :param test_roi: 测试区域 :param defect_mask: 缺陷掩码 :return: 缺陷类型 (color, texture, missing, stain) """ # 计算颜色差异 color_diff = np.mean(np.abs(sample_roi.astype(np.float32) - test_roi.astype(np.float32))) # 计算纹理差异 (使用LBP) radius = 2 n_points = 8 * radius lbp_sample = local_binary_pattern(sample_roi, n_points, radius, method='uniform') lbp_test = local_binary_pattern(test_roi, n_points, radius, method='uniform') hist_sample, _ = np.histogram(lbp_sample.ravel(), bins=np.arange(0, n_points + 3), range=(0, n_points + 2)) hist_test, _ = np.histogram(lbp_test.ravel(), bins=np.arange(0, n_points + 3), range=(0, n_points + 2)) hist_sample = hist_sample.astype("float") hist_test = hist_test.astype("float") hist_sample /= (hist_sample.sum() + 1e-7) hist_test /= (hist_test.sum() + 1e-7) texture_diff = distance.chisquare(hist_sample, hist_test)[0] # 计算边缘密度差异 edges_sample = cv2.Canny(sample_roi, 50, 150) edges_test = cv2.Canny(test_roi, 50, 150) edge_density_sample = np.sum(edges_sample > 0) / edges_sample.size edge_density_test = np.sum(edges_test > 0) / edges_test.size edge_diff = abs(edge_density_sample - edge_density_test) # 根据特征分类 if color_diff > 30 and texture_diff < 10: return "color" # 颜色缺陷 elif texture_diff > 15 and color_diff < 20: return "texture" # 纹理缺陷 elif edge_diff > 0.1 and np.mean(test_roi[defect_mask > 0]) < np.mean(sample_roi[defect_mask > 0]) - 20: return "missing" # 缺失图案 else: return "stain" # 污渍 def check_print_quality(sample_image_path, test_image, threshold=None): """ 增强版布料印花检测算法 :param sample_image_path: 合格样本图像路径 :param test_image: 内存中的测试图像 (numpy数组) :param threshold: 差异阈值 :return: 是否合格, 差异值, 标记图像, 缺陷列表 """ global sample_features, app_config # 使用配置中的阈值 if threshold is None: threshold = app_config.get("threshold", 0.05) # 读取样本图像 try: sample_img_data = np.fromfile(sample_image_path, dtype=np.uint8) sample_image = cv2.imdecode(sample_img_data, cv2.IMREAD_COLOR) if sample_image is None: logging.error(f"无法解码样本图像: {sample_image_path}") return None, None, None, None except Exception as e: logging.exception(f"样本图像读取异常: {str(e)}") return None, None, None, None # 确保测试图像是彩色 if len(test_image.shape) == 2: # 如果是灰度图像 test_image = cv2.cvtColor(test_image, cv2.COLOR_GRAY2BGR) # 确保两个图像大小一致 try: test_image = cv2.resize(test_image, (sample_image.shape[1], sample_image.shape[0])) except Exception as e: logging.error(f"图像调整大小失败: {str(e)}") return None, None, None, None # 特征提取和比较 if sample_image_path not in sample_features: logging.info(f"提取样本特征: {sample_image_path}") sample_features[sample_image_path] = extract_features(sample_image) test_features = extract_features(test_image) feature_diff = compute_feature_distance(sample_features[sample_image_path], test_features) # 转换为灰度图像进行像素级比较 sample_gray = cv2.cvtColor(sample_image, cv2.COLOR_BGR2GRAY) test_gray = cv2.cvtColor(test_image, cv2.COLOR_BGR2GRAY) # 自适应阈值差异检测 diff_map = adaptive_threshold_diff(sample_gray, test_gray) # 计算差异比例 diff_pixels = np.count_nonzero(diff_map) total_pixels = sample_gray.size pixel_diff_ratio = diff_pixels / total_pixels # 综合差异分数 combined_diff = 0.7 * feature_diff + 0.3 * pixel_diff_ratio # 检测和分类缺陷 defects, marked_image = detect_defects(sample_gray, test_gray, diff_map) # 判断是否合格 is_qualified = combined_diff <= threshold and len(defects) == 0 return is_qualified, combined_diff, marked_image, defects # ======================== UI更新函数 ======================== def update_diff_display(diff_ratio, is_qualified, defects): """ 更新差异度显示控件 """ # 更新当前差异度显示 ui.lblCurrentDiff.setText(f"当前差异度: {diff_ratio*100:.2f}%") # 显示缺陷数量 defect_count = len(defects) ui.lblDefectCount.setText(f"缺陷数量: {defect_count}") # 根据合格状态设置颜色 if is_qualified: ui.lblDiffStatus.setText("状态: 合格") ui.lblDiffStatus.setStyleSheet("color: green; font-size: 12px;") ui.lblDefectCount.setStyleSheet("color: green;") else: ui.lblDiffStatus.setText("状态: 不合格") ui.lblDiffStatus.setStyleSheet("color: red; font-size: 12px;") ui.lblDefectCount.setStyleSheet("color: red;") def update_diff_threshold(value): """ 当滑块值改变时更新阈值显示 """ global app_config app_config["threshold"] = value / 100.0 ui.lblDiffValue.setText(f"{value}%") save_config(app_config) def update_min_defect_area(value): """ 更新最小缺陷面积阈值 """ global app_config app_config["min_defect_area"] = value ui.lblMinAreaValue.setText(f"{value} px²") save_config(app_config) def update_max_defect_area(value): """ 更新最大缺陷面积阈值 """ global app_config app_config["max_defect_area"] = value ui.lblMaxAreaValue.setText(f"{value} px²") save_config(app_config) def toggle_texture_analysis(state): """ 切换纹理分析功能 """ global app_config app_config["texture_analysis"] = (state == Qt.Checked) save_config(app_config) def toggle_edge_detection(state): """ 切换边缘检测功能 """ global app_config app_config["edge_detection"] = (state == Qt.Checked) save_config(app_config) def toggle_adaptive_threshold(state): """ 切换自适应阈值功能 """ global app_config app_config["adaptive_threshold"] = (state == Qt.Checked) save_config(app_config) def update_color_sensitivity(value): """ 更新颜色敏感度 """ global app_config app_config["color_sensitivity"] = value / 100.0 ui.lblColorSensitivityValue.setText(f"{value}%") save_config(app_config) # 布料印花检测功能(增强版) def check_print(): global isGrabbing, obj_cam_operation, current_sample_path, detection_history, app_config logging.info("检测印花质量按钮按下") # 1. 检查相机状态 if not isGrabbing: logging.warning("相机未取流") QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 2. 检查相机操作对象 if not obj_cam_operation: logging.error("相机操作对象未初始化") QMessageBox.warning(mainWindow, "错误", "相机未正确初始化!", QMessageBox.Ok) return # 3. 检查样本路径 if not current_sample_path or not os.path.exists(current_sample_path): logging.warning(f"无效样本路径: {current_sample_path}") QMessageBox.warning(mainWindow, "错误", "请先设置有效的标准样本图像!", QMessageBox.Ok) return # 使用进度对话框防止UI阻塞 progress = QProgressDialog("正在检测...", "取消", 0, 100, mainWindow) progress.setWindowModality(Qt.WindowModal) progress.setValue(10) try: # 4. 获取当前帧 logging.info("尝试获取当前帧") test_image = obj_cam_operation.get_current_frame() progress.setValue(30) if test_image is None: logging.warning("获取当前帧失败") QMessageBox.warning(mainWindow, "错误", "无法获取当前帧图像!", QMessageBox.Ok) return # 5. 获取差异度阈值 diff_threshold = app_config.get("threshold", 0.05) logging.info(f"使用差异度阈值: {diff_threshold}") progress.setValue(50) # 6. 执行检测 is_qualified, diff_ratio, marked_image, defects = check_print_quality( current_sample_path, test_image, threshold=diff_threshold ) progress.setValue(70) # 检查返回结果是否有效 if is_qualified is None: logging.error("检测函数返回无效结果") QMessageBox.critical(mainWindow, "检测错误", "检测失败,请检查日志", QMessageBox.Ok) return logging.info(f"检测结果: 合格={is_qualified}, 差异={diff_ratio}, 缺陷数={len(defects)}") progress.setValue(90) # 7. 更新UI update_diff_display(diff_ratio, is_qualified, defects) # 显示详细结果 result_text = f"印花是否合格: {'合格' if is_qualified else '不合格'}\n" result_text += f"综合差异度: {diff_ratio*100:.2f}%\n" result_text += f"缺陷数量: {len(defects)}\n" result_text += f"阈值: {diff_threshold*100:.2f}%" if not is_qualified and defects: result_text += "\n\n缺陷详情:" for i, defect in enumerate(defects[:3]): # 最多显示3个主要缺陷 result_text += f"\n{i+1}. {defect['type']} (大小: {defect['area']}像素)" QMessageBox.information(mainWindow, "检测结果", result_text, QMessageBox.Ok) if marked_image is not None: cv2.imshow("缺陷标记结果", marked_image) cv2.waitKey(0) cv2.destroyAllWindows() else: logging.warning("标记图像为空") # 8. 记录检测结果 detection_result = { 'timestamp': datetime.now(), 'qualified': is_qualified, 'diff_ratio': diff_ratio, 'defects': defects, 'threshold': diff_threshold } detection_history.append(detection_result) update_history_display() progress.setValue(100) except Exception as e: logging.exception("印花检测失败") QMessageBox.critical(mainWindow, "检测错误", f"检测过程中发生错误: {str(e)}", QMessageBox.Ok) finally: progress.close() # 保存标准样本函数(增强版) def save_sample_image(): global isGrabbing, obj_cam_operation, current_sample_path, sample_features if not isGrabbing: QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 检查是否有有效图像 if not obj_cam_operation.is_frame_available(): QMessageBox.warning(mainWindow, "无有效图像", "未捕获到有效图像,请检查相机状态!", QMessageBox.Ok) return # 读取上次使用的路径 settings = QSettings("ClothInspection", "CameraApp") last_dir = settings.value("last_save_dir", os.path.join(os.getcwd(), "captures")) # 创建默认文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") default_filename = f"sample_{timestamp}" # 弹出文件保存对话框 file_path, selected_filter = QFileDialog.getSaveFileName( mainWindow, "保存标准样本图像", os.path.join(last_dir, default_filename), "BMP Files (*.bmp);;PNG Files (*.png);;JPEG Files (*.jpg);;所有文件 (*)", options=QFileDialog.DontUseNativeDialog ) if not file_path: logging.info("用户取消了图像保存操作") return # 用户取消保存 # 处理文件扩展名 file_extension = os.path.splitext(file_path)[1].lower() if not file_extension: # 根据选择的过滤器添加扩展名 if "BMP" in selected_filter: file_path += ".bmp" elif "PNG" in selected_filter: file_path += ".png" elif "JPEG" in selected_filter or "JPG" in selected_filter: file_path += ".jpg" else: # 默认使用BMP格式 file_path += ".bmp" file_extension = os.path.splitext(file_path)[1].lower() # 根据扩展名设置保存格式 format_mapping = { ".bmp": "bmp", ".png": "png", ".jpg": "jpg", ".jpeg": "jpg" } save_format = format_mapping.get(file_extension) if not save_format: QMessageBox.warning(mainWindow, "错误", "不支持的文件格式!", QMessageBox.Ok) return # 确保目录存在 directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): try: os.makedirs(directory, exist_ok=True) logging.info(f"创建目录: {directory}") except OSError as e: error_msg = f"无法创建目录 {directory}: {str(e)}" QMessageBox.critical(mainWindow, "目录创建错误", error_msg, QMessageBox.Ok) return # 保存当前帧作为标准样本 try: ret = obj_cam_operation.save_image(file_path, save_format) if ret != MV_OK: strError = f"保存样本图像失败: {hex(ret)}" QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) else: success_msg = f"标准样本已保存至:\n{file_path}" QMessageBox.information(mainWindow, "成功", success_msg, QMessageBox.Ok) # 更新当前样本路径 current_sample_path = file_path update_sample_display() # 提取并保存样本特征 sample_img = cv2.imread(file_path) if sample_img is not None: sample_features[file_path] = extract_features(sample_img) logging.info(f"样本特征已提取并保存: {file_path}") # 保存当前目录 settings.setValue("last_save_dir", os.path.dirname(file_path)) except Exception as e: error_msg = f"保存图像时发生错误: {str(e)}" QMessageBox.critical(mainWindow, "异常错误", error_msg, QMessageBox.Ok) logging.exception("保存样本图像时发生异常") # 预览当前样本 def preview_sample(): global current_sample_path if not current_sample_path or not os.path.exists(current_sample_path): QMessageBox.warning(mainWindow, "错误", "请先设置有效的标准样本图像!", QMessageBox.Ok) return try: # 使用安全方法读取图像 img_data = np.fromfile(current_sample_path, dtype=np.uint8) sample_img = cv2.imdecode(img_data, cv2.IMREAD_COLOR) if sample_img is None: raise Exception("无法加载图像") # 显示特征信息 if current_sample_path in sample_features: features = sample_features[current_sample_path] edge_density = features.get('edge_density', 0) feature_info = f"边缘密度: {edge_density:.4f}\n特征点: {len(features.get('multi_scale', []))}" cv2.putText(sample_img, feature_info, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("标准样本预览", sample_img) cv2.waitKey(0) cv2.destroyAllWindows() except Exception as e: QMessageBox.warning(mainWindow, "错误", f"预览样本失败: {str(e)}", QMessageBox.Ok) # 更新样本路径显示 def update_sample_display(): global current_sample_path if current_sample_path: ui.lblSamplePath.setText(f"当前样本: {os.path.basename(current_sample_path)}") ui.lblSamplePath.setToolTip(current_sample_path) ui.bnPreviewSample.setEnabled(True) else: ui.lblSamplePath.setText("当前样本: 未设置样本") ui.bnPreviewSample.setEnabled(False) # 更新历史记录显示 def update_history_display(): global detection_history ui.cbHistory.clear() for i, result in enumerate(detection_history[-10:]): # 显示最近10条记录 timestamp = result['timestamp'].strftime("%H:%M:%S") status = "合格" if result['qualified'] else "不合格" ratio = f"{result['diff_ratio']*100:.2f}%" defects = len(result.get('defects', [])) ui.cbHistory.addItem(f"[{timestamp}] {status} - 差异: {ratio} - 缺陷: {defects}") # ... [其余原有函数保持不变,如设备枚举、相机操作等] ... if __name__ == "__main__": # ... [原有初始化代码] ... # 初始化UI app = QApplication(sys.argv) mainWindow = QMainWindow() ui = Ui_MainWindow() ui.setupUi(mainWindow) # 扩大主窗口尺寸 mainWindow.resize(1400, 900) # 更宽的界面 # 创建工具栏 toolbar = mainWindow.addToolBar("检测工具") # 添加检测按钮 ui.bnCheckPrint = QPushButton("检测印花质量") toolbar.addWidget(ui.bnCheckPrint) # 添加保存样本按钮 ui.bnSaveSample = QPushButton("保存标准样本") toolbar.addWidget(ui.bnSaveSample) # 添加预览样本按钮 ui.bnPreviewSample = QPushButton("预览样本") toolbar.addWidget(ui.bnPreviewSample) # 添加历史记录下拉框 ui.cbHistory = QComboBox() ui.cbHistory.setMinimumWidth(350) toolbar.addWidget(QLabel("历史记录:")) toolbar.addWidget(ui.cbHistory) # 添加当前样本显示标签 ui.lblSamplePath = QLabel("当前样本: 未设置样本") status_bar = mainWindow.statusBar() status_bar.addPermanentWidget(ui.lblSamplePath) # === 增强版差异度调整面板 === # 创建右侧面板容器 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(10, 10, 10, 10) # 创建差异度调整组 diff_group = QGroupBox("检测参数设置") diff_layout = QVBoxLayout(diff_group) # 差异度阈值控制 ui.lblDiffThreshold = QLabel("差异度阈值 (0-100%):") ui.sliderDiffThreshold = QSlider(Qt.Horizontal) ui.sliderDiffThreshold.setRange(0, 100) # 0-100% ui.sliderDiffThreshold.setValue(int(app_config.get("threshold", 0.05) * 100)) ui.lblDiffValue = QLabel(f"{ui.sliderDiffThreshold.value()}%") # 当前差异度显示 ui.lblCurrentDiff = QLabel("当前差异度: -") ui.lblCurrentDiff.setStyleSheet("font-size: 14px; font-weight: bold;") # 缺陷数量显示 ui.lblDefectCount = QLabel("缺陷数量: -") ui.lblDefectCount.setStyleSheet("font-size: 14px;") # 差异度状态指示器 ui.lblDiffStatus = QLabel("状态: 未检测") ui.lblDiffStatus.setStyleSheet("font-size: 12px;") # 添加分隔线 diff_layout.addWidget(QLabel(" ")) diff_layout.addWidget(QLabel("缺陷检测参数")) # 最小缺陷面积 ui.lblMinArea = QLabel("最小缺陷面积:") ui.sliderMinArea = QSlider(Qt.Horizontal) ui.sliderMinArea.setRange(10, 500) ui.sliderMinArea.setValue(app_config.get("min_defect_area", 50)) ui.lblMinAreaValue = QLabel(f"{ui.sliderMinArea.value()} px²") # 最大缺陷面积 ui.lblMaxArea = QLabel("最大缺陷面积:") ui.sliderMaxArea = QSlider(Qt.Horizontal) ui.sliderMaxArea.setRange(1000, 10000) ui.sliderMaxArea.setValue(app_config.get("max_defect_area", 5000)) ui.lblMaxAreaValue = QLabel(f"{ui.sliderMaxArea.value()} px²") # 颜色敏感度 ui.lblColorSensitivity = QLabel("颜色敏感度:") ui.sliderColorSensitivity = QSlider(Qt.Horizontal) ui.sliderColorSensitivity.setRange(0, 100) ui.sliderColorSensitivity.setValue(int(app_config.get("color_sensitivity", 0.8) * 100)) ui.lblColorSensitivityValue = QLabel(f"{ui.sliderColorSensitivity.value()}%") # 添加分隔线 diff_layout.addWidget(QLabel(" ")) diff_layout.addWidget(QLabel("高级分析选项")) # 纹理分析开关 ui.chkTextureAnalysis = QCheckBox("启用纹理分析") ui.chkTextureAnalysis.setChecked(app_config.get("texture_analysis", True)) # 边缘检测开关 ui.chkEdgeDetection = QCheckBox("启用边缘检测") ui.chkEdgeDetection.setChecked(app_config.get("edge_detection", True)) # 自适应阈值开关 ui.chkAdaptiveThreshold = QCheckBox("使用自适应阈值") ui.chkAdaptiveThreshold.setChecked(app_config.get("adaptive_threshold", True)) # 布局控件 diff_layout.addWidget(ui.lblDiffThreshold) diff_layout.addWidget(ui.sliderDiffThreshold) diff_layout.addWidget(ui.lblDiffValue) diff_layout.addWidget(ui.lblCurrentDiff) diff_layout.addWidget(ui.lblDefectCount) diff_layout.addWidget(ui.lblDiffStatus) diff_layout.addWidget(ui.lblMinArea) diff_layout.addWidget(ui.sliderMinArea) diff_layout.addWidget(ui.lblMinAreaValue) diff_layout.addWidget(ui.lblMaxArea) diff_layout.addWidget(ui.sliderMaxArea) diff_layout.addWidget(ui.lblMaxAreaValue) diff_layout.addWidget(ui.lblColorSensitivity) diff_layout.addWidget(ui.sliderColorSensitivity) diff_layout.addWidget(ui.lblColorSensitivityValue) diff_layout.addWidget(ui.chkTextureAnalysis) diff_layout.addWidget(ui.chkEdgeDetection) diff_layout.addWidget(ui.chkAdaptiveThreshold) # 添加拉伸项使控件靠上 diff_layout.addStretch(1) # 创建停靠窗口 dock = QDockWidget("检测参数面板", mainWindow) dock.setWidget(right_panel) dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) mainWindow.addDockWidget(Qt.RightDockWidgetArea, dock) # === 连接信号 === # 差异度阈值滑块 ui.sliderDiffThreshold.valueChanged.connect(update_diff_threshold) # 最小缺陷面积滑块 ui.sliderMinArea.valueChanged.connect(update_min_defect_area) # 最大缺陷面积滑块 ui.sliderMaxArea.valueChanged.connect(update_max_defect_area) # 颜色敏感度滑块 ui.sliderColorSensitivity.valueChanged.connect(update_color_sensitivity) # 分析选项复选框 ui.chkTextureAnalysis.stateChanged.connect(toggle_texture_analysis) ui.chkEdgeDetection.stateChanged.connect(toggle_edge_detection) ui.chkAdaptiveThreshold.stateChanged.connect(toggle_adaptive_threshold) # 绑定按钮事件 ui.bnCheckPrint.clicked.connect(check_print) ui.bnSaveSample.clicked.connect(save_sample_image) ui.bnPreviewSample.clicked.connect(preview_sample) # ... [其余原有绑定代码] ... # 显示主窗口 mainWindow.show() # 执行应用 app.exec_() # 关闭设备 close_device() # ch:反初始化SDK | en: finalize SDK MvCamera.MV_CC_Finalize() sys.exit() 这个程序运行后出现了查找不到打不开相机同时原有的差异度检查还有新增的功能都看不到了

#include "ttt.h" #include "ui_ttt.h" #include <QInputDialog> #include <QMessageBox> // 在文件顶部定义皮肤信息结构体 struct SkinInfo { QString whitePath; QString blackPath; }; const SkinInfo skins[] = { {":/pic/white.png", ":/pic/black.png"}, // 皮肤0 {":/pic/nailong.png", ":/pic/baobaolong.png"}, // 皮肤1 {":/pic/chiikawa.png", ":/pic/usagi.png"} // 皮肤2 }; TTT::TTT(QWidget *parent) :QMainWindow(parent) { currentSkin = 0; // 默认使用第一套皮肤 gameSkin = currentSkin; initUI(); initMenubar(); boardPixmap.load(":/pic/board.png"); // 缩小棋盘到600x600 boardPixmap = boardPixmap.scaled(600, 600, Qt::KeepAspectRatio, Qt::SmoothTransformation); boardSize = boardPixmap.size(); clickSound = new QSoundEffect(this); clickSound->setSource(QUrl("qrc:/sound/place0.83.wav")); clickSound->setVolume(1.0); winSound = new QSoundEffect(this); winSound->setSource(QUrl("qrc:/sound/win.wav")); } void TTT::initMenubar() { QMenuBar* qb = new QMenuBar(this); // 设置菜单栏样式 qb->setStyleSheet( "QMenuBar {" " background-color: #f0f0f0;" " border-bottom: 1px solid #ccc;" "}" "QMenuBar::item {" " padding: 5px 15px;" " background-color: #f0f0f0;" " border: 1px solid #ccc;" " border-radius: 4px;" " margin: 3px 5px;" "}" "QMenuBar::item:selected {" " background-color: #e0e0e0;" "}" "QMenuBar::item:pressed {" " background-color: #d0d0d0;" "}" ); QMenu* item = new QMenu("选项", this); // 设置菜单样式 item->setStyleSheet( "QMenu {" " background-color: white;" " border: 1px solid #ccc;" " border-radius: 4px;" " padding: 5px;" "}" "QMenu::item {" " padding: 5px 20px;" " border-radius: 3px;" "}" "QMenu::item:selected {" " background-color: #e6f0ff;" "}" ); QAction* restart = new QAction("重新开始", this); QAction* revoke = new QAction("悔棋", this); QAction* changeSkinAction = new QAction("更改皮肤", this); // 设置动作样式 restart->setIcon(QIcon(":/pic/restart.png")); // 添加图标 revoke->setIcon(QIcon(":/pic/undo.png")); // 添加图标 connect(restart, &QAction::triggered, [=](){ cache = {{0,0,0},{0,0,0},{0,0,0}}; last.clear(); iswhite = true; gameSkin = currentSkin; statusBar()->showMessage("轮到白棋"); update(); }); connect(revoke, &QAction::triggered, [=](){ if(last.size()>0){ cache[(*(last.end()-1))[0]][(*(last.end()-1))[1]]=0; iswhite = !iswhite; last.pop_back(); statusBar()->showMessage(iswhite ? "接下来轮到:白棋" : "接下来轮到:黑棋"); update(); } }); connect(changeSkinAction, &QAction::triggered, this, &TTT::changeSkin); item->addAction(restart); item->addAction(revoke); qb->addMenu(item); qb->setFixedHeight(50); this->setMenuBar(qb); item->addAction(changeSkinAction); } void TTT::changeSkin() { QStringList skinNames; for(int i=0; i<3; ++i) { skinNames << QString("皮肤%1").arg(i+1); } bool ok; QString selected = QInputDialog::getItem(this, "选择皮肤", "请选择游戏皮肤:", skinNames, currentSkin, false, &ok); if (ok) { int selectedSkin = skinNames.indexOf(selected); if (selectedSkin != -1) { currentSkin = selectedSkin; // 弹出提示框 QMessageBox::information(this, "修改成功!", "皮肤已更改,将重新启动游戏!"); // 自动重新开始游戏 cache = {{0,0,0},{0,0,0},{0,0,0}}; last.clear(); iswhite = true; gameSkin = currentSkin; statusBar()->showMessage("轮到白棋"); update(); } } } void TTT::initUI() { // 缩小窗口尺寸 setFixedSize(700, 700); setWindowTitle("井字棋"); setMouseTracking(true); cache = {{0,0,0},{0,0,0},{0,0,0}}; mouse = {0,0}; statusBar()->setStyleSheet( "QStatusBar {" " background-color: white;" // 白色背景 " border-top: 1px solid #ccc;" // 顶部边框 " padding: 5px;" // 内边距 "}" "QStatusBar QLabel {" " color: black;" // 黑色文字 " font-weight: 900;" // 更粗的字体(900是最高级别) " font-size: 18px;" // 更大的字体尺寸 " fonwint-family: 'Microsoft YaHei', sans-serif;" // 使用支持粗体的字体 "}" ); statusBar()->showMessage("接下来轮到:白棋"); } void TTT::paintEvent(QPaintEvent *e) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); // 绘制居中棋盘 int boardX = (width() - boardSize.width()) / 2; int boardY = (height() - boardSize.height()) / 2; painter.drawPixmap(boardX, boardY, boardPixmap); // 计算格子尺寸 int cellWidth = boardSize.width() / 3; int cellHeight = boardSize.height() / 3; // 绘制棋子 for(int i=0; i<3; ++i) { for(int j=0; j<3; ++j) { if(cache[i][j] != 0) { QPixmap piece; if(cache[i][j] == 1) { piece.load(skins[gameSkin].whitePath); } else { piece.load(skins[gameSkin].blackPath); } piece = piece.scaled(120, 120, Qt::KeepAspectRatio, Qt::SmoothTransformation); // 计算棋子中心位置 int pieceX = boardX + i * cellWidth + (cellWidth - piece.width()) / 2; int pieceY = boardY + j * cellHeight + (cellHeight - piece.height()) / 2; painter.drawPixmap(pieceX, pieceY, piece); } } } // 绘制鼠标跟随提示 QPixmap cursorPixmap; cursorPixmap.load(iswhite ? skins[gameSkin].whitePath : skins[gameSkin].blackPath); cursorPixmap = cursorPixmap.scaled(80, 80, Qt::KeepAspectRatio); painter.drawPixmap(mouse[0] - 40, mouse[1] - 40, cursorPixmap); } void TTT::check() { bool iswin =false; if(cache[0][0] == cache[0][1]&& cache[0][0]==cache[0][2]&&cache[0][0]!=0)iswin=true; if(cache[1][0] == cache[1][1]&& cache[1][0]==cache[1][2]&&cache[1][0]!=0)iswin=true; if(cache[2][0] == cache[2][1]&& cache[2][0]==cache[2][2]&&cache[2][0]!=0)iswin=true; if(cache[0][0] == cache[1][0]&& cache[0][0]==cache[2][0]&&cache[0][0]!=0)iswin=true; if(cache[0][1] == cache[1][1]&& cache[0][1]==cache[2][1]&&cache[0][1]!=0)iswin=true; if(cache[0][2] == cache[1][2]&& cache[0][2]==cache[2][2]&&cache[0][2]!=0)iswin=true; if(cache[0][0] == cache[1][1]&& cache[0][0]==cache[2][2]&&cache[0][0]!=0)iswin=true; if(cache[0][2] == cache[1][1]&& cache[0][2]==cache[2][0]&&cache[0][2]!=0)iswin=true; if(iswin){ if (winSound) { winSound->play(); } QString who; if(cache[(*(last.end()-1))[0]][(*(last.end()-1))[1]]==1){ who="白棋"; }else { who="黑棋"; } //弹出提示框是否继续游戏 int check = QMessageBox::question(this, "Game Over!",who+"赢了,是否重新开始游戏"); if(check==QMessageBox::Yes){ cache={{0,0,0},{0,0,0},{0,0,0}}; last.clear(); iswhite = true; statusBar()->showMessage("轮到白棋"); update(); return; } //不继续就退出游戏 exit(0); } for(int i=0;i<3;i++){ for(int j=0;j<3;j++){ if(cache[i][j]==0)return ; } } //弹出提示框,和棋,是否继续游戏 int check=QMessageBox::question(this, "Game Over!","打成平手!是否继续游戏?"); if(check==QMessageBox::Yes){ cache={{0,0,0},{0,0,0},{0,0,0}}; last.clear(); iswhite = true; statusBar()->showMessage("轮到白棋"); update(); return; } //不继续就退出游戏 exit(0); } void TTT::mouseMoveEvent(QMouseEvent *e){ mouse[0]=e->x(); mouse[1]=e->y(); update(); } void TTT::mousePressEvent(QMouseEvent *e) { int x = e->x(); int y = e->y(); // 计算棋盘边界 int boardX = (width() - boardSize.width()) / 2; int boardY = (height() - boardSize.height()) / 2; int cellWidth = boardSize.width() / 3; int cellHeight = boardSize.height() / 3; // 检测点击是否在棋盘范围内 if (x >= boardX && x < boardX + 3*cellWidth && y >= boardY && y < boardY + 3*cellHeight) { // 转换为棋盘坐标 int i = (x - boardX) / cellWidth; int j = (y - boardY) / cellHeight; if (i >= 0 && i < 3 && j >= 0 && j < 3 && cache[i][j] == 0) { cache[i][j] = iswhite ? 1 : 2; iswhite = !iswhite; last.push_back({i, j}); if (clickSound) { clickSound->play(); } check(); statusBar()->showMessage(iswhite ? "接下来轮到:白棋" : "接下来轮到:黑棋"); update(); } } } TTT::~TTT(){ delete clickSound; delete winSound; } 代码如上,新加一个功能,实现在游戏胜利时放出烟花。请给我解释操作步骤,我完全是新手,包括需不需要去找烟花的素材,最后帮我写出完整的功能代码

import sys import json import cv2 import numpy as np import requests import os from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject, QThread from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import (QWidget, QLabel, QPushButton, QTextEdit, QHBoxLayout, QVBoxLayout, QSplitter, QApplication, QMessageBox) from caipanconnect import * SERVER_IP = "192.168.1.88" client = RefereeBoxClient(SERVER_IP, 6666) if not client.connect(): exit(1) # 信号类 - 用于线程间通信 class SignalEmitter(QObject): new_data = pyqtSignal(dict) # 工作线程 - 运行Flask应用 class FlaskThread(QThread): def __init__(self, flask_run_func): super().__init__() self.flask_run_func = flask_run_func self.emitter = SignalEmitter() def run(self): self.flask_run_func() # 调用Flask运行函数 def stop(self): """安全停止Flask""" if self.isRunning(): self.quit() self.wait(1000) # 等待1秒 if self.isRunning(): self.terminate() self.wait() class VideoThread(QThread): change_pixmap_signal = pyqtSignal(QImage) def __init__(self, url): super().__init__() self.url = url self.running = True self.session = requests.Session() def run(self): while(1): try: response = self.session.get(self.url, stream=True, timeout=10) if response.status_code != 200: return content_type = response.headers['Content-Type'] boundary = content_type.split('=')[1].encode() data_buffer = b'' for chunk in response.iter_content(chunk_size=8192): if not self.running: break data_buffer += chunk a = data_buffer.find(b'\r\n--' + boundary + b'\r\n') b = data_buffer.find(b'\r\n--' + boundary + b'--') if a != -1: if b == -1: frame_data = data_buffer[:a] data_buffer = data_buffer[a + len(b'\r\n--' + boundary + b'\r\n'):] else: frame_data = data_buffer[:b] data_buffer = b'' start = frame_data.find(b'\r\n\r\n') + 4 image_data = frame_data[start:] qimg = QImage.fromData(image_data) if not qimg.isNull(): self.change_pixmap_signal.emit(qimg) except Exception as e: print(f"Video stream error: {e}") def stop(self): self.running = False self.session.close() # 关闭请求会话 #self.wait(2000) # 等待2秒 if self.isRunning(): self.terminate() # 强制终止 self.wait() class MainWindow(QWidget): def __init__(self, data_queue): super().__init__() self.data_queue = data_queue self.setWindowTitle("Robocup") # 增大窗口的最小尺寸 self.setMinimumSize(1200, 800) self.textbutton = False self.should_clear_queue = False self.should_clear_queueimg = False self.video_thread = None self.desktop_path = os.path.join(os.path.expanduser('~'), 'Desktop', 'results_r') self.detection_result = [] self.detnum=0 self.detresultnum=[] self.signal=False # 创建Flask线程 from flasksignal import run_flask_app self.flask_thread = FlaskThread(run_flask_app) self.flask_thread.emitter.new_data.connect(self.update_display) self.flask_thread.start() self.should_clear_queue = True # 初始化UI self.init_ui() # 启动定时器检查数据队列 self.timer = QTimer(self) self.timer.timeout.connect(self.check_data_queue) self.timer.start(0) self._auto_started = False # 连接窗口显示事件 self.showEvent = self.window_shown def window_shown(self,event): if not self._auto_started: self._auto_started = True self.auto_start() return super().showEvent(event) def auto_start(self): self.start_flask_thread() def init_ui(self): # 创建主布局 main_layout = QHBoxLayout(self) # 创建左侧Splitter left_splitter = QSplitter(Qt.Vertical) self.image_label = QLabel("图像显示区域") self.image_label.setAlignment(Qt.AlignCenter) # 更改图像显示区域的背景颜色 self.image_label.setStyleSheet("background-color: #e0e0e0; border: 2px solid #888;") # 按钮区域 button_splitter = QSplitter(Qt.Horizontal) self.show_image_btn = QPushButton("展示图像") self.end_image_btn = QPushButton("结束图像") # 优化按钮的样式,增大按钮大小和文字大小 self.show_image_btn.setStyleSheet("background-color: #4CAF50; color: white; font-size: 18px; padding: 10px;") self.end_image_btn.setStyleSheet("background-color: #F44336; color: white; font-size: 18px; padding: 10px;") self.show_image_btn.setMaximumHeight(60) self.end_image_btn.setMaximumHeight(60) button_splitter.addWidget(self.show_image_btn) button_splitter.addWidget(self.end_image_btn) left_splitter.addWidget(self.image_label) left_splitter.addWidget(button_splitter) # 扩大显示界面的位置比例 left_splitter.setSizes([600, 60]) # 创建右侧区域 right_widget = QWidget() right_layout = QVBoxLayout(right_widget) self.text_edit = QTextEdit() self.text_edit.setReadOnly(True) # 更改文本编辑区域的样式,背景设为黑色,文字设为白色,增大文字大小 self.text_edit.setStyleSheet("background-color: #000000; color: #ffffff; border: 2px solid #888; font-size: 16px;") right_layout.addWidget(self.text_edit) button_layout = QHBoxLayout() self.end_btn = QPushButton("开始") self.start_btn = QPushButton("结束") self.force_quit_btn = QPushButton("退出") # 优化按钮的样式,增大按钮大小和文字大小 self.end_btn.setStyleSheet("background-color: #2196F3; color: white; font-size: 18px; padding: 10px;") self.start_btn.setStyleSheet("background-color: #FF9800; color: white; font-size: 18px; padding: 10px;") self.force_quit_btn.setStyleSheet("background-color: #ff4444; color: white; font-size: 18px; padding: 10px;") button_layout.addWidget(self.end_btn) button_layout.addWidget(self.start_btn) button_layout.addWidget(self.force_quit_btn) right_layout.addLayout(button_layout) # 将左右区域添加到主布局 main_splitter = QSplitter(Qt.Horizontal) main_splitter.addWidget(left_splitter) main_splitter.addWidget(right_widget) # 扩大显示界面的位置比例 main_splitter.setSizes([800, 400]) main_layout.addWidget(main_splitter) # 连接按钮点击事件 self.end_btn.clicked.connect(self.start_flask_thread) self.start_btn.clicked.connect(self.set_clear_queue_flag) self.show_image_btn.clicked.connect(self.start_flask_threadimg) self.end_image_btn.clicked.connect(self.set_clear_queue_flagimg) self.force_quit_btn.clicked.connect(self.force_quit) def start_flask_threadimg(self): self.should_clear_queueimg = True self.signal=True if self.video_thread is None : self.video_thread = VideoThread("http://127.0.0.1:5002/") self.video_thread.change_pixmap_signal.connect(self.update_image) self.video_thread.start() if not self.video_thread.isRunning(): self.video_thread.change_pixmap_signal.connect(self.update_image) self.video_thread.start() def set_clear_queue_flagimg(self): self.should_clear_queueimg = False if self.video_thread: self.image_label.clear() def start_flask_thread(self): team_id = "eeem001" client.send_team_id(team_id) time.sleep(0.1) # 给服务器处理时间 self.should_clear_queue = False self.signal=True self.start_flask_threadimg() if not self.flask_thread.isRunning(): self.flask_thread.start() def set_clear_queue_flag(self): self.should_clear_queue = True def check_data_queue(self): if self.should_clear_queue: while not self.data_queue.empty(): self.data_queue.get() else: while not self.data_queue.empty(): data = self.data_queue.get() self.flask_thread.emitter.new_data.emit(data) def update_display(self, data): self.detection_result.append(data) if data["state"]=="last" : self.detnum=self.detnum+1 print( self.detnum) if self.detnum==100: #print(self.detection_result) for it in self.detection_result: if self.detresultnum==None: self.detresultnum.append({"ID":it["ID"],"num":it["num"]}) else: flag=0 for itnum in self.detresultnum: if itnum["ID"]==it["ID"]: itnum["num"]=itnum["num"]+it["num"] flag=1 if flag==0: self.detresultnum.append({"ID":it["ID"],"num":it["num"]}) #content=self.text_edit.toPlainText() #print(content) #if not content.strip(): self.text_edit.clear() for it in self.detresultnum: import math #print(it["num"]) it["num"]=(it["num"]/(self.detnum+1)) integer_part=int(it["num"]) dec_part=it["num"]-integer_part if dec_part<0.15: it["num"]=int(it["num"]) else: it["num"]=int(it["num"]+1) if it["num"]==0: continue #self.text_edit.append("目标ID:" + it["ID"] + " 数量:" + str(it["num"])) self.text_edit.append(f"目标ID:{it['ID']} 数量:{it['num']}") self.text_edit.moveCursor(self.text_edit.textCursor().End) self.save_txt() #signal = 1 #self.force_quit() #try: #self.client.connect() #file_path = os.path.join(self.desktop_path, "shdjxy-team-Rx.txt") #with open(file_path,'r',encoding='utf-8') as f: # print(f.read()) #self.client.send_file(file_path, data_type=1) #finally: # import time # time.sleep(0.3) # self.client.close() #写入文件,在桌面文件夹里 ---在update-data中 #写入属性self.datalast+传来的string---在update-data中 #如果为state==last 就是最后一次---在update-data中 def update_image(self, qimg): if self.should_clear_queueimg: pixmap = QPixmap.fromImage(qimg) self.image_label.setPixmap( pixmap.scaled(self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) def force_quit(self): reply = QMessageBox.question( self, '确认退出', "确定要退出所有程序吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: # 停止视频线程 if hasattr(self, 'video_thread') and self.video_thread: self.video_thread.stop() #self.video_thread.wait() # 停止Flask线程 if hasattr(self, 'flask_thread') and self.flask_thread.isRunning(): self.flask_thread.stop() # 强制退出应用 QApplication.quit() def save_txt(self): os.makedirs(self.desktop_path, exist_ok=True) file_name = f"shdjxy-team-Rx.txt" file_path = os.path.join(self.desktop_path,file_name) with open(file_path,'w',encoding='utf-8') as f: f.write("START\n") for result in self.detresultnum: if result["num"]==0: continue line = f"Goal_ID={result['ID']};Num={result['num']}" f.write(line+"\n") f.write("END") self.detection_result=[] self.detnum=0 self.detresultnum=[] self.set_clear_queue_flag() file_path = r"/home/HwHiAiUser/Desktop/results_r/shdjxy-team-Rx.txt" client.send_file(file_path, data_type=1) time.sleep(0.1) client.close() if __name__ == "__main__": app = QApplication(sys.argv) from queue import Queue data_queue = Queue() window = MainWindow(data_queue) window.show() sys.exit(app.exec_()) 写出软件界面及功能说明。

大家在看

recommend-type

乳酸菌发酵植物蛋白质饮料的研究

俗话说国以民为本,民以食为天,食品的生产、安全、质量均需要相应的食品资料知识累积与制定,相信这一份...该文档为乳酸菌发酵植物蛋白质饮料的研究,是一份很不错的参考资料,具有较高参考价值,感兴趣的可以下载看看
recommend-type

2D-Mesh片上网络中通信密集点优化方法

针对2D-Mesh结构片上网络中通信密集点引发的网络拥塞问题,提出了一种分散通信密集点负载的方法,对网络互连结构进行局部调整,增加与大通信量模块相连的路由器数目,并设计了一种基于区域的XY-YX路由算法。仿真结果表明,该方法可以有效地降低通信延时,增大吞吐量。
recommend-type

易语言Base64_hmac_sha1加密算法源码.zip

易语言HMAC_SHA256算法源码
recommend-type

文献管理软件Endnote引文格式文件

文件类型:styles.ens 下载描述:20230512官网 数量:7455个期刊 包含Water Research.ens、J Hydrology.ens、Catena.ens、Science Total Environ.ens、Waste Management.ens、Water Air Soil Poll Focus.ens、Water Air Soil Poll.ens、Water Environ J.ens、Water Environment Res.ens、Water International.ens、Water Policy.ens、Water Resources Res.ens、Water SA.ens、Water Science Tech.ens等水文期刊
recommend-type

高强螺栓连接评估软件BoltWorks

新版本地址https://download.csdn.net/download/well0887/18612481 BoltWorks是一款螺栓连接强度评估软件,以VDI 2230为主要评估方法,同时兼顾国内外行业标准,对高强度螺栓连接进行系统评估。

最新推荐

recommend-type

微信生态下数据分析算法的公共利益属性研究.docx

微信生态下数据分析算法的公共利益属性研究.docx
recommend-type

基于Arduino的智能4x4键盘门锁系统设计与实现

在这个项目中,我们将构建一个基于Arduino UNO的无钥匙门锁系统,该系统将使用一个4x4键盘来输入密钥,并控制一个伺服电机以开启或关闭门锁。以下是对该项目中所使用到的关键技术点的详细解释: ### Arduino UNO和Genuino UNO Arduino UNO和Genuino UNO是开源电子原型平台,基于易于使用的硬件和软件。它们使用ATmega328P微控制器,并拥有众多扩展板和模块兼容,这使得它们在创建各种项目,包括无钥匙门锁系统时,成为非常流行的选项。 ### 4x4键盘输入 4x4键盘由4行4列共16个按键组成,常用的输入方式包括矩阵键盘扫描。在无钥匙门锁系统中,4x4键盘用于输入密码。每个按键按下时,都会产生一个唯一的信号,系统会根据这些信号来确定输入的密码。使用矩阵键盘扫描技术,Arduino可以通过少数几个引脚来检测每个按键的动作,这大大简化了硬件连接。 ### 伺服电机 伺服电机(Tower Pro MG996R)是该项目中的执行器,用于控制门锁的开关。伺服电机可以精确地控制角度,非常适合用来驱动门锁机械部分进行旋转操作。通过编程,Arduino可以向伺服电机发送脉冲信号,从而控制其转动到指定的位置,比如90度用于解锁,0度用于上锁。 ### 跳线和面包板 为了简化电路连接,跳线(通用)和面包板(通用)被用作临时的原型搭建工具。跳线允许模块间进行快速且可重配置的连接,而面包板则提供了一个方便的平台来组建电路,不需要焊接。 ### LED指示灯和蜂鸣器 5毫米LED灯(红色和黄色)以及蜂鸣器都是用于提供用户反馈的组件。红色LED可以指示门锁已锁定,而黄色LED可以指示门锁已被解锁。蜂鸣器用于当输入错误的密码时发出警报声,提示用户输入不正确。 ### Adafruit标准LCD Adafruit标准LCD - 16x2白色蓝色用于显示系统的状态信息,比如“输入密码”、“门已开”或“门已锁”等提示。16x2的LCD表示它有16个字符宽度和2行字符高度,非常适合显示简短的文本信息。 ### Blynk软件应用程序 Blynk是一个为物联网项目设计的手机应用,可以通过Wi-Fi或蓝牙连接到Arduino等微控制器。在这个项目中,Blynk可以用来远程控制门锁,允许用户通过手机应用程序来输入密码解锁门锁。 ### 安全性和加密 这个项目特别提到了安全性的问题,因此在设计上需要考虑密码的加密和存储。为了避免密码被轻易破解,应该使用一种加密算法来保护存储在系统中的密码。同时,还应考虑如何安全地传输密码,尤其是如果使用Blynk这样的远程控制方法。 ### 电路方案和编程 构建这样一个系统需要对Arduino进行编程,以便它可以读取4x4键盘输入的密码,并通过编程逻辑控制伺服电机。编程时,需要编写代码以实现以下功能: 1. 初始化所有硬件组件,包括键盘、LED灯、蜂鸣器和LCD显示屏。 2. 设置键盘扫描程序,以检测按键输入。 3. 检查输入的密码是否正确,通过与存储在代码中的正确密码进行比较。 4. 控制伺服电机解锁或上锁门锁。 5. 使用LED灯和蜂鸣器提供适当的用户反馈。 6. 如果使用Blynk,则需要编写与Blynk服务器通信的代码,以及处理远程输入的密码。 ### 项目文件说明 - `source_code.c`:包含Arduino项目的源代码,用于编程和控制整个系统。 - `FpB_saOmaPgP-e187qEVsYgrz8Qy.png`:可能是项目的原理图或演示图,用于展示电路连接和布局。 - `项目展示.rar`:一个压缩文件,可能包含项目相关的视频、图像或其他演示文件,用于展示项目的实际运行效果。 - `原文地址.txt`:包含指向项目详细说明、教程或发布源的链接,方便用户查看更多项目细节或下载资源。 这个项目结合了多种技术,从物理输入设备的控制到系统软件逻辑编程,再到用户界面的设计,是物联网和嵌入式系统学习的一个很好的实例。通过实施这个项目,可以加深对Arduino平台、外围硬件组件以及简单安全系统的理解。
recommend-type

深入探索TypeScript类型验证与状态管理

### 深入探索 TypeScript 类型验证与状态管理 #### 1. Redux 与 React 组件中的状态管理 在 React 应用中,Redux 是管理状态的强大工具。`useSelector` 是 Redux 提供的一个钩子,它接收两个 TypeScript 泛型参数:整个状态的类型和返回值的类型。通过使用 `useSelector`,我们可以将组件所需的数据限制在必要范围内。例如: ```typescript const state = useSelector<VenueState, VenueState>((state) => state) ``` 这里,直接返回了整个状态
recommend-type

trace切片怎么对齐

由于提供的引用内容未涉及trace切片对齐的方法相关信息,以下是一般情况下trace切片对齐的常见方法: ### 时间戳对齐 确定一个共同的时间基准,将不同trace切片的时间戳映射到该基准上。可以通过找到所有trace切片中最早的时间戳作为起始点,然后将其他时间戳减去这个起始时间,从而实现相对时间的对齐。 ```python import pandas as pd # 假设有两个trace切片,每个切片是一个包含时间戳和事件的DataFrame trace1 = pd.DataFrame({ 'timestamp': [100, 110, 120], 'event': [
recommend-type

Flink与Kafka实时数据充实流测试指南

根据给定的文件信息,我们将详细讨论以下知识点: 1. Apache Flink和Kafka在实时数据处理中的应用: Apache Flink是一个开源的流处理框架,用于在高吞吐量下进行有状态的计算。它特别适合实时数据处理场景,能够快速地处理无边界和有边界的数据流。Kafka是一个分布式流处理平台,主要用于构建实时数据管道和流应用程序。Flink与Kafka结合使用时,可以实现高效且可靠的数据摄入与处理流程,从而完成复杂的实时数据转换和分析任务。 2. 实时数据充实(Data Enrichment)概念: 数据充实是数据工程中的一个常见概念,指的是通过添加额外信息来增强数据的过程。在实时数据流处理中,数据充实通常用于为原始数据添加元数据、上下文信息或其他相关数据,以便对数据进行更全面的分析。例如,在零售行业中,通过实时数据充实,可以将销售数据与库存数据、价格信息等进行关联,从而获取更有价值的业务洞察。 3. 实践操作的先决条件和环境配置: - 在安装Flink之前,应确保系统满足最低硬件要求,即至少4GB可用内存。这是因为实时数据处理和流计算可能会占用较多计算资源,特别是内存资源。 - 存储库中包含的脚本和命令应在Linux或OS X操作系统上执行,这说明了Flink环境对操作系统有一定的要求,以确保最佳的运行效率和兼容性。 - 执行存储库中的脚本前需要确保脚本文件权限正确,即文件应设置为可执行(chmod +x ./start.sh)。这是基本的Linux系统操作,确保脚本文件具有正确的权限,以便能够被系统执行。 4. 本地环境的搭建与运行: - 提供了一个名为“start.sh”的脚本,用于本地环境的搭建和运行。执行此脚本后,需要在浏览器中输入指定的地址(http://localhost:8080和http://localhost:8081),以访问运行中的Flink和Kafka界面。这表明了如何在本地机器上快速搭建和启动一个实时数据处理和展示平台。 - Flink和Kafka的界面地址用于在研讨会期间展示相关数据处理结果,说明了如何利用这些工具的可视化特性来更好地理解和分析数据流处理过程。 5. 内容的表达方式和格式: - 该存储库中的内容主要通过名为“flink-kafka-workshop1”的笔记本进行表达。笔记本格式为一种方便记录和展示数据处理过程的方式,它通常包含一系列的代码、命令和注释,以便开发者更好地理解每一步的操作和结果。 - 笔记本的格式方便进行编码练习和知识分享,它使得实时数据处理的步骤和过程可视化,并且可以作为教学材料和学习资源。 6. Dockerfile的使用: 虽然给定文件中没有直接提及Dockerfile的内容,但根据标签可以推断,该存储库或相关环境可能涉及使用Docker容器技术。Dockerfile用于编写指令集,以自动化构建Docker镜像的过程。它通常包含了操作系统环境配置、依赖安装、服务部署等步骤,用于创建一个可以运行Flink和Kafka等应用程序的轻量级、可移植的容器环境。这说明了如何利用现代的容器化技术来简化大数据应用的部署和分发。 综上所述,该存储库涉及的知识点广泛,包括了实时数据处理、数据丰富、系统环境配置、本地运行环境搭建以及Docker容器技术的应用。通过实践操作,学习者可以深入理解Flink和Kafka在实时数据处理场景下的工作原理和应用方法。
recommend-type

前端应用中异步数据处理与获取的实践指南

### 前端应用中异步数据处理与获取的实践指南 在现代前端开发中,异步数据处理和获取是常见的需求。本文将介绍如何使用 JavaScript 的 `async/await` 语法简化异步操作,以及如何在 Stimulus 和 React 应用中实现数据的获取和更新。 #### 1. 异步操作与 `async/await` 在 JavaScript 中,处理多个异步操作时,传统的 `then` 链式调用可能会使代码变得复杂。例如: ```javascript updateData() { fetch("/sold_out_concerts").then((response) => {
recommend-type

ref,toRef,toRefs区别?

ref、toRef、toRefs都是Vue 3中用于创建响应式数据的方法,它们的区别如下: - **创建数据类型及响应式原理**:ref用于生成值类型的响应式数据,是对原始数据的拷贝,修改ref数据时,模板中的视图会改变,但原始数据不变。当修改ref的数据时,通过其`.value`属性进行操作,模板中的视图会相应更新,但原始数据不受影响。定义基础类型的响应式数据通常使用ref。例如: ```vue <template> <div>{{ num }}</div> </template> <script setup> import { ref } from 'vue'; let origin
recommend-type

OVER集成文档:虚拟传输与服务集成指南

在深入理解OVER集成文档之前,我们首先应该明确文档中所涉及的关键概念和技术组件。本篇文档主要介绍的是一个名为OVER的服务,其专注于虚拟传输和接收办公室服务相关的消息传递,并提供了一系列与之关联的操作功能。以下是对标题和描述中所蕴含知识点的详细阐述: 1. 集成文档简介 文档的简介部分提供了对OVER服务的总览。首先,它指出本文档旨在详细介绍与虚拟传输和接收办公室服务相关的消息传递内容。虚拟传输通常指的是在不同实体间通过网络进行的数据交换过程。接收办公室服务,则可能涉及到对数据的接收、存储、处理和分发的环节。 此外,简介中还提到,为了实现与OVER服务的集成,需要预先了解AOC联盟的PCI通用消息文档。这表明了文档使用者需要具备一定的前置知识,即对AOC联盟及其PCI标准有一定的认识,这是进行集成和后续操作的前提。 2. 可用的数据传输 文档进一步详细说明了通过OVER服务可以访问和操作的数据类型: - 发行人(Publisher):加泰罗尼亚开放行政联盟(Catalan Open Administration Alliance)是发布此服务的实体,表明了服务的官方来源和背书。 - 产品(Product):文档列出了几个核心的OVER相关产品,包括: - OVER_DOCUMENTATION:与特定流程关联的文档下载操作。 - OVER_FORMULARI:获取处理程序表单的操作,可能用于提交或查阅特定业务流程所需的表单。 - OVER_CONTEXT:获取过程上下文数据的操作,上下文数据可能包括与业务流程相关的各种环境变量或状态信息。 - OVER_PROCESSING:启动处理实例的操作,用于实例化业务流程的执行。 - OVER_UPDATE:允许向服务实例中分配信息的操作,如更新状态、文件编号和其他相关数据。 - OVER_CONSULT:提供查询功能,可以查询特定日期范围内的处理实例,这对于跟进和审计历史业务操作非常有帮助。 - OVER_LIST_FILES:列出基于关键参数或搜索执行的过程中的文件。这对于管理存储在服务中的文件数据是必要的,尤其在处理大量文档时能极大提升效率。 3. 文件名称列表 标题中提到的“OVER-main”很可能指的是包含在本集成文档中的主要文件或脚本。该文件可能是实现上述数据传输和操作功能的源代码、配置文件或其他重要资源。 总结以上内容,我们可以得知: - OVER服务是一个专注于虚拟传输和接收办公室业务流程的消息传递服务。 - 通过该服务,用户可以下载与流程相关的文档、获取和提交表单、管理处理实例、更新服务实例信息以及查询和列出相关文件。 - 在进行集成前,用户需要掌握AOC联盟的PCI通用消息标准的相关知识。 - “OVER-main”是本集成文档中所包含的核心文件或脚本,可能涉及到实际的服务部署或操作流程。 本集成文档为用户提供了详细的指导,帮助开发者或业务分析师理解如何使用OVER服务以及如何与其他系统进行集成,同时强调了遵循相关标准的重要性。
recommend-type

前端开发技术:React与Hotwire对比及相关技术要点解析

### 前端开发技术:React与Hotwire对比及相关技术要点解析 #### 1. 代码片段及功能解析 首先来看一段关键代码: ```typescript interface FavoritesControllerData { type: "addFavorite" | "removeFavorite" concertId: number } const getConcerts = (state: ScheduleState): Concert[] => { return Object.values(state.scheduleDays).flatMap(
recommend-type

html5文字水平居中

在HTML5中实现文字水平居中可以通过不同的方式,以下为几种常见的方法: ### 使用CSS的text-align属性 对于块级元素内的文本,可以使用`text-align:center`来实现水平居中。示例代码如下: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <style> div { width: 500px; height: 100px; float: left; }