前言
本课题研究的基本内容为关于镜头解像力评测国际标准ISO12233中的空间响应频率算法(SFR),基于2014年版本,开发出一款简单、操作方便快捷的可以直接输出图片SFR值结果的软件,包括输入一张图片、显示,并给出SFR值结果,生成曲线图进行直观展示。并且对SFR算法源码进行更改,原来是只能对PGM图片进行检测,更改成可以对BMP格式图片进行检测。
涉及到的知识点,有对BMP文件格式的理解,对MFC实现简单软件界面的学习,对网上下载的算法如何更改的经验(一步步调试),对统筹代码实现功能的项目经验。
学习资料是哔哩哔哩的MFC视频、一本MFC的书、和csdn。
理论
解像力就是分辨被摄原物细节的能力。 解像力是用来描述缩微摄影系统再现被摄原件细微部分能力的物理量,是评价摄影系统成像质量的重要指标。
SFR是空间频率响应(Spatial frequency response)的英文缩写,是指一个系统相对于输入的空间频率所输出的振幅响应,对于摄像系统,SFR类似于传统光学系统的MTF(modulation transfer function,调制传递函数),可以很直观地判定系统的解像能力。
1、mitre sfr 1.4.2编译
1.1 用Mingw编译
需要提前装好MinwG,在环境变量path里添加MinwG的bin路径,并配置好gcc、g++、mingw32-make。
起初还尝试在vs2017里进行编译,发现导入这个包那个库(比如tiff),改这个句那个词的,都不太好使啊。然后才想起可以在cmd里gcc编译,具体步骤如下:
1、win+R cmd
2、cd 文件目录
3、gcc -c mitre_sfr.c //gcc编译生成.o
4、gcc -c find_area.c //gcc编译生成.o
5、gcc -c sfr_iso.c //gcc编译生成.o
5、gcc -o test mitre_sfr.o find_area.o sfr_iso.o //gcc连接.o生成test.exe可执行文件
1.2 在VS2017进行编译
参考文章windows vs 运性SFR
后来仔细观察代码,发现只要做这简单的两步即可。前面1-4可以不用看了。
- mitre_sfr.c在“#if !defined(MSDOS)”前面添加下面几句宏定义即可
#ifndef MSDOS
#define MSDOS
#endif
- 打开工程文件的属性->c/c+±>预处理器->预处理器定义 中添加:
_CRT_SECURE_NO_WARNINGS
此方法似乎是解决代码因版本问题而产生安全性的常用方法。
2、test.exe参数的理解
Edge.pgm
ROI区域是人为规定的,按照官方给定的数据,规定的结果展示如下图:
七个参数
//这里的输入输出是按照in-out.doc(官方提供的输入输出数据进行测试的)
Enter run options…or…Press RETURN if none: de //因为enter不好传,所以在mitre_sfr.cpp里把相关代码去掉,test.exe里即不出现这段代码。
Enter image filename: Edge.pgm //输入需要测量的图片的名称
OECF: if nonlinear enter filename, if linear press RETURN oecf.txt //非线性输入文件名oecf.txt oecf.txt是辅助完成算法的一个工具性文档 要么空着 要么写了就必须有对应的内容 不然就会报错
//一开始以为oecf.txt是运行SFR需要写入的数据,所以写了一个空的txt文本,准备接受sfr算法运行的结果,结果却老是报错”OECF range does not cover the entire image“的错误。后来才意识到这点,为oecf.txt添加对应的内容(即官方提供的oecf.txt对应内容)。
Enter pixels per inch (PPI): 500 //在图像中每英寸所表达的像素数目 分辨率 越高越好
//ROI区域不是任意选取的
Enter Col, Row for UL pixel of ROI: 130 66 //ROI区域左上角的坐标(最左上角是(0,0))
//ROI区域:算法重点计算的区域
Enter Width, Height for region of interest: 120 260 //ROI区域宽高
SFROUT.txt的输出是什么?
其中cy/mm指的是周期每mm,单位长度的周期出现的周期数。
如上的数据总共有120行,对应ROI区域的宽度是120,相当于是在某一列,对ROI区域的所有行进行采样得到的结果。
3、对SFR源码的更改
方法是一步步调试,将需要得到的输入改为从MFC用户输入自动获取(测试时用main.cpp 全局静态数据进行调试)。SFR源码除了支持PGM格式和TIFF格式外,还支持BMP格式(有无其他格式支持并不知道),只需要提供相应的header bytes(头字节 字节数),和图像宽高。
PGM格式和BMP格式
PGM格式头字节是22位
BMP格式头字节是14位
BMP文件的数据按照从文件头开始的先后顺序分为四个部分:
1、bmp文件头(bmp file header):提供文件的格式、大小等信息。
2、位图信息头(bitmap information):提供图像数据的尺寸、位平面数、压缩方式、颜色索引等信息。
3、调色板(color palette):可选,如使用索引来表示图像,调色板就是索引与其对应的颜色的映射表。
4、位图数据(bitmap data):就是图像数据。
因为本设计对SFR算法源码的改动,只需要获取到bmp文件头的信息即可,所以下面对bmp文件头格式展开来进行讲解。BMP的文件头共有14个字节,其中1-2个字节是位图的文件头,一般都是‘BM’,表示的是Windows支持的BMP格式;3-6字节是整个文件的大小;7-10字节是保留字节,必须设置为0,11-14字节是从文件头到位图数据之间的偏移量。
比如对edge.bmp,此图像的文件头是:BM8uSTXNULNULNULNULNUL6EOTNULNUL(其中STX、NUL、EOT都是ASCII 码((American Standard Code for Information Interchange): 美国信息交换标准代码))。文件头共有14个字节,其中1-2个字节是‘BM’,表示的是Windows支持的BMP格式;3-6字节是整个文件的大小;7~10字节是保留字节,必须设置为0,11-14字节是从文件头到位图数据之间的偏移量,是由ASCII码的形式呈现的,经过转化为十进制数,得到头文件到位图数据的偏移量为1078(字节),将此数值用于之后SFR算法源码对图像信息的读取,得到结果是正确的。
*ASC码的一个字,是一个字节byte大小,用二进制表示,由八个bit组成。
文件流!
对写到本地SFROUT.txt文件流手动关闭,不然从别的代码调用时,除非直接用exit()退出程序,否则无法在函数运行时,完成写到本地文件的操作。这一步是因为注意到对输出到命令行窗口的文件流进行了fclose(),而对输出到本地的文件流并没有显式关闭。
fclose(g_mtfout);
整合代码
将SFR的算法mirte_sfr.cpp里的main(argc,argv)改写为submain(),添加mirte_sfr.h,用mfc的代码包含mirte_sfr.h。
//用extern关键字在C++代码里添加C代码
extern "C" {
#include "mitre_sfr.h"
}
本来的想法是先写到SFROUT.txt的本地文档,再从本地文档读取数值(一组double)。后来觉得太傻,就用全局变量double*对数值进行了接收。
在mirte_sfr.cpp里声明double* sfr_data、cymm_data、data_length
,在MFC.cpp里声明
extern double* cymm_data;
extern double* sfr_data;
extern int data_length;
//extern关键字声明变量,表示此变量在别的代码文件里被定义,以防止重复定义,且实现共享`
malloc和new
知道数据的长度后,要创建对应长度的double数组,但是怎么初始化长度呢?因为是在c代码里操作,用不到new方法,所以用对应的malloc方法。
cymm_data = malloc(sizeof(double)*data_length);
sfr_data = malloc(sizeof(double)*data_length);
在使用完这些数据后,用free方法(对应new的delete对内存进行回收)。
free(cymm_data);
free(sfr_data);
4、MFC的实现 版本1
想法:注意到SFR源码里的mitre_sfr.c里main.cpp可以传参进去(参数列表的argc、argv:main(int argc, char **argv){…};),所以想法是用MFC生成的软件(即test02.exe)调用编译好的sfr.exe,并根据用户的输入动态传递参数,将SFR算法运行的结果写到当前目录的SFROUT.txt文件。
关于MFC代码
MFC代码是一个自前往后、层层包含的结构,其中cpp和dlg是一一对应的关系。新建一个子窗口Dlg,就要为其新建对应的类,才能添加对应的方法,否则是只有样子没有里子,添加对应的方法也要注意跟Dlg、cpp要对应上。此外,父窗口包含子窗口的.h。
更新:
问题1:MFC界面不够美观,需要添加图片。
问题2:只是写到txt不够,李老师的需求是生成一张图片展示,想到用opengl绘图,绘图的数据来源于上面生成的SFROUT.txt文档。
opengl对数据绘制的代码完成后,生成paint.exe,采用调用sfr.exe同样的函数进行。这样,在算法运行完后,生成文档,也生成图片。
获取输入并传参调用exe
将edit control的输入获取打包成CString,用ShellExcute()方法传参调用可执行文件。
//输入完数据后,点击MFC的确定按钮,将数据打包传入
void Ctest02Dlg::OnBnClickedOk()
{
// TODO: 在此添加控件通知处理程序代码
CDialogEx::OnOK();
//设置七个cstring类型的变量接受七个edit control(文本输入框)中用户输入的数据
CString cs1,cs2, cs3, cs4, cs5, cs6, cs7;
GetDlgItemText(IDC_EDIT1, cs1);
GetDlgItemText(IDC_EDIT2, cs2);
GetDlgItemText(IDC_EDIT3, cs3);
GetDlgItemText(IDC_EDIT4, cs4);
GetDlgItemText(IDC_EDIT5, cs5);
GetDlgItemText(IDC_EDIT6, cs6);
GetDlgItemText(IDC_EDIT7, cs7);
// 将用户输入的数据做处理,组装成cs参数
CString cs = L".\\sfr\\";
cs += cs1; cs += " ";
cs += ".\\sfr\\"; cs += cs2; cs += " ";
cs += cs4; cs += " ";
cs += cs5; cs += " ";
cs += cs6; cs += " ";
cs += cs7; cs += " ";
cs += cs3;
ShellExecute(NULL, L"open", L".\\sfr\\sfr.exe", cs, NULL, SW_HIDE); //打开已经编译好的sfr.exe,将cs(参数)传递进去,计算
// "open",表示执行由参数指定的程序,或者打开由参数指定的文件或文件夹。
//“.\\sfr\\test.exe”,表示打开的程序名称,sfr文件夹下的test.exe
// “cs”是打开exe时传入的参数列表
//SW_HIDE是调用的程序不对用户显示,在后台运行
MessageBox(_T("请到代码目录下SFROUT.txt查看结果"), _T("运行成功!")); //弹出提示窗口,提示把结果写进源码目录下的SFROUT.txt
ShellExecute(NULL, L"open", L".\\Paint.exe", NULL, NULL, SW_NORMAL); //打开已经编译好的Paint.exe,显示图片
}
将初始界面设置为图片
//test04Dlg.cpp: BOOL Ctest04Dlg::OnInitDialog(){}内部分代码:
//启动页面设置为一张图片
CString s = _T("D:\\Project\\XQJ\\Code\\test04\\test04\\res\\windows3.bmp");
CImage img;
img.Load(s);
HBITMAP hbmp = img.Detach();
CBitmap bmp;
bmp.Attach(hbmp);
_brush.CreatePatternBrush(&bmp);
界面图片:
MFC的实现 最终版本
CString转char*
char * CStringtochar(CString str)
{
char *ptr;
#ifdef _UNICODE
LONG len;
len = WideCharToMultiByte(CP_ACP, 0, str, -1, NULL, 0, NULL, NULL);
ptr = new char[len + 1]; memset(ptr, 0, len + 1);
WideCharToMultiByte(CP_ACP, 0, str, -1, ptr, len + 1, NULL, NULL);
#else
ptr = new char[str.GetAllocLength() + 1];
sprintf(ptr, _T("%s"), str);
#endif
return ptr;
}
点击按钮打开图片并显示
void SFRDlg::OnBnClickedOk()
{
//打开文件
//CString filter = (CString)"图像文件(*.bmp)|*.bmp;*.BMP|图像文件(*.pgm)|*.pgm;*.PGM||";
//指明可供选择的文件类型和相应的扩展名
CString filter = (CString)"图像文件(*.bmp)|*.bmp;*.BMP||";//指明可供选择的文件类型和相应的扩展名
CFileDialog dlg(TRUE, NULL, NULL, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, filter, NULL);
//按下确定按钮 dlg.DoModal() 函数显示对话框
if (dlg.DoModal() == IDOK)
{
//打开对话框获取图像信息
CString BmpName = dlg.GetPathName();
CString EntName = dlg.GetFileExt();
CString BmpFileName = dlg.GetFileName();
CString BmpPathName = dlg.GetPathName();
EntName.MakeLower();
//从本地目录拷贝文件到当前目录
char * source = CStringtochar(BmpPathName);//源文件
char temp2[] = "edge.bmp";
char * destination = temp2;//目标文件
CopyFile(source, destination, FALSE);//false代表覆盖,true不覆盖
SetDlgItemText(IDC_EDIT2, BmpFileName);
if (EntName.Compare(_T("bmp")) == 0)
{
//定义变量存储图片信息
BITMAPINFO *pBmpInfo;
BYTE *pBmpData;
BITMAPFILEHEADER bmpHeader; //文件头
BITMAPINFOHEADER bmpInfo;
CFile bmpFile;
//以只读的方式打开文件 读取bmp图片各部分 bmp文件头 信息 数据
if (!bmpFile.Open(BmpName, CFile::modeRead | CFile::typeBinary))
return;
if (bmpFile.Read(&bmpHeader, sizeof(BITMAPFILEHEADER)) != sizeof(BITMAPFILEHEADER))
return;
if (bmpFile.Read(&bmpInfo, sizeof(BITMAPINFOHEADER)) != sizeof(BITMAPINFOHEADER))
return;
pBmpInfo = (BITMAPINFO *)new char[sizeof(BITMAPINFOHEADER)];
//读取头文件长度 传给header_bytes img_width等变量(unsigned interesting)
header_bytes = bmpHeader.bfOffBits;
img_width = bmpInfo.biWidth;
img_height = bmpInfo.biHeight;
//为图像数据申请空间
memcpy(pBmpInfo, &bmpInfo, sizeof(BITMAPINFOHEADER));
DWORD dataBytes = bmpHeader.bfSize - bmpHeader.bfOffBits;//图像数据大小,单位为字节
pBmpData = (BYTE*)new char[dataBytes];
bmpFile.Read(pBmpData, dataBytes);
bmpFile.Close();
CStatic *pwnd = (CStatic*)GetDlgItem(IDC_STATIC_PIC);
pwnd->ModifyStyle(0xf, SS_BITMAP);
pwnd->SetBitmap((HBITMAP)::LoadImage(GetModuleHandle(NULL), BmpName, IMAGE_BITMAP, 330, 330, LR_LOADFROMFILE));//打开图片
}
}
}
绘制图形
//画线
void DrawLine(CDC* pDC, int x1, int y1, int x2, int y2)//写好的画直线函数
{
pDC->MoveTo(x1, y1);
pDC->LineTo(x2, y2);
}
//点击按钮获取参数 传递参数 绘图
void SFRDlg::OnBnClickedButton2()
{
//画矩形覆盖已有的曲线 下面再重新画 不然曲线会重复出现
CDC* pDC = new CDC;
pDC = GetDC();
CRect rec(630, 120, 900, 360);
CBrush brush(RGB(240, 240, 240));
pDC->FillRect(rec, &brush);
//获取数据
CString cs1, cs2, cs3, cs4, cs5, cs6, cs7;
GetDlgItemText(IDC_EDIT2, cs2);
GetDlgItemText(IDC_EDIT1, cs1);
GetDlgItemText(IDC_EDIT3, cs3);
GetDlgItemText(IDC_EDIT4, cs4);
GetDlgItemText(IDC_EDIT5, cs5);
GetDlgItemText(IDC_EDIT6, cs6);
GetDlgItemText(IDC_EDIT7, cs7);
//删除本地已存在的SFROUT文档 重新输出
std::fstream delte_file("SFROUT.txt", std::ios::out);
//格式转换 cstring 转为char* double int
input_imgname=CStringtochar(cs2);
input_filename = CStringtochar(cs1);
ppi=(double)atoi(CStringtochar(cs3));
roi_ulx = atoi(CStringtochar(cs4));
roi_uly = atoi(CStringtochar(cs5));
roi_width = atoi(CStringtochar(cs6));
roi_height = atoi(CStringtochar(cs7));
subMain();
MessageBox("运行成功!");
//绘制曲线 因为不会获取控件的位置 所以这些坐标数据是自己测出来的
int graph_x = 630;
int graph_y = 360;
for (int i = 0; i < data_length-1; i++) {
DrawLine(pDC, cymm_data[i]*12+graph_x, graph_y-sfr_data[i]*220, cymm_data[i+1]*12+graph_x, graph_y - sfr_data[i+1]*220);//调用它
}
//回收内存
free(cymm_data);
free(sfr_data);
//用完就删 不知道为啥删不掉
char temp1[] = "edge.bmp";
char * destination = temp1;//目标文件
DeleteFile(destination);
char temp2[] = "oecf.txt";
destination = temp2;
DeleteFile(destination);
}
退出程序
调试时,点击退出button,调试未退出(仍在后台运行?),遂添加此函数
void SFRDlg::OnCancel()
{
HANDLE hself = GetCurrentProcess();
TerminateProcess(hself, 0);
CDialogEx::OnCancel();
}
绘制坐标系
绘制出的函数图像的箭头、坐标都是用static text实现的,在程序一开始就有,所以为了配合,应添加坐标系的直线,但在oninit()函数里添加并没有用,遂在预处理函数里添加画坐标系的语句。
BOOL SFRDlg::PreTranslateMessage(MSG* pMsg)
{
// TODO: 在此添加专用代码和/或调用基类
CDC* pDC = new CDC;
pDC = GetDC();
//画坐标系
DrawLine(pDC, 630, 120, 630, 360);
DrawLine(pDC, 630, 360, 900, 360);
return CDialogEx::PreTranslateMessage(pMsg);
}
5、打包发布
用winRAR打包成exe,即可使用。
原理是双击此exe时,解压并存储到本地的一个临时文件夹,运行完毕后将此文件夹删除。
无用功:OpenGL的实现
考虑到画图的需求,所以采用OpenGL来绘制图形,再将opengl的代码打包成exe供mfc主程序调用。
Opengl代码路径:D:\Project\XQJ\Code\PaintTest\Project2
#include <iostream>
#include <fstream>
#include <cassert>
#include <string>
#include <vector>
#include <gl/glut.h>
#include <sstream>
using namespace std;
//存储从SFROUT.txt读取来的数据
vector<string> cymm_sfr_data;
//模板函数:将string类型变量转换为常用的数值类型(此方法具有普遍适用性)
//用到string流 sstream头文件
template <class Type>
Type stringToNum(const string& str)
{
istringstream iss(str);
Type num;
iss >> num;
return num;
}
//从SFROUT.txt读取数据
void readData() {
ifstream infile;
infile.open("SFROUT.txt", ios::in); //将文件流对象与文件连接起来
if (!infile.is_open())
{
cout << "文件打开失败" << endl;
}
string buf;
bool is_data = false;
while (getline(infile, buf))
{
if (buf == "(obj.plane)") {
is_data = true;
continue;
}
if (buf == "# Frequencies above Nyquist") {
is_data = false;
break;
}
if (is_data) {
//为什么有错 同一个字符串不能分两次?
/*cymm_data.push_back(buf.substr(0, 5));
sfr_data.push_back(buf.substr(6, 9));*/
cymm_sfr_data.push_back(buf.substr(0,17));
//cout << cymm_sfr_data.back() << endl;
}
}
infile.close(); //关闭文件输入流
}
void display()
{
GLfloat x;
float cymm_data;
float sfr_data;
glClear(GL_COLOR_BUFFER_BIT);
glBegin(GL_LINES);
glVertex2f(-0.8,-0.8);
glVertex2f(0.9, -0.8);
glVertex2f(-0.8, -0.8);
glVertex2f(-0.8, 0.9);
glEnd();
//数据解包并绘图
//数据举例: cymm_sfr_data :" 5.678 1.032"
//对于string类型的理解:" "跟"\0"不同,前者是空格,后者是空字符,空字符代表一个字符串的结尾,
//本身不具备长度,空格长度为1,且就像上面的字符串,有几个空格,长度就是几,
//提取string字符的时候就要注意怎么跳过空格来进行提取,这样才能转成合理的数字,不然获取到的是乱码。
glBegin(GL_LINE_STRIP);
for (string::size_type i = 0; i < cymm_sfr_data.size(); i++) {
string s1, s2;
bool is_sfr = false;
for (auto s : cymm_sfr_data[i]) {
if (s != ' ' && !is_sfr) {
s1 += s;
}
else if (s == ' '&& s1.length()) {
is_sfr = true;
}
if (s != ' ' && is_sfr) {
s2 += s;
}
}
cymm_data=(stringToNum<float>(s1));
sfr_data=(stringToNum<float>(s2));
if (sfr_data > 0) {
glVertex2f(-0.8 + cymm_data * (1.6 / 20.), -0.8 + sfr_data * 1.6);
}
}
glEnd();
glFlush();
}
int main(int argc, char **argv)
{
//Opengl初始化
readData();
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
glutInitWindowPosition(100, 100);
glutInitWindowSize(500, 360);
glutCreateWindow("Mitre_SFR(横轴:cy/mm(0~20),纵轴:SFR(0~1))");
//init();
glutDisplayFunc(display);
glutMainLoop();
return 0;
}