在Windows系统上使用Golang操作摄像头设备

在Windows系统上使用Golang操作摄像头设备 大家好,我遇到了以下代码的一些问题,该代码旨在使用 Windows API 拍摄照片。当我运行这段代码时,我的网络摄像头指示灯会亮起几秒钟(表明摄像头已被调用),但保存的 PNG 图像却全是黑色像素。

我找不到问题所在。有人能帮我找到解决方案吗?

package main

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"syscall"
	"unsafe"
)

var (
	avicap32                    = syscall.NewLazyDLL("avicap32.dll")
	proccapCreateCaptureWindowA = avicap32.NewProc("capCreateCaptureWindowA")

	user32           = syscall.NewLazyDLL("user32.dll")
	procSendMessageA = user32.NewProc("SendMessageA")
)

func CaptureWebcam() {
	var name = "WebcamCapture"
	handle, _, _ := proccapCreateCaptureWindowA.Call(uintptr(unsafe.Pointer(&name)), 0, 0, 0, 320, 240, 0, 0)
	procSendMessageA.Call(handle, 0x40A, 0, 0)  //WM_CAP_DRIVER_CONNECT
	procSendMessageA.Call(handle, 0x432, 30, 0) //WM_CAP_SET_PREVIEW
	procSendMessageA.Call(handle, 0x43C, 0, 0)  //WM_CAP_GRAB_FRAME
	procSendMessageA.Call(handle, 0x41E, 0, 0)  //WM_CAP_EDIT_COPY
	procSendMessageA.Call(handle, 0x40B, 0, 0)  //WM_CAP_DRIVER_DISCONNECT
	camera, err := os.Create("Image.png")
	if err != nil {
		fmt.Println(err)
		return
	}
	clip, err := readClipboard()
	if err != nil {
		fmt.Println(err)
		return
	}
	_, err = io.Copy(camera, clip)
	if err != nil {
		fmt.Println(err)
		return
	}
	camera.Close()
}

func readClipboard() (io.Reader, error) {
	f, err := ioutil.TempFile("", "")
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	f.Close()
	_, err = exec.Command("PowerShell", "-Command", "Add-Type", "-AssemblyName", fmt.Sprintf("System.Windows.Forms;$clip=[Windows.Forms.Clipboard]::GetImage();if ($clip -ne $null) { $clip.Save('%s') };", f.Name())).CombinedOutput()
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	r := new(bytes.Buffer)
	file, err := os.Open(f.Name())
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	if _, err := io.Copy(r, file); err != nil {
		fmt.Println(err)
		return nil, err
	}
	file.Close()
	os.Remove(f.Name())
	return r, nil
}

func main() {
	CaptureWebcam()
}

更多关于在Windows系统上使用Golang操作摄像头设备的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我开发了一个用于在Windows上访问摄像头的Go库,它基于Windows Media Foundation API。它通过DLL工作,不需要CGO。

Github: Kirizu-Official/windows-camera-go: 📷 A Go camera library for Windows, built on Windows Media Foundation.

更多关于在Windows系统上使用Golang操作摄像头设备的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


问题出在代码没有给摄像头足够的时间来捕获和传输帧数据。WM_CAP_GRAB_FRAME 是同步操作,但需要等待帧数据就绪。以下是修正后的代码:

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
    "os/exec"
    "syscall"
    "time"
    "unsafe"
)

var (
    avicap32                    = syscall.NewLazyDLL("avicap32.dll")
    proccapCreateCaptureWindowA = avicap32.NewProc("capCreateCaptureWindowA")
    
    user32           = syscall.NewLazyDLL("user32.dll")
    procSendMessageA = user32.NewProc("SendMessageA")
    procSetWindowPos = user32.NewProc("SetWindowPos")
)

const (
    WM_CAP_DRIVER_CONNECT     = 0x40A
    WM_CAP_DRIVER_DISCONNECT  = 0x40B
    WM_CAP_SET_PREVIEW        = 0x432
    WM_CAP_SET_PREVIEWRATE    = 0x434
    WM_CAP_GRAB_FRAME         = 0x43C
    WM_CAP_EDIT_COPY          = 0x41E
    WM_CAP_SET_SCALE          = 0x433
    WM_CAP_DLG_VIDEOFORMAT    = 0x419
    WM_CAP_DLG_VIDEOSOURCE    = 0x41A
    WM_CAP_DLG_VIDEODISPLAY   = 0x41B
    WM_CAP_FILE_SAVEDIB       = 0x419
    SWP_NOZORDER              = 0x4
    SWP_NOMOVE                = 0x2
    SWP_NOSIZE                = 0x1
)

func CaptureWebcam() error {
    // 创建捕获窗口
    windowName := "WebcamCapture\x00"
    hwnd, _, _ := proccapCreateCaptureWindowA.Call(
        uintptr(unsafe.Pointer(&[]byte(windowName)[0])),
        0,
        0, 0, 640, 480,
        0, 0,
    )
    
    if hwnd == 0 {
        return fmt.Errorf("无法创建捕获窗口")
    }
    
    // 隐藏窗口
    procSetWindowPos.Call(hwnd, 0, 0, 0, 0, 0, SWP_NOZORDER|SWP_NOMOVE|SWP_NOSIZE)
    
    // 连接摄像头驱动
    ret, _, _ := procSendMessageA.Call(hwnd, WM_CAP_DRIVER_CONNECT, 0, 0)
    if ret == 0 {
        return fmt.Errorf("无法连接摄像头驱动")
    }
    
    // 设置预览
    procSendMessageA.Call(hwnd, WM_CAP_SET_PREVIEW, 1, 0)
    procSendMessageA.Call(hwnd, WM_CAP_SET_PREVIEWRATE, 30, 0)
    procSendMessageA.Call(hwnd, WM_CAP_SET_SCALE, 1, 0)
    
    // 等待摄像头初始化
    time.Sleep(2 * time.Second)
    
    // 抓取帧
    ret, _, _ = procSendMessageA.Call(hwnd, WM_CAP_GRAB_FRAME, 0, 0)
    if ret == 0 {
        procSendMessageA.Call(hwnd, WM_CAP_DRIVER_DISCONNECT, 0, 0)
        return fmt.Errorf("无法抓取帧")
    }
    
    // 复制到剪贴板
    ret, _, _ = procSendMessageA.Call(hwnd, WM_CAP_EDIT_COPY, 0, 0)
    if ret == 0 {
        procSendMessageA.Call(hwnd, WM_CAP_DRIVER_DISCONNECT, 0, 0)
        return fmt.Errorf("无法复制到剪贴板")
    }
    
    // 等待剪贴板操作完成
    time.Sleep(500 * time.Millisecond)
    
    // 断开连接
    procSendMessageA.Call(hwnd, WM_CAP_DRIVER_DISCONNECT, 0, 0)
    
    // 从剪贴板读取图像
    imgData, err := readClipboardImage()
    if err != nil {
        return fmt.Errorf("读取剪贴板失败: %v", err)
    }
    
    // 保存图像
    if err := os.WriteFile("capture.png", imgData, 0644); err != nil {
        return fmt.Errorf("保存文件失败: %v", err)
    }
    
    fmt.Println("图像已保存为 capture.png")
    return nil
}

func readClipboardImage() ([]byte, error) {
    // 创建临时文件
    tmpFile, err := os.CreateTemp("", "clipboard_*.png")
    if err != nil {
        return nil, err
    }
    tmpPath := tmpFile.Name()
    tmpFile.Close()
    defer os.Remove(tmpPath)
    
    // 使用PowerShell从剪贴板读取图像
    psCmd := fmt.Sprintf(
        `Add-Type -AssemblyName System.Windows.Forms;
        $img = [Windows.Forms.Clipboard]::GetImage();
        if ($img -ne $null) { 
            $img.Save('%s', [System.Drawing.Imaging.ImageFormat]::Png);
            Write-Output "OK";
        } else { 
            Write-Output "ERROR: No image in clipboard";
        }`,
        tmpPath,
    )
    
    cmd := exec.Command("powershell", "-Command", psCmd)
    output, err := cmd.CombinedOutput()
    if err != nil {
        return nil, fmt.Errorf("PowerShell执行失败: %v, 输出: %s", err, output)
    }
    
    if !bytes.Contains(output, []byte("OK")) {
        return nil, fmt.Errorf("剪贴板中没有图像数据")
    }
    
    // 读取图像数据
    imgData, err := os.ReadFile(tmpPath)
    if err != nil {
        return nil, fmt.Errorf("读取临时文件失败: %v", err)
    }
    
    // 验证PNG文件头
    if len(imgData) < 8 || string(imgData[:8]) != "\x89PNG\r\n\x1a\n" {
        return nil, fmt.Errorf("无效的PNG数据")
    }
    
    return imgData, nil
}

func main() {
    if err := CaptureWebcam(); err != nil {
        fmt.Printf("捕获失败: %v\n", err)
        os.Exit(1)
    }
}

主要修改:

  1. 增加了窗口隐藏和摄像头初始化等待时间
  2. 添加了错误检查,确保每个步骤都成功执行
  3. 改进了剪贴板读取逻辑,直接保存为PNG格式
  4. 增加了PNG文件头验证
  5. 将分辨率从320x240提高到640x480以获得更好的图像质量

运行这个修正版本应该能正确捕获摄像头图像。如果仍然得到黑色图像,可能是摄像头需要更长的初始化时间,可以尝试增加第53行的等待时间。

回到顶部