65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的 Canon EDSDK 教程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (146投票s)

2013年11月26日

MIT

14分钟阅读

viewsIcon

4145852

downloadIcon

35573

一个关于 Canon SDK 的教程,用于远程控制 DSLR 相机。从拍照到使用 LiveView。

新版本下载

旧版本下载

引言

Canon EOS Digital SDK 是一个功能强大的 SDK,用于远程控制 Canon DSLR。不幸的是,在互联网上很难找到一些好的示例,而且提供的文档也不够完整。由于我已经发现了许多东西,并希望让其他人更容易上手,所以我认为可以整理一些最重要的内容并做一个教程。

本教程和库是用 C# 编写的,但它符合 CLS 标准,因此可以从任何 .NET 语言中使用。

本教程包含

注意:我与佳能公司没有任何隶属关系或资金支持。

我对本软件不作任何保证。请自行承担使用风险!

背景

您必须拥有 Canon EDSDK 的副本才能使其正常工作。我不允许在项目中包含 DLL 文件,因此您必须在此处申请获取它们

获取 DLL 文件后,将它们放在可执行文件旁边。否则,如果主 DLL 调用子 DLL,将会出现问题。

为了使某些方法正常工作,使用相机的全手动或至少半自动模式也很重要。

Using the Code

该解决方案由四个项目组成

  • EDSDKLib:处理所有 SDK 和相机操作的主要项目
  • WinFormsExample:一个示例项目,它使用 EDSDKLib 并在 Windows Forms UI 应用程序中使用它
  • WpfExample:一个示例项目,它使用 EDSDKLib 并在 WPF UI 应用程序中使用它
  • ConsoleExample:一个示例项目,它使用 EDSDKLib 并在控制台应用程序中使用它

我将只关注 EDSDKLib 项目,因为这正是本文的主题:在 C# 中使用 EDSDK。

首先,我们来看看主要的类

  • CanonAPI:主要负责 SDK 生命周期、已连接相机和 SDK 事件的类
  • Camera:用于与物理相机通信。设置/获取属性、拍照、下载数据等。
  • CanonSDK:此类包含所有本地对 Canon SDK DLL 的调用以及一些用于设置/获取值的辅助方法。
  • STAThread: 一个辅助类,用于创建 STA 线程或在 STA 线程上执行代码
  • ErrorHandler:一个静态类,提供检查 SDK 返回值/错误代码的方法

让我们更仔细地研究这些类的内部工作原理。

初始化和终止 SDK

初始化和终止是最简单的事情。当您启动程序时,创建一个新的 CanonAPI 类实例

public CanonAPI(bool useCallingThread)
{
    try
    {
        //Ensure that only one caller at a time can increase the counter
        lock (InitLock)
        {
            //If no instance exists yet, initialize everything
            if (RefCount == 0)
            {
                if (useCallingThread)
                {
                    if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
                        throw new ThreadStateException("Calling thread must be in STA");
                    ErrorHandler.CheckError(this, CanonSDK.EdsInitializeSDK());
                }
                else
                {
                    //Trying to trigger DllNotFoundException so it's not thrown
                    //in the event loop on a different thread:
                    CanonSDK.EdsRelease(IntPtr.Zero);

                    //Start the main thread where SDK will run on
                    MainThread = new ApiThread();
                    MainThread.Start();
                    //Initialize the SDK on the main thread
                    MainThread.Invoke(() => ErrorHandler.CheckError
                                     (this, CanonSDK.EdsInitializeSDK()));
                }

                CanonSDK.InitializeVersion();
                //Subscribe to the CameraAdded event
                CameraAddedEvent = new SDKCameraAddedHandler(CanonAPI_CameraAddedEvent);
                ErrorHandler.CheckError(this, 
                    CanonSDK.EdsSetCameraAddedHandler(CameraAddedEvent, IntPtr.Zero));
                _IsSDKInitialized = true;
            }
            RefCount++;
        }
    }
    catch
    {
        IsDisposed = true;
        if (MainThread?.IsRunning == true) MainThread.Shutdown();
        throw;
    }
}

确保您保留此类的实例,否则 GC 可能会收集它,SDK 也会随之终止(因为 CanonAPI 类的析构函数)。

当您关闭程序时,调用 Dispose 方法。公共 Dispose 方法依次调用此重载

protected virtual void Dispose(bool managed)
{
    //Ensure that only one caller at a time can decrease the counter
    lock (InitLock)
    {
        if (!IsDisposed)
        {
            //If it's the last instance, release everything
            if (RefCount == 1)
            {
                _IsSDKInitialized = false;//Set beforehand because if an error happens, 
                                          //the SDK will be in an unstable state anyway

                //Remove event handler for the CameraAdded event
                ErrorCode err = CanonSDK.EdsSetCameraAddedHandler(null, IntPtr.Zero);
                if (managed)
                {
                    ErrorHandler.CheckError(this, err);
                    //Dispose all the connected cameras
                    CurrentCameras.ForEach(t => t.Dispose());
                }
                //Terminate the SDK
                if (MainThread?.IsRunning == true) 
                    err = MainThread.Invoke(() => { return CanonSDK.EdsTerminateSDK(); });
                //Close the main thread
                if (MainThread?.IsRunning == true) MainThread.Shutdown();
                if (managed) ErrorHandler.CheckError(this, err);
            }
            RefCount--;
            IsDisposed = true;
        }
    }
}

获取已连接的相机

现在 SDK 已经初始化,我们可以获取当前已连接相机的列表

public List<Camera> GetCameraList()
{
    if (IsDisposed) throw new ObjectDisposedException(nameof(CanonAPI));

    //Ensure that only one caller at a time can access the camera list
    lock (CameraLock)
    {
        //Get a list of camera pointers
        IEnumerable<IntPtr> ptrList = GetCameraPointerList();
        List<Camera> camList = new List<Camera>();

        //Find cameras that were connected before and add new ones
        foreach (var ptr in ptrList)
        {
            var oldCam = CurrentCameras.FirstOrDefault(t => t.Reference == ptr);
            if (oldCam != null && !oldCam.IsDisposed) 
                camList.Add(oldCam);           //Pointer exists already so we reuse it
            else camList.Add(new Camera(ptr)); //Pointer does not exists yet, so we add it
        }

        //Ensure that cameras not connected anymore are disposed properly
        var oldCameras = CurrentCameras.Where(t => !ptrList.Any(u => u == t.Reference));
        foreach (var cam in oldCameras) { if (!cam.IsDisposed) cam.Dispose(); }

        CurrentCameras.Clear();
        CurrentCameras.AddRange(camList);
        return camList;
    }
}

CanonAPI 类维护一个已连接相机的列表,以确保每个 SDK 相机指针仅与一个 Camera 类实例相关联。两个或多个使用相同 SDK 指针的 Camera 类实例在同时访问相机时可能会导致问题。

上面使用的子程序 GetCameraPointerList 获取指向 SDK 相机对象的指针列表

protected IEnumerable<IntPtr> GetCameraPointerList()
{
    if (IsDisposed) throw new ObjectDisposedException(nameof(CanonAPI));

    IntPtr camlist;
    //Get camera list
    ErrorHandler.CheckError(this, CanonSDK.EdsGetCameraList(out camlist));

    //Get number of connected cameras
    int camCount;
    ErrorHandler.CheckError(this, CanonSDK.EdsGetChildCount(camlist, out camCount));
    List<IntPtr> ptrList = new List<IntPtr>();
    for (int i = 0; i < camCount; i++)
    {
        //Get camera pointer
        IntPtr cptr;
        ErrorHandler.CheckError(this, CanonSDK.EdsGetChildAtIndex(camlist, i, out cptr));
        ptrList.Add(cptr);
    }
    //Release the list
    ErrorHandler.CheckError(this, CanonSDK.EdsRelease(camlist));
    return ptrList;
}

打开和关闭与相机的会话

目前,我们完成了 CanonAPI 类的工作,可以开始使用 Camera 类并与其打开一个会话

public void OpenSession()
{
    CheckState(false);

    if (!SessionOpen)
    {
        MainThread.Invoke(() =>
        {
            ErrorHandler.CheckError(this, CanonSDK.EdsOpenSession(CamRef));
                    
            //Check if Record is available
            uint property;
            _IsRecordAvailable = CanonSDK.GetPropertyData
                (CamRef, PropertyID.Record, 0, out property) == ErrorCode.OK;

            //Subscribe to events
            SDKStateEvent += new SDKStateEventHandler(Camera_SDKStateEvent);
            SDKPropertyEvent += new SDKPropertyEventHandler(Camera_SDKPropertyEvent);
            SDKProgressCallbackEvent += new SDKProgressCallback
                                        (Camera_SDKProgressCallbackEvent);
            SDKObjectEvent += new SDKObjectEventHandler(Camera_SDKObjectEvent);
            ErrorHandler.CheckError(this, 
            CanonSDK.EdsSetCameraStateEventHandler
                     (CamRef, StateEventID.All, SDKStateEvent, CamRef));
            ErrorHandler.CheckError(this, 
            CanonSDK.EdsSetObjectEventHandler
                     (CamRef, ObjectEventID.All, SDKObjectEvent, CamRef));
            ErrorHandler.CheckError(this, 
            CanonSDK.EdsSetPropertyEventHandler
                     (CamRef, PropertyEventID.All, SDKPropertyEvent, CamRef));

            SessionOpen = true;
        });
    }
}

当您完成与此相机的操作时,可以通过这种方式关闭会话

public void CloseSession()
{
    CheckState(false);

    if (SessionOpen)
    {
        //Unsubscribe from all events
        UnsubscribeEvents();

        //If the live view is on, stop it
        if (IsLiveViewOn)
        {
            KeepLVAlive = false;
            LVThread.Join(5000);
        }

        MainThread.Invoke(() =>
        {
            //Close the session with the camera
            ErrorHandler.CheckError(this, CanonSDK.EdsCloseSession(CamRef));
            SessionOpen = false;
        });
    }
}

在相机连接到计算机时,您可以安全地多次打开和关闭会话。

当您完全完成与此相机的操作后,可以通过调用 Dispose 方法来释放它(与之前一样,它会在内部调用此重载)

protected virtual void Dispose(bool managed)
{
    if (!IsDisposed)
    {
        //Unsubscribe from all events
        UnsubscribeEvents();

        //If the live view is on, stop it
        if (IsLiveViewOn)
        {
            KeepLVAlive = false;
            LVThread.Join();
        }
        IsLiveViewOn = false;

        MainThread.Invoke(() =>
        {
            if (CanonAPI.IsSDKInitialized)
            {
                //If it's open, close the session
                if (SessionOpen) CanonSDK.EdsCloseSession(CamRef);
                //Release the camera
                CanonSDK.EdsRelease(CamRef);
            }
            _IsDisposed = true;
        });
        //Shutdown the main camera thread
        MainThread.Shutdown();
    }
}

UnsubscribeEvents 只是一个小的辅助方法

private void UnsubscribeEvents()
{
    SDKStateEvent -= Camera_SDKStateEvent;
    SDKPropertyEvent -= Camera_SDKPropertyEvent;
    SDKProgressCallbackEvent -= Camera_SDKProgressCallbackEvent;
    SDKObjectEvent -= Camera_SDKObjectEvent;

    if (CanonAPI.IsSDKInitialized)
    {
        MainThread.Invoke(() =>
        {
            //Clear callbacks from Canon SDK
            CanonSDK.EdsSetCameraStateEventHandler(CamRef, StateEventID.All, null, CamRef);
            CanonSDK.EdsSetObjectEventHandler(CamRef, ObjectEventID.All, null, CamRef);
            CanonSDK.EdsSetPropertyEventHandler(CamRef, PropertyEventID.All, null, CamRef);
        });
    }
}

获取相机设置

对于带有 ID 的值,设置和获取相机设置可以非常简单,但对于结构值则更困难。

这是普通int值(如 Tv、Av 或 ISO)的获取方法

public int GetInt32Setting(PropertyID propID, int inParam = 0)
{
    CheckState();

    return MainThread.Invoke(() =>
    {
        int property;
        ErrorHandler.CheckError(this, CanonSDK.GetPropertyData
                               (CamRef, propID, inParam, out property));
        return property;
    });
}

有多种方法可以获取不同类型的值,但它们基本上看起来都一样。重要的是,它们都调用 CanonSDK.GetPropertyData 方法,该方法处理所有详细信息。如果您对如何从托管代码获取非托管值感兴趣,请查看 CanonSDK 类中提到的方法及其重载。

设置相机设置

设置相机设置与获取它类似,但只有两种方法,因为我们可以以更通用的方式处理它,使用object类型。

string之外的任何值都可以用此方法设置(例如,整数用于 Tv、Av 或 ISO)

public void SetSetting(PropertyID propID, object value, int inParam = 0)
{
    CheckState();

    MainThread.Invoke(() =>
    {
        int propsize;
        DataType proptype;
        ErrorHandler.CheckError(this, 
        CanonSDK.EdsGetPropertySize(CamRef, propID, inParam, out proptype, out propsize));
        ErrorHandler.CheckError(this, 
        CanonSDK.EdsSetPropertyData(CamRef, propID, inParam, propsize, value));
    });
}

设置string

public void SetSetting(PropertyID propID, string value, int inParam = 0, int MAX = 32)
{
    CheckState();

    if (value == null) value = string.Empty;
    if (value.Length > MAX - 1) value = value.Substring(0, MAX - 1);

    byte[] propBytes = System.Text.Encoding.ASCII.GetBytes(value + '\0');
    MainThread.Invoke(() =>
    {
        ErrorHandler.CheckError(this, CanonSDK.EdsSetPropertyData(CamRef,
        propID, inParam, propBytes.Length, propBytes));
    });
}

String 长度可以通过 MAX 参数限制,因为某些相机属性只能具有特定长度。但大多数长度为 32 个字符。

获取可用设置列表

不同的相机具有不同的可用设置,镜头也只有一定的 Av 值范围。因此,您需要获取所有支持设置的列表。这仅适用于“AEModeSelect”、“ISO”、“Av”、“Tv”、“MeteringMode”和“ExposureCompensation”。此方法返回一个 CameraValue 数组。每个 CameraValue 都有一个整数值(这是 SDK 使用的 ID),一个string值(可以用作标签,例如“ISO 100”或“1/100”)以及(如果适用)一个双精度值(可用于计算,例如 ISO 100 的值为 100,Tv 值为 1/100 的值为 0.01)。

public CameraValue[] GetSettingsList(PropertyID propId)
{
    CheckState();

    if (propId == PropertyID.AEModeSelect || propId == PropertyID.ISO || 
    propId == PropertyID.Av || propId == PropertyID.Tv
        || propId == PropertyID.MeteringMode || 
        propId == PropertyID.ExposureCompensation)
    {
        CameraValue[] vals = null;
        PropertyDesc des = default(PropertyDesc);
        MainThread.Invoke(() => ErrorHandler.CheckError
        (this, CanonSDK.EdsGetPropertyDesc(CamRef, propId, out des)));
        vals = new CameraValue[des.NumElements];
        for (int i = 0; i < vals.Length; i++) 
            vals[i] = new CameraValue(des.PropDesc[i], propId);
        return vals;
    }
    else throw new ArgumentException($"Method cannot be used with Property ID {propId}");
}

在该库中,还有五个对应类型的类支持此方法。这些类包含所有可能值的列表以及一些常用字段(如AutoBulb)。这些类称为AvValuesTvValuesISOValuesExpCompValuesAEModeValuesMeteringModeValues。这些类中的所有方法、属性和字段都是静态的。

向相机发送命令

通过相机命令,您可以控制多项操作,例如,拍照、按下快门按钮或驱动镜头的对焦马达。

public void SendCommand(CameraCommand command, int inParam = 0)
{
    CheckState();
    MainThread.Invoke(() => ErrorHandler.CheckError
    (this, CanonSDK.EdsSendCommand(CamRef, command, inParam)));
}

其中一些命令将在以下主题中使用。

要更改相机的锁定状态或进入/退出直接传输模式,您可以使用相机状态命令

public void SendCommand(CameraCommand command, int inParam = 0)
{
    CheckState();
    MainThread.Invoke(() => ErrorHandler.CheckError
               (this, CanonSDK.EdsSendCommand(CamRef, command, inParam)));
}

拍照(普通模式和 B 门模式)

要使用当前设置拍照,请调用 TakePhoto 方法

public void TakePhoto()
{
    CheckState();
    SendCommand(CameraCommand.TakePicture);
}

或者,您可以使用 TakePhotoAsync 方法,该方法异步执行命令(在Threadpool线程上,而不是在 .NET 4.5 任务上)。

或者,如果您的相机支持,您可以使用 TakePhoto快门 方法,该方法使用 PressShutterButton 命令而不是 TakePicture 命令

public void TakePhotoShutter()
{
    CheckState();
    SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.Completely);
    SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.OFF);
}

与之前一样,您也可以使用 TakePhoto快门Async 方法进行异步执行。

要在 B 门模式下拍照,请调用 TakePhotoB门 方法

public void TakePhotoBulb(int bulbTime)
{
    CheckState();

    SendCommand(CameraCommand.BulbStart);
    Thread.Sleep(bulbTime);
    SendCommand(CameraCommand.BulbEnd);
}

请注意,在调用此方法之前,您必须将 Tv 值设置为Bulb,否则将出错。反之亦然,对于 TakePhoto 方法,Tv 不能设置为Bulb

如果您想直接将拍摄的照片下载到您的计算机,请参阅下一个主题。

将拍摄的照片下载到计算机

要将拍摄的照片直接保存到计算机而不是相机内存中,请通过调用 SetSetting 方法进行设置

SetSetting(PropertyID.SaveTo, (int)SaveTo.Host); //SaveTo.Both would save the image 
                                                 //to the camera AND the computer

大多数相机要求您在拍照前设置计算机上的剩余磁盘空间。如果这样做,大多数相机将假设没有剩余磁盘空间并返回错误。

您可以调用 SetCapacity 方法设置剩余磁盘空间(如果需要,您可以设置任意高的值)

public void SetCapacity(int bytesPerSector, int numberOfFreeClusters)
{
    CheckState();
    MainThread.Invoke(() =>
    {
        Capacity capacity = new Capacity(numberOfFreeClusters, bytesPerSector, true);
        ErrorHandler.CheckError(this, CanonSDK.EdsSetCapacity(CamRef, capacity));
    });
}

一旦您拍摄了照片,SDKObjectEvent 将会触发,inEvent 变量为 ObjectEventID.DirItemRequestTransfer。为了方便起见,Camera 类有一个 DownloadReady 事件,该事件将触发并提供您所需的所有信息。

DownloadReady 事件有两个参数,一个是触发事件的 Camera 对象,第二个是 DownloadInfo 对象。DownloadInfo 对象提供有关文件的信息,并可传递给三个方法

DownloadFile 方法将图像下载到提供的目录中。要获取或设置文件名,您可以使用 DownloadInfo 对象的 FileName 属性。

public void DownloadFile(DownloadInfo Info, string directory)
{
    CheckState();
    if (Info == null) throw new ArgumentNullException(nameof(Info));
    if (directory == null || string.IsNullOrEmpty(directory.Trim())) directory = ".";

    string currentFile = Path.Combine(directory, Info.FileName);
    if (!Directory.Exists(directory)) Directory.CreateDirectory(directory);
    DownloadToFile(Info, currentFile);
}

不带目录参数的 DownloadFile 方法将图像下载到 System.IO.Stream 中,而不是将其保存到文件系统中。

public Stream DownloadFile(DownloadInfo Info)
{
    CheckState();
    if (Info == null) throw new ArgumentNullException(nameof(Info));
    return DownloadToStream(Info);
}

使用 CancelDownload 方法,您可以取消并丢弃拍摄的图像

public void CancelDownload(DownloadInfo Info)
{
    CheckState();
    if (Info == null) throw new ArgumentNullException(nameof(Info));

    MainThread.Invoke(() =>
    {
        ErrorHandler.CheckError(this, CanonSDK.EdsDownloadCancel(Info.Reference));
        ErrorHandler.CheckError(this, CanonSDK.EdsRelease(Info.Reference));
    });
}

两个 DownloadFile 方法都使用了这些子程序来执行实际工作

protected void DownloadToFile(DownloadInfo Info, string filepath)
{
    using (var stream = new SDKStream
          (filepath, FileCreateDisposition.CreateAlways, FileAccess.ReadWrite))
    {
        DownloadData(Info, stream.Reference);
    }
}

protected Stream DownloadToStream(DownloadInfo Info)
{
    SDKStream stream = new SDKStream(Info.Size64);
    DownloadData(Info, stream.Reference);
    stream.Position = 0;
    return stream;
}

SDKStream 类是 SDK 流的包装器,因此它可以像普通的 System.IO.Stream 一样使用。

这就是实际的下载

protected void DownloadData(DownloadInfo Info, IntPtr stream)
{
    MainThread.Invoke(() =>
    {
        try
        {
            //Set the progress callback
            ErrorHandler.CheckError(this, CanonSDK.EdsSetProgressCallback
            (stream, SDKProgressCallbackEvent, ProgressOption.Periodically, Info.Reference));
            //Check which SDK version is used and download the data with the correct method
            if (CanonSDK.IsVerGE34) ErrorHandler.CheckError
            (this, CanonSDK.EdsDownload(Info.Reference, Info.Size64, stream));
            else ErrorHandler.CheckError
            (this, CanonSDK.EdsDownload(Info.Reference, Info.Size, stream));
        }
        finally
        {
            //Release all data
            ErrorHandler.CheckError(this, CanonSDK.EdsDownloadComplete(Info.Reference));
            ErrorHandler.CheckError(this, CanonSDK.EdsRelease(Info.Reference));
        }
    });
}

启动和查看实时视图

实时视图是比较困难的事情之一,尤其是在需要高性能的情况下。首先,我们这样启动实时视图

public void StartLiveView()
{
    CheckState();
    if (!IsLiveViewOn) SetSetting(PropertyID.Evf_OutputDevice, (int)EvfOutputDevice.PC);
}

完成此操作后,SDKPropertyEvent 将触发,inPropertyID 变量为 PropertyID.Evf_OutputDevice

private ErrorCode Camera_SDKPropertyEvent
(PropertyEventID inEvent, PropertyID inPropertyID, int inParameter, IntPtr inContext)
{
    ThreadPool.QueueUserWorkItem((state) =>
    {
        try
        {
            if (inPropertyID == PropertyID.Evf_OutputDevice || 
                inPropertyID == PropertyID.Record)
            {
                lock (lvThreadLockObj)
                {
                    EvfOutputDevice outDevice = GetEvf_OutputDevice();
                    Recording recordState = IsRecordAvailable ? 
                    ((Recording)GetInt32Setting(PropertyID.Record)) : Recording.Off;

                    if (outDevice == EvfOutputDevice.PC || 
                    (recordState == Recording.Ready && 
                                    outDevice == EvfOutputDevice.Filming) ||
                        (useFilmingPcLv && recordState == Recording.On && 
                        (outDevice == EvfOutputDevice.Filming || 
                         outDevice == EvfOutputDevice.Camera)))
                    {
                        if (!KeepLVAlive)
                        {
                            KeepLVAlive = true;
                            LVThread = STAThread.CreateThread(DownloadEvf);
                            LVThread.Start();
                        }
                    }
                    else if (KeepLVAlive) { KeepLVAlive = false; }
                }
            }
        }
        catch (Exception ex) 
        { if (!IsDisposed && !ErrorHandler.ReportError(this, ex)) throw; }

        PropertyChanged?.Invoke(this, inEvent, inPropertyID, inParameter);
    });
    return ErrorCode.OK;
}

其中还有一些额外的检查,以确保实时视图在拍摄视频时也能正常工作。

并且 DownloadEvf 方法是

private void DownloadEvf()
{
    if (IsLiveViewOn) return;

    try
    {
        //Create variables
        IntPtr evfImageRef = IntPtr.Zero;
        ErrorCode err;

        //Create stream
        using (var stream = new SDKStream(0))
        {
            IsLiveViewOn = true;
                    
            //Run live view
            while (KeepLVAlive)
            {
                //Get live view image
                lock (STAThread.ExecLock)
                {
                    err = CanonSDK.EdsCreateEvfImageRef(stream.Reference, out evfImageRef);
                    if (err == ErrorCode.OK) err = 
                        CanonSDK.EdsDownloadEvfImage(CamRef, evfImageRef);
                }

                //Check for errors
                if (err == ErrorCode.OBJECT_NOTREADY) { continue; }
                else if (err != ErrorCode.OK) { ErrorHandler.CheckError(err); continue; }

                //Release current evf image
                CanonSDK.EdsRelease(evfImageRef);
                        
                //Set stream position back to zero
                stream.Position = 0;

                //Update live view
                LiveViewUpdated?.Invoke(this, stream);
            }
        }
    }
    catch (Exception ex) { if (ex is ThreadAbortException || 
                               !ErrorHandler.ReportError(this, ex)) throw; }
    finally
    {
        IsLiveViewOn = false;
        ThreadPool.QueueUserWorkItem((state) => LiveViewStopped?.Invoke(this));
    }
}

要停止实时视图,只需调用 StopLiveView 方法。这将再次触发 SDKPropertyEvent,其中 KeepLVAlive 变量设置为false,实时视图循环将停止。使用 LVOff 参数,您可以设置是完全关闭实时视图还是在相机上显示它。(某些旧相机可能会一直关闭。)

public void StopLiveView(bool LVOff = true)
{
    CheckState();
    if (LVOff) SetSetting(PropertyID.Evf_OutputDevice, (int)EvfOutputDevice.Off);
    else SetSetting(PropertyID.Evf_OutputDevice, (int)EvfOutputDevice.Camera);
}

录制视频

较新的相机内置了录制视频的可能性。要使用此功能,您必须首先将相机设置为视频录制模式(否则将无法工作!)。通常,您需要切换某个按钮、旋钮或拨盘。完成后,您可以调用启动方法

public void StartFilming(bool PCLiveview)
{
    CheckState();

    Recording state = (Recording)GetInt32Setting(PropertyID.Record);
    if (state != Recording.On)
    {
        if (state != Recording.Ready) throw new InvalidOperationException
        ("The camera is not ready to film. The Record property has to be Recording.Ready");
        useFilmingPcLv = PCLiveview;
        //When recording videos, it has to be saved on the camera internal memory
        SetSetting(PropertyID.SaveTo, (int)SaveTo.Camera);
        //Start the video recording
        SetSetting(PropertyID.Record, (int)Recording.On);
    }
}

请注意,SaveTo 属性设置为Camera。这是 SDK 的要求,据推测是因为传输到计算机的数据速度太慢。

尽管如此,您仍然可以在停止拍摄后通过将 StopFilming 方法的 saveFilm 参数设置为true来下载已完成的影片

public void StopFilming(bool saveFilm, bool stopLiveView)
{
    CheckState();

    Recording state = (Recording)GetInt32Setting(PropertyID.Record);
    if (state == Recording.On)
    {
        this.saveFilm = saveFilm;
        //Stop video recording
        SetSetting(PropertyID.Record, (int)Recording.Off);
        useFilmingPcLv = false;
        if (IsLiveViewOn && stopLiveView) StopLiveView(false);
    }
}

内部,它会检查 SDKObjectEvent,该事件将触发,inEventObjectEventID.DirItemCreated。与下载照片一样,DownloadReady 事件将触发,您可以使用 DownloadFile 方法之一下载影片。

锁定/解锁相机 UI

为了防止用户更改物理相机上的设置,或者允许更改,您可以像这样锁定或解锁相机 UI

public void UILock(bool lockState)
{
    if (lockState) SendStatusCommand(CameraStatusCommand.UILock);
    else SendStatusCommand(CameraStatusCommand.UIUnLock);
}

控制镜头的对焦

互联网上经常被问到的一个问题是如何控制相机的对焦。如果您知道方法,其实很简单。最重要的是,相机必须处于实时视图模式,并且镜头必须设置为 AF。然后,您只需调用 SendCommand 并将 DriveLensEvf 作为命令类型

SendCommand(CameraCommand.DriveLensEvf, (int)DriveLens.Near2);

要控制距离和方向,您需要相应地设置 DriveLens 枚举。Near 表示对焦更近,Far 表示对焦更远,1 表示小步,2 表示中等步,3 表示大步。

相机上的文件和文件夹

有几种方法可以处理相机的文件系统

  • 使用 GetAllVolumes 获取所有相机卷(例如 CF 或 SD 卡)
  • 使用 GetAllEntries 获取相机上的所有卷、文件和文件夹
  • 使用 GetAllImages 获取相机上的所有图像
  • 使用 FormatVolume 格式化卷
  • 使用 DownloadFiles 将一个或多个文件下载到目录中
  • 使用 DeleteFiles 删除相机上的一个或多个文件

我不会在这里详细介绍,但请随时查看这些方法的源代码,以了解它们的确切工作方式。

获取图像缩略图

有时,获取图像的缩略图很有用,无论它是 RAW 还是 JPG。为此,您可以调用 CanonAPI 类的 GetFileThumb 方法。

public Bitmap GetFileThumb(string filepath)
{
    //create a file stream to given file
    using (var stream = new SDKStream
          (filepath, FileCreateDisposition.OpenExisting, FileAccess.Read))
    {
        //Create a thumbnail Bitmap from the stream
        return GetImage(stream.Reference, ImageSource.Thumbnail);
    }
}

而更通用的方法 GetImage 定义如下

protected Bitmap GetImage(IntPtr imgStream, ImageSource imageSource)
{
    IntPtr imgRef = IntPtr.Zero;
    IntPtr streamPointer = IntPtr.Zero;
    ImageInfo imageInfo;

    try
    {
        //create reference and get image info
        ErrorHandler.CheckError(this, 
            CanonSDK.EdsCreateImageRef(imgStream, out imgRef));
        ErrorHandler.CheckError(this, 
            CanonSDK.EdsGetImageInfo(imgRef, imageSource, out imageInfo));

        Size outputSize = new Size();
        outputSize.Width = imageInfo.EffectiveRect.Width;
        outputSize.Height = imageInfo.EffectiveRect.Height;
        //calculate amount of data
        int datalength = outputSize.Height * outputSize.Width * 3;
        //create buffer that stores the image
        byte[] buffer = new byte[datalength];
        //create a stream to the buffer
        using (var stream = new SDKStream(buffer))
        {
            //load image into the buffer
            ErrorHandler.CheckError(this, CanonSDK.EdsGetImage
            (imgRef, imageSource, TargetImageType.RGB, 
             imageInfo.EffectiveRect, outputSize, stream.Reference));

            //make BGR from RGB (System.Drawing (i.e. GDI+) uses BGR)
            unsafe
            {
                byte tmp;
                fixed (byte* pix = buffer)
                {
                    for (long i = 0; i < datalength; i += 3)
                    {
                        tmp = pix[i];        //Save B value
                        pix[i] = pix[i + 2]; //Set B value with R value
                        pix[i + 2] = tmp;    //Set R value with B value
                    }
                }
            }

            //Get pointer to stream data
            ErrorHandler.CheckError(this, 
                CanonSDK.EdsGetPointer(stream.Reference, out streamPointer));
            //Create bitmap with the data in the buffer
            return new Bitmap(outputSize.Width, outputSize.Height, 
            datalength, PixelFormat.Format24bppRgb, streamPointer);
        }
    }
    finally
    {
        //Release all data
        if (imgStream != IntPtr.Zero) ErrorHandler.CheckError
           (this, CanonSDK.EdsRelease(imgStream));
        if (imgRef != IntPtr.Zero) ErrorHandler.CheckError
           (this, CanonSDK.EdsRelease(imgRef));
    }
}

关于方法的说明

我没有在这里添加代码中使用的所有方法。如果有些地方不太清楚,请下载源代码。如果您在此之后仍有疑问,请随时问我。 :)

关于线程的说明

Canon SDK 要求使用单线程单元(=STA)中的线程,这使得事情有些棘手。幸运的是,我添加了 STAThread 类,它处理这种线程模型的设置和问题。CanonAPI 类有一个用于初始化和终止 SDK 以及获取所有 SDK 事件的线程。每个 Camera 实例都有一个用于在上面执行所有命令的线程。为了确保没有任何东西同时执行,所有命令都用锁包围(以 ExecLock 字段作为锁对象)。如果两个命令同时执行,SDK 在大多数情况下会完全挂起。CanonAPICamera 提供的所有方法都可以安全执行,但如果您想调用 CanonSDK 类的方法,您必须确保在正确的线程上执行它(通过调用 STAThread 类的 Invoke 方法)。如果您在 SDK 挂起方面遇到任何问题,请告诉我。

使用 GUI

此项目包括一个 Windows Forms UI 和一个 WPF UI。它们的工作方式相同,旨在向您展示如何在实际程序中使用上述代码。

注意这些 GUI 不适用于生产环境,仅作为示例供您入门!

插入您的相机并在列表中选择。点击“打开会话”并开始使用相机。

在下拉菜单中选择值以在相机中设置它们。

使用“StartLV”按钮启动实时视图,使用带有箭头的按钮可以控制对焦(仅当镜头处于 AF 模式且实时视图正在运行时才有效)。

关注点

此代码已在以下设备上测试过

  • EOS 5D Mark III
  • EOS 7D
  • EOS 40D
  • EOS 60D
  • EOS 700D
  • EOS 600D
  • EOS 550D
  • EOS 500D
  • EOS 450D
  • EOS 100D/Rebel SL1
  • EOS 1200D/Rebel T5
  • EOS 1100D/Rebel T3
  • 以及其他一些型号

如果您在其他型号上进行过测试,请告诉我,以便我将其添加到此列表中。

如果您发现了 bug、有改进建议或新的代码片段,我很高兴收到您的来信!

历史

  • 2013 年 11 月 - 初始版本
  • 2013 年 11 月 - 修复了在查看LiveView和拍照同时进行时的 bug
  • 2013 年 12 月 - 添加了GetStringPropertyGetStructProperty,并添加了视频录制
  • 2014 年 1 月 - 添加了SetStringPropertySetStructProperty,并对 UI 进行了小的更改
  • 2014 年 1 月 - 添加了多个新方法,并使代码更加安全
  • 2014 年 6 月 - 更改了一些方法以提高安全性,并对代码和 UI 进行了少量修改
  • 2014 年 10 月 - 修复了一些 bug,修改了 Winforms UI 并添加了 WPF UI
  • 2014 年 11 月 - 修复了实时视图运行时调用相机命令时的死锁问题
  • 2015 年 4 月 - 修复了相机在拍摄前后挂起的 bug。修复了损坏的链接
  • 2015 年 5 月 - 相机命令现在在 STA 线程上正确执行。其他小修复
  • 2015 年 8 月 - UI:修复了CameraAdded+Shutdown bug;添加了基本错误处理。Lib:小的修复

  • 2016 年 3 月(1.0.0) - 项目完全修订。现在它使用了与商业库类似的架构。
  • 2016 年 3 月(1.0.1) - 修复了StopFilming方法中的错误检查
  • 2016 年 4 月(1.1.0) - 多项 bug 修复、改进以及对 Canon SDK 3.4 的支持
  • 2016 年 4 月(1.1.1) - 修复了STAThread.Invoke方法中的不正确锁定(感谢 Dave)
  • 2016 年 5 月(1.1.2) - 各种小修复(感谢 elgarf)并添加了LiveViewStopped事件
© . All rights reserved.