佳能数码单反相机是众多相机SDK里面最难对接的一个,应该说数码相机要比普通工业相机难对接,因为工业相机仅仅只是采集图像,而数码单反相机SDK意味着操作一部相机,有时我们需要像普通相机一样使用数码单反相机,本文就是实现这样的需求,需要实现的功能包括:
1、打开和关闭相机
2、实时显示图像
3、拍照和录像
由于佳能相机拍照和录像的特殊性(通过回调的方式),因此我们定义的相机功能接口如下(适合大部分相机):
/// <summary>
/// 相机接口
/// </summary>
public interface ICamera : IDisposable {
/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
Boolean Init (out String errMsg);
/// <summary>
/// 开始运行
/// </summary>
/// <returns></returns>
Boolean Play (out String errMsg);
/// <summary>
/// 停止运行
/// </summary>
/// <returns></returns>
Boolean Stop (out String errMsg);
/// <summary>
/// 开始录像
/// </summary>
/// <returns></returns>
Boolean BeginRecord (out String errMsg);
/// <summary>
/// 停止录像
/// </summary>
/// <returns></returns>
Boolean EndRecord (out String errMsg);
/// <summary>
/// 拍照
/// </summary>
/// <returns></returns>
Boolean TakePicture (out String errMsg);
/// <summary>
/// 图像源改变事件回调通知
/// </summary>
Action<ImageSource> ImageSourceChanged { get; set; }
/// <summary>
/// 相机名称
/// </summary>
String CameraName { get; }
/// <summary>
/// 新照片回调通知
/// </summary>
Action<String> NewImage { get; set; }
/// <summary>
/// 新录像回调通知
/// </summary>
Action<String> NewVideo { get; set; }
/// <summary>
/// 储存图像文件夹
/// </summary>
String ImageFolder { get; set; }
/// <summary>
/// 储存录像文件夹
/// </summary>
String VideoFolder { get; set; }
/// <summary>
/// 命名规则
/// </summary>
Func<String> NamingRulesFunc { get; set; }
}
View Code
创建相机对象时,类似于这样:
var camera = new Camera {
ImageSourceChanged = n => { this.img.Source = n; }, // 更新图像源
ImageFolder = Path.Combine (Environment.CurrentDirectory, "Images"), // 图像保存路径
VideoFolder = Path.Combine (Environment.CurrentDirectory, "Videos"), // 录像保存路径
NamingRulesFunc = () => (DateTime.Now - new DateTime (1970, 1, 1)).TotalMilliseconds.ToString ("0") // 新文件命名方式
};
相机的实现类比较长,代码已上传至Github:https://github.com/LowPlayer/CanonCamera;源码里面有官方SDK文档和Demo,强烈建议看完第六章的示例,因为Demo封装得太多,不易看懂;
相机的连接:
public Boolean Init (out String errMsg) {
errMsg = null;
lock (sdkLock) {
var err = InitCamera (); // 初始化相机
var ret = err == EDSDK.EDS_ERR_OK;
if (!ret) {
errMsg = "未检测到相机,错误代码:" + err;
Close (); // 关闭相机
}
return ret;
}
}
private UInt32 InitCamera () {
var err = EDSDK.EDS_ERR_OK;
if (!isSDKLoaded) {
err = EDSDK.EdsInitializeSDK (); // 初始化SDK
if (err != EDSDK.EDS_ERR_OK)
return err;
isSDKLoaded = true;
}
err = GetFirstCamera (out camera); // 获取相机对象
if (err == EDSDK.EDS_ERR_OK) {
// 注册回调函数
err = EDSDK.EdsSetObjectEventHandler (camera, EDSDK.ObjectEvent_All, objectEventHandler, handle);
if (err == EDSDK.EDS_ERR_OK)
err = EDSDK.EdsSetPropertyEventHandler (camera, EDSDK.PropertyEvent_All, propertyEventHandler, handle);
if (err == EDSDK.EDS_ERR_OK)
err = EDSDK.EdsSetCameraStateEventHandler (camera, EDSDK.StateEvent_All, stateEventHandler, handle);
// 打开会话
if (err == EDSDK.EDS_ERR_OK)
err = EDSDK.EdsOpenSession (camera);
if (err == EDSDK.EDS_ERR_OK)
isSessionOpened = true;
}
return err;
}
View Code
相机的退出:
private void Close (Boolean isDisposed = false) {
// 关闭实时取景
if ((EvfOutputDevice & EDSDK.EvfOutputDevice_PC) != 0)
Stop (out _);
// 停止录像
if (videoFileWriter != null)
EndRecord (out _);
// 结束会话
if (isSessionOpened) {
lock (sdkLock) {
if (EDSDK.EdsCloseSession (camera) == EDSDK.EDS_ERR_OK)
isSessionOpened = false;
}
}
// 释放相机对象
if (camera != IntPtr.Zero) {
EDSDK.EdsRelease (camera);
camera = IntPtr.Zero;
}
if (isDisposed) {
GCHandle.FromIntPtr (handle).Free (); // 释放当前对象
this.ImageSourceChanged = null;
this.NewImage = null;
this.NewVideo = null;
this.NamingRulesFunc = null;
} else
EDSDK.EdsSetCameraAddedHandler (cameraAddedHandler, handle); // 监听相机连接
}
View Code
获取相机对象:
private UInt32 GetFirstCamera (out IntPtr camera) {
camera = IntPtr.Zero;
// 获取相机列表对象
var err = EDSDK.EdsGetCameraList (out IntPtr cameraList);
if (err == EDSDK.EDS_ERR_OK) {
err = EDSDK.EdsGetChildCount (cameraList, out Int32 count);
if (err == EDSDK.EDS_ERR_OK && count > 0) {
err = EDSDK.EdsGetChildAtIndex (cameraList, 0, out camera);
// 释放相机列表对象
EDSDK.EdsRelease (cameraList);
cameraList = IntPtr.Zero;
return err;
}
}
if (cameraList != IntPtr.Zero)
EDSDK.EdsRelease (cameraList);
return EDSDK.EDS_ERR_DEVICE_NOT_FOUND;
}
View Code
相机连接之后的相机设置:
// 获取相机名称
if (err == EDSDK.EDS_ERR_OK)
err = EDSDK.EdsGetPropertyData (camera, EDSDK.PropID_ProductName, 0, out cameraName);
if (err == EDSDK.EDS_ERR_OK)
err = EDSDK.EdsGetPropertySize (camera, EDSDK.PropID_Evf_OutputDevice, 0, out _, out deviceSize);
// 保存到计算机
if (err == EDSDK.EDS_ERR_OK)
err = SaveToHost ();
if (err == EDSDK.EDS_ERR_OK) {
// 设置自动曝光
if (ISOSpeed != 0)
EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_ISOSpeed, 0, sizeof (UInt32), 0);
// 设置拍摄图片质量
if (ImageQualityDesc != null)
SetImageQualityJpegOnly ();
// 设置曝光补偿+3
if (ExposureCompensation != 0x18)
EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_ExposureCompensation, 0, sizeof (UInt32), 0x18);
// 设置白平衡;自动:环境优先
if (ExposureCompensation != 0)
EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_WhiteBalance, 0, sizeof (UInt32), 0);
// 设置测光模式:点测光
if (MeteringMode != 0)
EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_MeteringMode, 0, sizeof (UInt32), 0);
// 设置单拍模式
if (DriveMode != 0)
EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_DriveMode, 0, sizeof (UInt32), 0);
// 设置快门速度
if (Tv != 0x60)
EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_Tv, 0, sizeof (UInt32), 0x60);
}
View Code
开始实时取景,将画面传输到PC:
public Boolean Play (out String errMsg) {
errMsg = null;
if (camera == IntPtr.Zero) {
if (!Init (out errMsg))
return false;
else
Thread.Sleep (500);
}
if ((EvfOutputDevice & EDSDK.EvfOutputDevice_PC) != 0)
return true;
UInt32 err = EDSDK.EDS_ERR_OK;
lock (sdkLock) {
// 不允许设置AE模式转盘
//if (AEMode != EDSDK.AEMode_Tv)
// err = EDSDK.EdsSetPropertyData(camera, EDSDK.PropID_Evf_Mode, 0, sizeof(UInt32), EDSDK.AEMode_Tv);
// 开启实时取景
if (err == EDSDK.EDS_ERR_OK && (EvfOutputDevice & EDSDK.EvfOutputDevice_PC) == 0)
err = EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_Evf_OutputDevice, 0, deviceSize, EvfOutputDevice | EDSDK.EvfOutputDevice_PC);
}
var ret = err == EDSDK.EDS_ERR_OK;
if (ret) {
thread_evf = new Thread (ReadEvf) { IsBackground = true };
thread_evf.SetApartmentState (ApartmentState.STA);
thread_evf.Start ();
} else
errMsg = "开启实时图像模式失败,错误代码:" + err;
return ret;
}
View Code
关闭实时取景:
public Boolean Stop (out String errMsg) {
errMsg = null;
if (camera == IntPtr.Zero || (EvfOutputDevice & EDSDK.EvfOutputDevice_PC) == 0)
return true;
var err = EDSDK.EDS_ERR_OK;
// 停止实时取景
lock (sdkLock) {
if (DepthOfFieldPreview != EDSDK.EvfDepthOfFieldPreview_OFF)
err = EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_Evf_DepthOfFieldPreview, 0, sizeof (UInt32), EDSDK.EvfDepthOfFieldPreview_OFF);
if (err == EDSDK.EDS_ERR_OK && (EvfOutputDevice & EDSDK.EvfOutputDevice_PC) != 0)
err = EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_Evf_OutputDevice, 0, deviceSize, EvfOutputDevice & ~EDSDK.EvfOutputDevice_PC);
}
if (err != EDSDK.EDS_ERR_OK)
errMsg = "关闭实时图像模式失败,错误代码:" + err;
return err == EDSDK.EDS_ERR_OK;
}
View Code
获取实时取景画面:
private void ReadEvf () {
// 等待实时图像传输开启
SpinWait.SpinUntil (() => (EvfOutputDevice & EDSDK.EvfOutputDevice_PC) != 0, 5000);
IntPtr stream = IntPtr.Zero;
IntPtr evfImage = IntPtr.Zero;
IntPtr evfStream = IntPtr.Zero;
UInt64 length = 0, maxLength = 2 * 1024 * 1024;
var err = EDSDK.EDS_ERR_OK;
// 当实时图像传输开启时,不断地循环
while (isSessionOpened && (EvfOutputDevice & EDSDK.EvfOutputDevice_PC) != 0) {
lock (sdkLock) {
err = EDSDK.EdsCreateMemoryStream (maxLength, out stream); // 创建用于保存图像的流对象
if (err == EDSDK.EDS_ERR_OK) {
err = EDSDK.EdsCreateEvfImageRef (stream, out evfImage); // 创建evf图像对象
if (err == EDSDK.EDS_ERR_OK)
err = EDSDK.EdsDownloadEvfImage (camera, evfImage); // 从相机下载evf图像
if (err == EDSDK.EDS_ERR_OK)
err = EDSDK.EdsGetPointer (stream, out evfStream); // 获取流对象的流地址
if (err == EDSDK.EDS_ERR_OK)
err = EDSDK.EdsGetLength (stream, out length); // 获取流的长度
}
}
if (err == EDSDK.EDS_ERR_OK)
RenderBitmap (evfStream, length); // 渲染图像
if (stream != IntPtr.Zero) {
EDSDK.EdsRelease (stream);
stream = IntPtr.Zero;
}
if (evfImage != IntPtr.Zero) {
EDSDK.EdsRelease (evfImage);
evfImage = IntPtr.Zero;
}
if (evfStream != IntPtr.Zero) {
EDSDK.EdsRelease (evfStream);
evfStream = IntPtr.Zero;
}
}
// 停止显示图像
context.Send (n => { WriteableBitmap = null; }, null);
}
View Code
拍摄:
public Boolean TakePicture (out String errMsg) {
errMsg = null;
if (camera == IntPtr.Zero) {
errMsg = "未检测到相机";
return false;
}
lock (sdkLock) {
// 存储到计算机
var err = SaveToHost ();
if (err == EDSDK.EDS_ERR_OK) {
err = EDSDK.EdsSendCommand (camera, EDSDK.CameraCommand_PressShutterButton, (Int32) EDSDK.EdsShutterButton.CameraCommand_ShutterButton_Completely); // 按下拍摄按钮
if (err == EDSDK.EDS_ERR_OK)
err = EDSDK.EdsSendCommand (camera, EDSDK.CameraCommand_PressShutterButton, (Int32) EDSDK.EdsShutterButton.CameraCommand_ShutterButton_OFF); // 弹起拍摄按钮
}
if (err != EDSDK.EDS_ERR_OK)
errMsg = "拍照失败,错误代码:" + err;
return err == EDSDK.EDS_ERR_OK;
}
}
View Code
开始录像:
public Boolean BeginRecord (out String errMsg) {
errMsg = null;
if (camera == IntPtr.Zero) {
errMsg = "未检测到相机";
return false;
}
if (videoFileWriter != null)
return true;
if ((EvfOutputDevice & EDSDK.EvfOutputDevice_PC) == 0 && !Play (out errMsg))
return false;
videoFileWriter = new VideoFileWriter ();
stopwatch = new Stopwatch ();
return true;
}
View Code
停止录像:
public Boolean EndRecord (out String errMsg) {
errMsg = null;
if (camera == IntPtr.Zero) {
errMsg = "未检测到相机";
return false;
}
if (videoFileWriter == null)
return true;
lock (videoFileWriter) {
videoFileWriter.Close ();
videoFileWriter = null;
stopwatch.Stop ();
stopwatch = null;
}
return true;
}
View Code
录像使用Accord.Video.FFMPEG.VideoFileWriter类,佳能相机的帧率不稳定,这里使用固定帧率16PFS,这会导致录像文件时长不对,因此需要使用计时器StopWatch计算当前帧的时间戳;
using (var bmp = (Bitmap) imageConverter.ConvertFrom (data)) // 解码获取Bitmap
{
// 获取Bitmap的像素数据指针
var bmpData = bmp.LockBits (new Rectangle (bmpStartPoint, bmp.Size), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
if (videoFileWriter != null) {
lock (videoFileWriter) {
// 保存录像
if (!videoFileWriter.IsOpen) {
var folder = VideoFolder ?? Environment.CurrentDirectory;
if (!Directory.Exists (folder))
Directory.CreateDirectory (folder);
var fileName = NamingRulesFunc?.Invoke () ?? (DateTime.Now - new DateTime (1970, 1, 1)).TotalMilliseconds.ToString ("0");
var filePath = Path.Combine (folder, fileName + ".mp4");
videoFileWriter.Open (filePath, this.width, this.height, 16, VideoCodec.MPEG4); // 使用16FPS,MP4文件保存
spf = 1000 / 16; // 计算一帧毫秒数
stopwatch.Restart ();
frameIndex = 0;
videoFileWriter.WriteVideoFrame (bmpData);
} else {
// 写入视频帧时传入时间戳,否则录像时长将对不上
var frame_index = (UInt32) (stopwatch.ElapsedMilliseconds / spf);
if (frameIndex != frame_index) {
frameIndex = frame_index;
videoFileWriter.WriteVideoFrame (bmpData, frameIndex);
}
}
}
}
bmp.UnlockBits (bmpData);
}
View Code
如果是winfrom,可以使用PictureBox直接渲染Bitmap,本项目使用wpf技术,使用WriteableBitmap高效渲染,在第一帧时创建WriteableBitmap对象,之后将Bitmap数据写入WriteableBitmap的后台缓冲区,监听程序渲染事件CompositionTarget.Rendering不断更新画面;
private WriteableBitmap writeableBitmap;
/// <summary>
/// WPF的一个高性能渲染图像,利用后台缓冲区,渲染图像时不必每次都切换线程
/// </summary>
private WriteableBitmap WriteableBitmap {
get => this.writeableBitmap;
set {
if (this.writeableBitmap == value)
return;
if (this.writeableBitmap == null)
CompositionTarget.Rendering += OnRender;
else if (value == null)
CompositionTarget.Rendering -= OnRender;
this.writeableBitmap = value;
this.ImageSourceChanged?.Invoke (value);
}
}
private void RenderBitmap (IntPtr evfStream, UInt64 length) {
var data = new Byte[length];
var bmpStartPoint = new System.Drawing.Point (0, 0);
Marshal.Copy (evfStream, data, 0, (Int32) length); // 从流地址拷贝一份到字节数组,再解码获取图像(如果可以写一个从指针解码图像,可以优化此步骤)
using (var bmp = (Bitmap) imageConverter.ConvertFrom (data)) // 解码获取Bitmap
{
if (this.WriteableBitmap == null || this.width != bmp.Width || this.height != bmp.Height) {
// 第一次或宽高不对应时创建WriteableBitmap对象
this.width = bmp.Width;
this.height = bmp.Height;
// 通过线程同步上下文切换到主线程
context.Send (n => {
WriteableBitmap = new WriteableBitmap (this.width, this.height, 96, 96, PixelFormats.Bgr24, null);
backBuffer = WriteableBitmap.BackBuffer; // 保存后台缓冲区指针
this.stride = WriteableBitmap.BackBufferStride; // 单行像素数据中的字节数
this.length = this.stride * this.height; // 像素数据的总字节数
}, null);
}
// 获取Bitmap的像素数据指针
var bmpData = bmp.LockBits (new Rectangle (bmpStartPoint, bmp.Size), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
// 将Bitmap的像素数据拷贝到WriteableBitmap
if (this.stride == bmpData.Stride)
Memcpy (backBuffer, bmpData.Scan0, this.length);
else {
var s = Math.Min (this.stride, bmpData.Stride);
var tPtr = backBuffer;
var sPtr = bmpData.Scan0;
for (var i = 0; i < this.height; i++) {
Memcpy (tPtr, sPtr, s);
tPtr += this.stride;
sPtr += bmpData.Stride;
}
}
bmp.UnlockBits (bmpData);
Interlocked.Exchange (ref newFrame, 1);
}
}
private void OnRender (Object sender, EventArgs e) {
var curRenderingTime = ((RenderingEventArgs) e).RenderingTime;
if (curRenderingTime == lastRenderingTime)
return;
lastRenderingTime = curRenderingTime;
if (Interlocked.CompareExchange (ref newFrame, 0, 1) != 1)
return;
var bmp = this.WriteableBitmap;
bmp.Lock ();
bmp.AddDirtyRect (new Int32Rect (0, 0, bmp.PixelWidth, bmp.PixelHeight));
bmp.Unlock ();
}
View Code
下面说一下新人容易踩到的坑:
1、EDSDK的API不能同时调用,否则会卡死;为了解决这个问题,加了一个锁,保证多条线程不能同时调API;
2、同时执行多条API期间可能需要等待500ms,真是坑;
3、图像回调还需要下载,而且下载的是Jpeg文件流而不是BGR24或YUV等RAW数据;因此还需要解码获取BGR24数据;
4、录像必须保存到相机,因此需要存储卡,并且录像文件未编码,因此特别大,1秒1兆的样子,再传回电脑特别慢,再加上上面加锁的关系,卡住其他功能操作;还有录像结束后会自动停止实时图像传输,因此在停止录像后需要等待几秒再打开实时图像传输;并且打开录像模式之后,实时图像传输明显变卡;综合以上原因,我决定不打开录像模式,而是在实时图像传输时保存视频帧;
佳能相机在30分钟未操作后,会自动进入休眠模式,需要通电(或关闭再打开相机)才能调用,这里的解决方案是,创建了相机对象,只要不调用Dispose方法,即使初始化失败,当相机重新连接时,会自动初始化并打开实时图像传输;