海康工业相机镜头阴影矫正LSC
LSC原理简介,部分内容参考:ISP-镜头阴影校正(LSC)
前言
- 镜头阴影校正(Lens Shading Correction)是为了解决由于lens的光学特性,由于镜头对于光学折射不均匀导致的镜头周围出现阴影的情况。
- 由于Lens的光学特性,Sensor影像区的边缘区域接收的光强比中心小,所造成的中心和四角亮度不一致的现象。镜头本身就是一个凸透镜,由于凸透镜原理,中心的感光必然比周边多。如图所示:
实际成像效果如图:
中间区域发亮,四角发黑
海康工业相机SDK LSC算法矫正接口
SDK下载获取
海康机器视觉sdk简介,下载链接:机器视觉下载中心,下载安装MVS即可
示例程序demo,参考“C:\Program Files (x86)\MVS\Development\Samples\VC\VS\SimpleSamples*LensShadingCorrection*”
代码流程简介
LSC矫正主要分为标定文件生成、lsc矫正使用
LSC标定文件生成
核心思想是在取流接口中,或者到一张待标定的图片,调用标定接口进行标定文件生成
如下代码,再回调函数中,调用MV_CC_LSCCalib接口,生成LSCCalib.bin标定文件
void __stdcall ImageCallBackEx(unsigned char * pData, MV_FRAME_OUT_INFO_EX* pFrameInfo, void* pUser)
{
printf("Get One Frame: Width[%d], Height[%d], nFrameNum[%d]\n", pFrameInfo->nWidth, pFrameInfo->nHeight, pFrameInfo->nFrameNum);
int nRet = MV_OK;
//判断是否需要标定
if (true == g_IsNeedCalib)
{
// LSC标定
MV_CC_LSC_CALIB_PARAM stLSCCalib = {
0};
stLSCCalib.nWidth = pFrameInfo->nWidth;
stLSCCalib.nHeight = pFrameInfo->nHeight;
stLSCCalib.enPixelType = pFrameInfo->enPixelType;
stLSCCalib.pSrcBuf = pData;
stLSCCalib.nSrcBufLen = pFrameInfo->nFrameLen;
if (g_pCalibBuf == NULL || g_nCalibBufSize < (pFrameInfo->nWidth*pFrameInfo->nHeight*sizeof(unsigned short)))
{
if (g_pCalibBuf)
{
free(g_pCalibBuf);
g_pCalibBuf = NULL;
g_nCalibBufSize = 0;
}
g_pCalibBuf = (unsigned char *)malloc(pFrameInfo->nWidth*pFrameInfo->nHeight*sizeof(unsigned short));
if (g_pCalibBuf == NULL)
{
printf("malloc pCalibBuf fail !\n");
return;
}
g_nCalibBufSize = pFrameInfo->nWidth*pFrameInfo->nHeight*sizeof(unsigned short);
}
stLSCCalib.pCalibBuf = g_pCalibBuf;
stLSCCalib.nCalibBufSize = g_nCalibBufSize;
stLSCCalib.nSecNumW = 689;
stLSCCalib.nSecNumH = 249;
stLSCCalib.nPadCoef = 2;
stLSCCalib.nCalibMethod = 2;
stLSCCalib.nTargetGray = 100;
//同一个相机,在场景、算法参数和图像参数都不变情况下,理论上只需要进行一次标定,可将标定表保存下来。
//不同相机图片标定出来的标定表不可复用,当场景改变或算法参数改变或图像参数改变时,需要重新标定。
nRet = MV_CC_LSCCalib(pUser, &stLSCCalib);
if (MV_OK != nRet)
{
printf("LSC Calib fail! nRet [0x%x]\n", nRet);
return;
}
//保存标定表到本地
FILE* fp_out = fopen("./LSCCalib.bin", "wb");
if (NULL == fp_out)
{
return ;
}
fwrite(stLSCCalib.pCalibBuf, 1, stLSCCalib.nCalibBufLen, fp_out);
fclose(fp_out);
g_IsNeedCalib = false;
}
}
MV_CC_LSCCalib接口中,需要传入算法参数,来看一下这个结构体MV_CC_LSC_CALIB_PARAM的构成
除了常规的图像宽、高、像素格式之外的参数,部分算法参数如下解释:
参数 | 解释 | 建议值 |
---|---|---|
nSecNumW | 宽度分快数,nSecNumW越大,矫正后一致性越好,但此值越大,会导致部分缺陷类检测缺陷失真;nSecNumW<widthmax | 图像最大宽度除以4 |
nSecNumH | 高度分快数,nSecNumH越大,矫正后一致性越好,但此值越大,会导致部分缺陷类检测缺陷失真;nSecNumH<hightmax | 图像最大高度除以4 |
nPadCoef | 边缘填充系数,范围1-5 | 2 |
nCalibMethod | 标定方式[0-2],提供三种标定方法1:中心为亮度基准,取图像中心区域灰度值为基准进行矫正;2:最亮区域;3:目标亮度,像数格式8bit时,nTargetGray值范围可设置为[0-255];像数格式10bit时,nTargetGray值范围可设置为[0-1023];像数格式12bit时,nTargetGray值范围可设置为[0-4095] | 0 |
nTargetGray | nCalibMethod取值为2时有效 | nCalibMethod为2时,图像中心区域平均亮度 |
typedef struct _MV_CC_LSC_CALIB_PARAM_T_
{
unsigned int nWidth; ///< [IN] \~chinese 图像宽度[16,65535] \~english Image Width
unsigned int nHeight; ///< [IN] \~chinese 图像高度[16-65535] \~english Image Height
enum MvGvspPixelType enPixelType; ///< [IN] \~chinese 像素格式 \~english Pixel format
unsigned char* pSrcBuf; ///< [IN] \~chinese 输入数据缓存 \~english Input data buffer
unsigned int nSrcBufLen; ///< [IN] \~chinese 输入数据长度 \~english Input data length
unsigned char* pCalibBuf; ///< [OUT] \~chinese 输出标定表缓存 \~english Output calib buffer
unsigned int nCalibBufSize; ///< [IN] \~chinese 提供的标定表缓冲大小(nWidth*nHeight*sizeof(unsigned short)) \~english Provided output buffer size
unsigned int nCalibBufLen; ///< [OUT] \~chinese 输出标定表缓存长度 \~english Output calib buffer length
unsigned int nSecNumW; ///< [IN] \~chinese 宽度分块数 \~english Width Sec num
unsigned int nSecNumH; ///< [IN] \~chinese 高度分块数 \~english Height Sec num
unsigned int nPadCoef; ///< [IN] \~chinese 边缘填充系数[1,5] \~english Pad Coef[1,5]
unsigned int nCalibMethod; ///< [IN] \~chinese 标定方式(0-中心为基准;1-最亮区域为基准;2-目标亮度为基准) \~english Calib method
unsigned int nTargetGray; ///< [IN] \~chinese 目标亮度(标定方式为2时有效) \~english Target Gray
///< \~chinese 8位,范围:[0,255] \~english 8bit,range:[0,255]
///< \~chinese 10位,范围:[0,1023] \~english 10bit,range:[0,1023]
///< \~chinese 12位,范围:[0,4095] \~english 12bit,range:[0,4095]
unsigned int nRes[8]; ///< \~chinese 预留 \~english Reserved
}MV_CC_LSC_CALIB_PARAM;
标定过程,需要特定场景的,按照如下步骤进行
1、相机对准均匀场景(均匀光源、白纸、白板等),通过调节曝光、增益等,使得图像灰度在120-160之间(镜头光圈保持与实际使用场景一致)
2、运行标定程序,生成标定文件;可直接运行LensShadingCorrection示例demo,exe路径下会有当前标定文件生成
3、拍摄实际物体,运行矫正程序,观察效果;可直接运行LensShadingCorrection示例demo观察
如果标定矫正效果不佳,那么就需要重新调整标定场景,重新生成标定文件
同理,如遇到多种光源切换场景,可以生成不同的标定文件,进行动态切换矫正
LSC矫正
拿到标定文件后,矫正过程相对简单,获取一张图像后,再调用MV_CC_LSCCorrect接口即可完成(每一张图片都需要调用,一张一张矫正)
void __stdcall ImageCallBackEx(unsigned char * pData, MV_FRAME_OUT_INFO_EX* pFrameInfo, void* pUser)
{
printf("Get One Frame: Width[%d], Height[%d], nFrameNum[%d]\n", pFrameInfo->nWidth, pFrameInfo->nHeight, pFrameInfo->nFrameNum);
int nRet = MV_OK;
// LSC校正
if (g_pDstData == NULL || g_nDstDataSize < pFrameInfo->nFrameLen)
{
if (g_pDstData)
{
free(g_pDstData);
g_pDstData = NULL;
g_nDstDataSize = 0;
}
g_pDstData = (unsigned char *)malloc(pFrameInfo->nFrameLen);
if (g_pDstData == NULL)
{
printf("malloc pDstData fail !\n");
return;
}
g_nDstDataSize = pFrameInfo->nFrameLen;
}
MV_CC_LSC_CORRECT_PARAM stLSCCorrectParam = {
0};
stLSCCorrectParam.nWidth = pFrameInfo->nWidth;
stLSCCorrectParam.nHeight = pFrameInfo->nHeight;
stLSCCorrectParam.enPixelType = pFrameInfo->enPixelType;
stLSCCorrectParam.pSrcBuf = pData;
stLSCCorrectParam.nSrcBufLen = pFrameInfo->nFrameLen;
stLSCCorrectParam.pDstBuf = g_pDstData;
stLSCCorrectParam.nDstBufSize = g_nDstDataSize;
stLSCCorrectParam.pCalibBuf = g_pCalibBuf;
stLSCCorrectParam.nCalibBufLen = g_nCalibBufSize;
nRet = MV_CC_LSCCorrect(pUser, &stLSCCorrectParam);
if (MV_OK != nRet)
{
printf("LSC Correct fail! nRet [0x%x]\n", nRet);
return;
}
//矫正完成,其他图像处理
}
矫正耗时测试
测试1:不同分辨率图像,不同版本算法,耗时情况
分辨率 | x64 | x32 |
---|---|---|
2448*2048 | 4.3ms | 5.6ms |
3840*2748 | 8.6ms | 11.89ms |
5472*3648 | 16.45ms | 22.14ms |
- 算法耗时取决于图像分辨率大小,分辨率越大,耗时越大
测试2:不同算法参数,矫正模块耗时情况
分辨率 | nCalibMethod | 耗时 | 分块数 | 耗时 | 边缘填充系数 | 耗时 |
---|---|---|---|---|---|---|
2448*2048 | 0:中心基准 | 4.25ms | 16*16 | 4.28ms | 1 | 4.26ms |
2448*2048 | 1:最亮基准 | 4.25ms | 600*400 | 4.30ms | 3 | 4.20ms |
2448*2048 | 2:目标基准 | 4.28ms | 2448*2048 | 4.25ms | 5 | 4.26ms |
图像效果对比
测试参数:
参数 | Value |
---|---|
分辨率 | 2448*2048 |
nSecNumW | 2448 |
nSecNumH | 2048 |
nPadCoef | 2 |
nCalibMethod | 0 |
LensShadingCorrection完整示例程序
#include <stdio.h>
#include <Windows.h>
#include <conio.h>
#include <io.h>
#include "MvCameraControl.h"
// ch:等待按键输入 | en:Wait for key press
void WaitForKeyPress(void)
{
while(!_kbhit())
{
Sleep(10);
}
_getch();
}
bool PrintDeviceInfo(MV_CC_DEVICE_INFO* pstMVDevInfo)
{
if (NULL == pstMVDevInfo)
{
printf("The Pointer of pstMVDevInfo is NULL!\n");
return false;
}
if (pstMVDevInfo->nTLayerType == MV_GIGE_DEVICE)
{
int nIp1 = ((pstMVDevInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0xff000000) >> 24);
int nIp2 = ((pstMVDevInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0x00ff0000) >> 16);
int nIp3 = ((pstMVDevInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0x0000ff00) >> 8);
int nIp4 = (pstMVDevInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0x000000ff);
// ch:打印当前相机ip和用户自定义名字 | en:print current ip and user defined name
printf("CurrentIp: %d.%d.%d.%d\n" , nIp1, nIp2, nIp3, nIp4);
printf("UserDefinedName: %s\n\n" , pstMVDevInfo->SpecialInfo.stGigEInfo.chUserDefinedName);
}
else if (pstMVDevInfo->nTLayerType == MV_USB_DEVICE)
{
printf("UserDefinedName: %s\n", pstMVDevInfo->SpecialInfo.stUsb3VInfo.chUserDefinedName);
printf("Serial Number: %s\n", pstMVDevInfo->SpecialInfo.stUsb3VInfo.chSerialNumber);
printf("Device Number: %d\n\n", pstMVDevInfo->SpecialInfo.stUsb3VInfo.nDeviceNumber);
}
else
{
printf("Not support.\n");
}
return true;
}
unsigned char * g_pDstData = NULL;
unsigned int g_nDstDataSize = 0;
unsigned char * g_pCalibBuf = NULL;
unsigned int g_nCalibBufSize = 0;
bool g_IsNeedCalib = true;
void __stdcall ImageCallBackEx(unsigned char * pData, MV_FRAME_OUT_INFO_EX* pFrameInfo, void* pUser)
{
printf("Get One Frame: Width[%d], Height[%d], nFrameNum[%d]\n", pFrameInfo->nWidth, pFrameInfo->nHeight, pFrameInfo->nFrameNum);
int nRet = MV_OK;
//判断是否需要标定
if (true == g_IsNeedCalib)
{
// LSC标定
MV_CC_LSC_CALIB_PARAM stLSCCalib = {
0};
stLSCCalib.nWidth = pFrameInfo->nWidth;
stLSCCalib.nHeight = pFrameInfo->nHeight;
stLSCCalib.enPixelType = pFrameInfo->enPixelType;
stLSCCalib.pSrcBuf = pData;
stLSCCalib.nSrcBufLen = pFrameInfo->nFrameLen;
if (g_pCalibBuf == NULL || g_nCalibBufSize < (pFrameInfo->nWidth*pFrameInfo->nHeight*sizeof(unsigned short)))
{
if (g_pCalibBuf)
{
free(g_pCalibBuf);
g_pCalibBuf = NULL;
g_nCalibBufSize = 0;
}
g_pCalibBuf = (unsigned char *)malloc(pFrameInfo->nWidth*pFrameInfo->nHeight*sizeof(unsigned short));
if (g_pCalibBuf == NULL)
{
printf("malloc pCalibBuf fail !\n");
return;
}
g_nCalibBufSize = pFrameInfo->nWidth*pFrameInfo->nHeight*sizeof(unsigned short);
}
stLSCCalib.pCalibBuf = g_pCalibBuf;
stLSCCalib.nCalibBufSize = g_nCalibBufSize;
stLSCCalib.nSecNumW = 689;
stLSCCalib.nSecNumH = 249;
stLSCCalib.nPadCoef = 2;
stLSCCalib.nCalibMethod = 2;
stLSCCalib.nTargetGray = 100;
//同一个相机,在场景、算法参数和图像参数都不变情况下,理论上只需要进行一次标定,可将标定表保存下来。
//不同相机图片标定出来的标定表不可复用,当场景改变或算法参数改变或图像参数改变时,需要重新标定。
nRet = MV_CC_LSCCalib(pUser, &stLSCCalib);
if (MV_OK != nRet)
{
printf("LSC Calib fail! nRet [0x%x]\n", nRet);
return;
}
//保存标定表到本地
FILE* fp_out = fopen("./LSCCalib.bin", "wb");
if (NULL == fp_out)
{
return ;
}
fwrite(stLSCCalib.pCalibBuf, 1, stLSCCalib.nCalibBufLen, fp_out);
fclose(fp_out);
g_IsNeedCalib = false;
}
// LSC校正
if (g_pDstData == NULL || g_nDstDataSize < pFrameInfo->nFrameLen)
{
if (g_pDstData)
{
free(g_pDstData);
g_pDstData = NULL;
g_nDstDataSize = 0;
}
g_pDstData = (unsigned char *)malloc(pFrameInfo->nFrameLen);
if (g_pDstData == NULL)
{
printf("malloc pDstData fail !\n");
return;
}
g_nDstDataSize = pFrameInfo->nFrameLen;
}
MV_CC_LSC_CORRECT_PARAM stLSCCorrectParam = {
0};
stLSCCorrectParam.nWidth = pFrameInfo->nWidth;
stLSCCorrectParam.nHeight = pFrameInfo->nHeight;
stLSCCorrectParam.enPixelType = pFrameInfo->enPixelType;
stLSCCorrectParam.pSrcBuf = pData;
stLSCCorrectParam.nSrcBufLen = pFrameInfo->nFrameLen;
stLSCCorrectParam.pDstBuf = g_pDstData;
stLSCCorrectParam.nDstBufSize = g_nDstDataSize;
stLSCCorrectParam.pCalibBuf = g_pCalibBuf;
stLSCCorrectParam.nCalibBufLen = g_nCalibBufSize;
nRet = MV_CC_LSCCorrect(pUser, &stLSCCorrectParam);
if (MV_OK != nRet)
{
printf("LSC Correct fail! nRet [0x%x]\n", nRet);
return;
}
if (pFrameInfo->nFrameNum < 10)
{
//保存图像到文件
MV_SAVE_IMG_TO_FILE_PARAM stSaveFileParam;
memset(&stSaveFileParam, 0, sizeof(MV_SAVE_IMG_TO_FILE_PARAM));
stSaveFileParam.enImageType = MV_Image_Bmp;
stSaveFileParam.enPixelType = pFrameInfo->enPixelType;
stSaveFileParam.nWidth = pFrameInfo->nWidth;
stSaveFileParam.nHeight = pFrameInfo->nHeight;
stSaveFileParam.nDataLen = pFrameInfo->nFrameLen;
stSaveFileParam.pData = pData;
sprintf_s(stSaveFileParam.pImagePath, 256, "BeforeImage_w%d_h%d_fn%03d.bmp", stSaveFileParam.nWidth, stSaveFileParam.nHeight, pFrameInfo->nFrameNum);
nRet = MV_CC_SaveImageToFile(pUser, &stSaveFileParam);
if (MV_OK != nRet)
{
printf("SaveImageToFile failed[%x]!\n", nRet);
return;
}
stSaveFileParam.pData = g_pDstData;
sprintf_s(stSaveFileParam.pImagePath, 256, "AfterImage_w%d_h%d_fn%03d.bmp", stSaveFileParam.nWidth, stSaveFileParam.nHeight, pFrameInfo->nFrameNum);
nRet = MV_CC_SaveImageToFile(pUser, &stSaveFileParam);
if (MV_OK != nRet)
{
printf("SaveImageToFile failed[%x]!\n", nRet);
return;
}
}
}
int main()
{
int nRet = MV_OK;
void* handle = NULL;
do
{
// ch:枚举设备 | en:Enum device
MV_CC_DEVICE_INFO_LIST stDeviceList;
memset(&stDeviceList, 0, sizeof(MV_CC_DEVICE_INFO_LIST));
nRet = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList);
if (MV_OK != nRet)
{
printf("Enum Devices fail! nRet [0x%x]\n", nRet);
break;
}
if (stDeviceList.nDeviceNum > 0)
{
for (unsigned int i = 0; i < stDeviceList.nDeviceNum; i++)
{
printf("[device %d]:\n", i);
MV_CC_DEVICE_INFO* pDeviceInfo = stDeviceList.pDeviceInfo[i];
if (NULL == pDeviceInfo)
{
break;
}
PrintDeviceInfo(pDeviceInfo);
}
}
else
{
printf("Find No Devices!\n");
break;
}
printf("Please Input camera index(0-%d):", stDeviceList.nDeviceNum-1);
unsigned int nIndex = 0;
scanf_s("%d", &nIndex);
if (nIndex >= stDeviceList.nDeviceNum)
{
printf("Input error!\n");
break;
}
// ch:选择设备并创建句柄 | en:Select device and create handle
nRet = MV_CC_CreateHandle(&handle, stDeviceList.pDeviceInfo[nIndex]);
if (MV_OK != nRet)
{
printf("Create Handle fail! nRet [0x%x]\n", nRet);
break;
}
// ch:打开设备 | en:Open device
nRet = MV_CC_OpenDevice(handle);
if (MV_OK != nRet)
{
printf("Open Device fail! nRet [0x%x]\n", nRet);
break;
}
// ch:探测网络最佳包大小(只对GigE相机有效) | en:Detection network optimal package size(It only works for the GigE camera)
if (stDeviceList.pDeviceInfo[nIndex]->nTLayerType == MV_GIGE_DEVICE)
{
int nPacketSize = MV_CC_GetOptimalPacketSize(handle);
if (nPacketSize > 0)
{
nRet = MV_CC_SetIntValue(handle,"GevSCPSPacketSize",nPacketSize);
if(nRet != MV_OK)
{
printf("Warning: Set Packet Size fail nRet [0x%x]!", nRet);
}
}
else
{
printf("Warning: Get Packet Size fail nRet [0x%x]!", nPacketSize);
}
}
// ch:设置触发模式为off | eb:Set trigger mode as off
nRet = MV_CC_SetEnumValue(handle, "TriggerMode", MV_TRIGGER_MODE_OFF);
if (MV_OK != nRet)
{
printf("Set Trigger Mode fail! nRet [0x%x]\n", nRet);
break;
}
//判断是否可以本地导入
FILE* fp = fopen("./LSCCalib.bin", "rb");
if (fp)
{
int nFileLen = filelength(fileno(fp));
if (g_pCalibBuf == NULL || g_nCalibBufSize < nFileLen)
{
if (g_pCalibBuf)
{
free(g_pCalibBuf);
g_pCalibBuf = NULL;
g_nCalibBufSize = 0;
}
g_pCalibBuf = (unsigned char *)malloc(nFileLen);
if (g_pCalibBuf == NULL)
{
printf("malloc pCalibBuf fail !\n");
break;
}
g_nCalibBufSize = nFileLen;
}
fread(g_pCalibBuf, 1, g_nCalibBufSize, fp);
fclose(fp);
g_IsNeedCalib = false;
}
// ch:注册抓图回调 | en:Register image callback
nRet = MV_CC_RegisterImageCallBackEx(handle, ImageCallBackEx, handle);
if (MV_OK != nRet)
{
printf("Register Image CallBack fail! nRet [0x%x]\n", nRet);
break;
}
// ch:开始取流 | en:Start grab image
nRet = MV_CC_StartGrabbing(handle);
if (MV_OK != nRet)
{
printf("Start Grabbing fail! nRet [0x%x]\n", nRet);
break;
}
printf("Press a key to stop grabbing.\n");
WaitForKeyPress();
Sleep(1000);
// ch:停止取流 | en:Stop grab image
nRet = MV_CC_StopGrabbing(handle);
if (MV_OK != nRet)
{
printf("Stop Grabbing fail! nRet [0x%x]\n", nRet);
break;
}
// ch:关闭设备 | en:Close device
nRet = MV_CC_CloseDevice(handle);
if (MV_OK != nRet)
{
printf("Close Device fail! nRet [0x%x]\n", nRet);
break;
}
// ch:销毁句柄 | en:Destroy handle
nRet = MV_CC_DestroyHandle(handle);
if (MV_OK != nRet)
{
printf("Destroy Handle fail! nRet [0x%x]\n", nRet);
break;
}
} while (0);
if (g_pCalibBuf)
{
free(g_pCalibBuf);
g_pCalibBuf = NULL;
g_nCalibBufSize = 0;
}
if (g_pDstData)
{
free(g_pDstData);
g_pDstData = NULL;
g_nDstDataSize = 0;
}
if (nRet != MV_OK)
{
if (handle != NULL)
{
MV_CC_DestroyHandle(handle);
handle = NULL;
}
}
printf("Press a key to exit.\n");
WaitForKeyPress();
return 0;
}